mirror of
https://github.com/maputnik/editor.git
synced 2026-07-25 23:37:27 +00:00
Merge branch 'main' into codex/replace-react-sortable-hoc-with-dnd-kit
This commit is contained in:
@@ -0,0 +1,43 @@
|
|||||||
|
Maputnik is a MapLibre style editor written using React and TypeScript.
|
||||||
|
|
||||||
|
To get started, install all npm packages:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify code correctness by running ESLint:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
Or try fixing lint issues with:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run lint -- --fix
|
||||||
|
```
|
||||||
|
|
||||||
|
The project type checked and built with:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
To run the tests make sure that xvfb is installed:
|
||||||
|
|
||||||
|
```
|
||||||
|
apt run xvfb
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the development server in the background with Vite:
|
||||||
|
|
||||||
|
```
|
||||||
|
nohup npm run start &
|
||||||
|
```
|
||||||
|
|
||||||
|
Then start the Cypress tests with:
|
||||||
|
|
||||||
|
```
|
||||||
|
xvfb-run -a npm run test
|
||||||
|
```
|
||||||
@@ -14,11 +14,13 @@
|
|||||||
- When loading a style into localStorage that causes a QuotaExceededError, purge localStorage and retry
|
- When loading a style into localStorage that causes a QuotaExceededError, purge localStorage and retry
|
||||||
- Remove react-autobind dependency
|
- Remove react-autobind dependency
|
||||||
- Remove usage of legacy `childContextTypes` API
|
- Remove usage of legacy `childContextTypes` API
|
||||||
|
- Replace react-autocomplete with Downshift in the autocomplete component
|
||||||
- _...Add new stuff here..._
|
- _...Add new stuff here..._
|
||||||
|
|
||||||
### 🐞 Bug fixes
|
### 🐞 Bug fixes
|
||||||
|
|
||||||
- Fix incorrect handing of network error response (#944)
|
- Fix incorrect handing of network error response (#944)
|
||||||
|
- Show an error when adding a layer with a duplicate ID
|
||||||
- _...Add new stuff here..._
|
- _...Add new stuff here..._
|
||||||
|
|
||||||
## 2.1.1
|
## 2.1.1
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export default defineConfig({
|
|||||||
exclude: "cypress/**/*.*",
|
exclude: "cypress/**/*.*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
e2e: {
|
e2e: {
|
||||||
setupNodeEvents(on, config) {
|
setupNodeEvents(on, config) {
|
||||||
// implement node event listeners here
|
// implement node event listeners here
|
||||||
@@ -20,4 +21,11 @@ export default defineConfig({
|
|||||||
openMode: 0,
|
openMode: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
component: {
|
||||||
|
devServer: {
|
||||||
|
framework: "react",
|
||||||
|
bundler: "vite",
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -272,6 +272,22 @@ describe("modals", () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("add layer", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
when.setStyle("layer");
|
||||||
|
when.modal.open();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows duplicate id error", () => {
|
||||||
|
when.setValue("add-layer.layer-id.input", "background");
|
||||||
|
when.click("add-layer");
|
||||||
|
then(get.elementByTestId("modal:add-layer")).shouldExist();
|
||||||
|
then(get.element(".maputnik-modal-error")).shouldContainText(
|
||||||
|
"Layer ID already exists"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("sources", () => {
|
describe("sources", () => {
|
||||||
it("toggle");
|
it("toggle");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<title>Components App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div data-cy-root></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// ***********************************************************
|
||||||
|
// This example support/component.ts is processed and
|
||||||
|
// loaded automatically before your test files.
|
||||||
|
//
|
||||||
|
// This is a great place to put global configuration and
|
||||||
|
// behavior that modifies Cypress.
|
||||||
|
//
|
||||||
|
// You can change the location of this file or turn off
|
||||||
|
// automatically serving support files with the
|
||||||
|
// 'supportFile' configuration option.
|
||||||
|
//
|
||||||
|
// You can read more here:
|
||||||
|
// https://on.cypress.io/configuration
|
||||||
|
// ***********************************************************
|
||||||
|
|
||||||
|
// Import commands.js using ES2015 syntax:
|
||||||
|
import './commands'
|
||||||
|
|
||||||
|
import { mount } from 'cypress/react'
|
||||||
|
|
||||||
|
// Augment the Cypress namespace to include type definitions for
|
||||||
|
// your custom command.
|
||||||
|
// Alternatively, can be defined in cypress/support/component.d.ts
|
||||||
|
// with a <reference path="./component" /> at the top of your spec.
|
||||||
|
declare global {
|
||||||
|
/* eslint-disable @typescript-eslint/no-namespace */
|
||||||
|
namespace Cypress {
|
||||||
|
interface Chainable {
|
||||||
|
mount: typeof mount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Cypress.Commands.add('mount', mount)
|
||||||
|
|
||||||
|
// Example use:
|
||||||
|
// cy.mount(<MyComponent />)
|
||||||
Generated
+29
-30
@@ -24,6 +24,7 @@
|
|||||||
"codemirror": "^5.65.19",
|
"codemirror": "^5.65.19",
|
||||||
"color": "^5.0.0",
|
"color": "^5.0.0",
|
||||||
"detect-browser": "^5.3.0",
|
"detect-browser": "^5.3.0",
|
||||||
|
"downshift": "^9.0.9",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"i18next": "^25.3.1",
|
"i18next": "^25.3.1",
|
||||||
@@ -48,7 +49,6 @@
|
|||||||
"react-accessible-accordion": "^5.0.1",
|
"react-accessible-accordion": "^5.0.1",
|
||||||
"react-aria-menubutton": "^7.0.3",
|
"react-aria-menubutton": "^7.0.3",
|
||||||
"react-aria-modal": "^5.0.2",
|
"react-aria-modal": "^5.0.2",
|
||||||
"react-autocomplete": "^1.8.1",
|
|
||||||
"react-collapse": "^5.1.1",
|
"react-collapse": "^5.1.1",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
@@ -83,7 +83,6 @@
|
|||||||
"@types/react": "^18.2.67",
|
"@types/react": "^18.2.67",
|
||||||
"@types/react-aria-menubutton": "^6.2.14",
|
"@types/react-aria-menubutton": "^6.2.14",
|
||||||
"@types/react-aria-modal": "^5.0.0",
|
"@types/react-aria-modal": "^5.0.0",
|
||||||
"@types/react-autocomplete": "^1.8.11",
|
|
||||||
"@types/react-collapse": "^5.0.4",
|
"@types/react-collapse": "^5.0.4",
|
||||||
"@types/react-color": "^3.0.13",
|
"@types/react-color": "^3.0.13",
|
||||||
"@types/react-dom": "^18.2.22",
|
"@types/react-dom": "^18.2.22",
|
||||||
@@ -3168,16 +3167,6 @@
|
|||||||
"focus-trap": "^7.4.3"
|
"focus-trap": "^7.4.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/react-autocomplete": {
|
|
||||||
"version": "1.8.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-autocomplete/-/react-autocomplete-1.8.11.tgz",
|
|
||||||
"integrity": "sha512-JYaD/OGfVFMK5NaGOCAd25QRs4MEevapn38xvYWjwo5brxrUMo2PucYmShTfuTX99r80UtncCMDrFZ9MGIVyvQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/react": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/react-collapse": {
|
"node_modules/@types/react-collapse": {
|
||||||
"version": "5.0.4",
|
"version": "5.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-collapse/-/react-collapse-5.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-collapse/-/react-collapse-5.0.4.tgz",
|
||||||
@@ -4744,6 +4733,12 @@
|
|||||||
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
|
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/compute-scroll-into-view": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -5265,11 +5260,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dom-scroll-into-view": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/dom-scroll-into-view/-/dom-scroll-into-view-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-1Dmy6uH1vRcm2+Lvggyrlc04cMh+mr+VA+qcgs085hAEZp+v+6NT/xhRjfc6vRc7965sCSDdQcw063VkG+eNmQ=="
|
|
||||||
},
|
|
||||||
"node_modules/dom-serializer": {
|
"node_modules/dom-serializer": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
@@ -5335,6 +5325,28 @@
|
|||||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/downshift": {
|
||||||
|
"version": "9.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/downshift/-/downshift-9.0.9.tgz",
|
||||||
|
"integrity": "sha512-ygOT8blgiz5liDuEFAIaPeU4dDEa+w9p6PHVUisPIjrkF5wfR59a52HpGWAVVMoWnoFO8po2mZSScKZueihS7g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.24.5",
|
||||||
|
"compute-scroll-into-view": "^3.1.0",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
|
"react-is": "18.2.0",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/downshift/node_modules/react-is": {
|
||||||
|
"version": "18.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
|
||||||
|
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
@@ -10817,19 +10829,6 @@
|
|||||||
"react-dom": ">=16.3.0"
|
"react-dom": ">=16.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-autocomplete": {
|
|
||||||
"version": "1.8.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-autocomplete/-/react-autocomplete-1.8.1.tgz",
|
|
||||||
"integrity": "sha512-YQGVN5POdcI3G89wUVWnJhk9rLF6JeB6Ik6xnNpfvSMG4tJkksBzqOE4mkFNGqEz+2AaQw13xNmVXresg9E3zg==",
|
|
||||||
"dependencies": {
|
|
||||||
"dom-scroll-into-view": "1.0.1",
|
|
||||||
"prop-types": "^15.5.10"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^0.14.7 || ^15.0.0-0 || ^16.0.0-0",
|
|
||||||
"react-dom": "^0.14.7 || ^15.0.0-0 || ^16.0.0-0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-collapse": {
|
"node_modules/react-collapse": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-collapse/-/react-collapse-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-collapse/-/react-collapse-5.1.1.tgz",
|
||||||
|
|||||||
+1
-2
@@ -60,7 +60,7 @@
|
|||||||
"react-accessible-accordion": "^5.0.1",
|
"react-accessible-accordion": "^5.0.1",
|
||||||
"react-aria-menubutton": "^7.0.3",
|
"react-aria-menubutton": "^7.0.3",
|
||||||
"react-aria-modal": "^5.0.2",
|
"react-aria-modal": "^5.0.2",
|
||||||
"react-autocomplete": "^1.8.1",
|
"downshift": "^9.0.9",
|
||||||
"react-collapse": "^5.1.1",
|
"react-collapse": "^5.1.1",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
@@ -114,7 +114,6 @@
|
|||||||
"@types/react": "^18.2.67",
|
"@types/react": "^18.2.67",
|
||||||
"@types/react-aria-menubutton": "^6.2.14",
|
"@types/react-aria-menubutton": "^6.2.14",
|
||||||
"@types/react-aria-modal": "^5.0.0",
|
"@types/react-aria-modal": "^5.0.0",
|
||||||
"@types/react-autocomplete": "^1.8.11",
|
|
||||||
"@types/react-collapse": "^5.0.4",
|
"@types/react-collapse": "^5.0.4",
|
||||||
"@types/react-color": "^3.0.13",
|
"@types/react-color": "^3.0.13",
|
||||||
"@types/react-dom": "^18.2.22",
|
"@types/react-dom": "^18.2.22",
|
||||||
|
|||||||
@@ -876,8 +876,8 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
this.setModal(modalName, !this.state.isOpen[modalName]);
|
this.setModal(modalName, !this.state.isOpen[modalName]);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSetFileHandle(fileHandle: FileSystemFileHandle | null) {
|
onSetFileHandle = (fileHandle: FileSystemFileHandle | null) => {
|
||||||
this.setState({fileHandle: fileHandle});
|
this.setState({ fileHandle });
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeOpenlayersDebug = (key: keyof AppState["openlayersDebugOptions"], value: boolean) => {
|
onChangeOpenlayersDebug = (key: keyof AppState["openlayersDebugOptions"], value: boolean) => {
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ class FieldSourceLayerInternal extends React.Component<FieldSourceLayerInternalP
|
|||||||
error={this.props.error}
|
error={this.props.error}
|
||||||
>
|
>
|
||||||
<InputAutocomplete
|
<InputAutocomplete
|
||||||
keepMenuWithinWindowBounds={!!this.props.isFixed}
|
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
onChange={this.props.onChange}
|
onChange={this.props.onChange}
|
||||||
options={this.props.sourceLayerIds?.map(l => [l, l])}
|
options={this.props.sourceLayerIds?.map(l => [l, l])}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import InputAutocomplete from './InputAutocomplete'
|
||||||
|
import { mount } from 'cypress/react'
|
||||||
|
|
||||||
|
const fruits = ['apple', 'banana', 'cherry'];
|
||||||
|
|
||||||
|
describe('<InputAutocomplete />', () => {
|
||||||
|
it('filters options when typing', () => {
|
||||||
|
mount(
|
||||||
|
<InputAutocomplete aria-label="Fruit" options={fruits.map(f => [f, f])} />
|
||||||
|
);
|
||||||
|
cy.get('input').focus();
|
||||||
|
cy.get('.maputnik-autocomplete-menu-item').should('have.length', 3);
|
||||||
|
cy.get('input').type('ch');
|
||||||
|
cy.get('.maputnik-autocomplete-menu-item').should('have.length', 1).and('contain', 'cherry');
|
||||||
|
cy.get('.maputnik-autocomplete-menu-item').click();
|
||||||
|
cy.get('input').should('have.value', 'cherry');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,100 +1,116 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import Autocomplete from 'react-autocomplete'
|
import {useCombobox} from 'downshift'
|
||||||
|
|
||||||
|
const MAX_HEIGHT = 140
|
||||||
const MAX_HEIGHT = 140;
|
|
||||||
|
|
||||||
export type InputAutocompleteProps = {
|
export type InputAutocompleteProps = {
|
||||||
value?: string
|
value?: string
|
||||||
options: any[]
|
options?: any[]
|
||||||
onChange(value: string | undefined): unknown
|
onChange?(value: string | undefined): unknown
|
||||||
keepMenuWithinWindowBounds?: boolean
|
|
||||||
'aria-label'?: string
|
'aria-label'?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class InputAutocomplete extends React.Component<InputAutocompleteProps> {
|
export default function InputAutocomplete({
|
||||||
state = {
|
value,
|
||||||
maxHeight: MAX_HEIGHT
|
options = [],
|
||||||
}
|
onChange = () => {},
|
||||||
|
'aria-label': ariaLabel,
|
||||||
|
}: InputAutocompleteProps) {
|
||||||
|
const [input, setInput] = React.useState(value || '')
|
||||||
|
const menuRef = React.useRef<HTMLDivElement>(null)
|
||||||
|
const [maxHeight, setMaxHeight] = React.useState(MAX_HEIGHT)
|
||||||
|
|
||||||
autocompleteMenuEl: HTMLDivElement | null = null;
|
const filteredItems = React.useMemo(() => {
|
||||||
|
const lv = input.toLowerCase()
|
||||||
|
return options.filter((item) => item[0].toLowerCase().includes(lv))
|
||||||
|
}, [options, input])
|
||||||
|
|
||||||
static defaultProps = {
|
const calcMaxHeight = React.useCallback(() => {
|
||||||
onChange: () => {},
|
if (menuRef.current) {
|
||||||
options: [],
|
const space = window.innerHeight - menuRef.current.getBoundingClientRect().top
|
||||||
}
|
setMaxHeight(Math.min(space, MAX_HEIGHT))
|
||||||
|
|
||||||
calcMaxHeight() {
|
|
||||||
if(this.props.keepMenuWithinWindowBounds) {
|
|
||||||
const maxHeight = window.innerHeight - this.autocompleteMenuEl!.getBoundingClientRect().top;
|
|
||||||
const limitedMaxHeight = Math.min(maxHeight, MAX_HEIGHT);
|
|
||||||
|
|
||||||
if(limitedMaxHeight != this.state.maxHeight) {
|
|
||||||
this.setState({
|
|
||||||
maxHeight: limitedMaxHeight
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}, [])
|
||||||
|
|
||||||
componentDidMount() {
|
const {
|
||||||
this.calcMaxHeight();
|
isOpen,
|
||||||
}
|
getMenuProps,
|
||||||
|
getInputProps,
|
||||||
|
getItemProps,
|
||||||
|
highlightedIndex,
|
||||||
|
openMenu,
|
||||||
|
} = useCombobox({
|
||||||
|
items: filteredItems,
|
||||||
|
inputValue: input,
|
||||||
|
itemToString: (item) => (item ? item[0] : ''),
|
||||||
|
stateReducer: (_state, action) => {
|
||||||
|
if (action.type === useCombobox.stateChangeTypes.InputClick) {
|
||||||
|
return {...action.changes, isOpen: true}
|
||||||
|
}
|
||||||
|
return action.changes
|
||||||
|
},
|
||||||
|
onSelectedItemChange: ({selectedItem}) => {
|
||||||
|
const v = selectedItem ? selectedItem[0] : ''
|
||||||
|
setInput(v)
|
||||||
|
onChange(selectedItem ? selectedItem[0] : undefined)
|
||||||
|
},
|
||||||
|
onInputValueChange: ({inputValue: v}) => {
|
||||||
|
if (typeof v === 'string') {
|
||||||
|
setInput(v)
|
||||||
|
onChange(v === '' ? undefined : v)
|
||||||
|
openMenu()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
componentDidUpdate() {
|
React.useEffect(() => {
|
||||||
this.calcMaxHeight();
|
if (isOpen) {
|
||||||
}
|
calcMaxHeight()
|
||||||
|
}
|
||||||
|
}, [isOpen, calcMaxHeight])
|
||||||
|
|
||||||
onChange(v: string) {
|
React.useEffect(() => {
|
||||||
this.props.onChange(v === "" ? undefined : v);
|
window.addEventListener('resize', calcMaxHeight)
|
||||||
}
|
return () => window.removeEventListener('resize', calcMaxHeight)
|
||||||
|
}, [calcMaxHeight])
|
||||||
|
|
||||||
render() {
|
React.useEffect(() => {
|
||||||
return <div
|
setInput(value || '')
|
||||||
ref={(el) => {
|
}, [value])
|
||||||
this.autocompleteMenuEl = el;
|
|
||||||
}}
|
return (
|
||||||
>
|
<div className="maputnik-autocomplete">
|
||||||
<Autocomplete
|
<input
|
||||||
menuStyle={{
|
{...getInputProps({
|
||||||
position: "fixed",
|
'aria-label': ariaLabel,
|
||||||
overflow: "auto",
|
className: 'maputnik-string',
|
||||||
maxHeight: this.state.maxHeight,
|
spellCheck: false,
|
||||||
zIndex: '998'
|
onFocus: () => openMenu(),
|
||||||
}}
|
})}
|
||||||
wrapperProps={{
|
|
||||||
className: "maputnik-autocomplete",
|
|
||||||
style: {}
|
|
||||||
}}
|
|
||||||
inputProps={{
|
|
||||||
'aria-label': this.props['aria-label'],
|
|
||||||
className: "maputnik-string",
|
|
||||||
spellCheck: false
|
|
||||||
}}
|
|
||||||
value={this.props.value}
|
|
||||||
items={this.props.options}
|
|
||||||
getItemValue={(item) => item[0]}
|
|
||||||
onSelect={v => this.onChange(v)}
|
|
||||||
onChange={(_e, v) => this.onChange(v)}
|
|
||||||
shouldItemRender={(item, value="") => {
|
|
||||||
if (typeof(value) === "string") {
|
|
||||||
return item[0].toLowerCase().indexOf(value.toLowerCase()) > -1
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}}
|
|
||||||
renderItem={(item, isHighlighted) => (
|
|
||||||
<div
|
|
||||||
key={item[0]}
|
|
||||||
className={classnames({
|
|
||||||
"maputnik-autocomplete-menu-item": true,
|
|
||||||
"maputnik-autocomplete-menu-item-selected": isHighlighted,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{item[1]}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
{...getMenuProps({}, {suppressRefError: true})}
|
||||||
|
ref={menuRef}
|
||||||
|
style={{position: 'fixed', overflow: 'auto', maxHeight, zIndex: 998}}
|
||||||
|
className="maputnik-autocomplete-menu"
|
||||||
|
>
|
||||||
|
{isOpen &&
|
||||||
|
filteredItems.map((item, index) => (
|
||||||
|
<div
|
||||||
|
key={item[0]}
|
||||||
|
{...getItemProps({
|
||||||
|
item,
|
||||||
|
index,
|
||||||
|
className: classnames('maputnik-autocomplete-menu-item', {
|
||||||
|
'maputnik-autocomplete-menu-item-selected': highlightedIndex === index,
|
||||||
|
}),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{item[1]}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,10 +23,16 @@ type ModalAddState = {
|
|||||||
id: string
|
id: string
|
||||||
source?: string
|
source?: string
|
||||||
'source-layer'?: string
|
'source-layer'?: string
|
||||||
|
error?: string | null
|
||||||
};
|
};
|
||||||
|
|
||||||
class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddState> {
|
class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddState> {
|
||||||
addLayer = () => {
|
addLayer = () => {
|
||||||
|
if (this.props.layers.some(l => l.id === this.state.id)) {
|
||||||
|
this.setState({ error: this.props.t('Layer ID already exists') })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const changedLayers = this.props.layers.slice(0)
|
const changedLayers = this.props.layers.slice(0)
|
||||||
const layer: ModalAddState = {
|
const layer: ModalAddState = {
|
||||||
id: this.state.id,
|
id: this.state.id,
|
||||||
@@ -41,9 +47,10 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
changedLayers.push(layer as LayerSpecification)
|
changedLayers.push(layer as LayerSpecification)
|
||||||
|
this.setState({ error: null }, () => {
|
||||||
this.props.onLayersChange(changedLayers)
|
this.props.onLayersChange(changedLayers)
|
||||||
this.props.onOpenToggle(false)
|
this.props.onOpenToggle(false)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props: ModalAddInternalProps) {
|
constructor(props: ModalAddInternalProps) {
|
||||||
@@ -51,6 +58,7 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
|
|||||||
const state: ModalAddState = {
|
const state: ModalAddState = {
|
||||||
type: 'fill',
|
type: 'fill',
|
||||||
id: '',
|
id: '',
|
||||||
|
error: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
if(props.sources.length > 0) {
|
if(props.sources.length > 0) {
|
||||||
@@ -129,6 +137,21 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
|
|||||||
const t = this.props.t;
|
const t = this.props.t;
|
||||||
const sources = this.getSources(this.state.type);
|
const sources = this.getSources(this.state.type);
|
||||||
const layers = this.getLayersForSource(this.state.source!);
|
const layers = this.getLayersForSource(this.state.source!);
|
||||||
|
let errorElement;
|
||||||
|
if (this.state.error) {
|
||||||
|
errorElement = (
|
||||||
|
<div className="maputnik-modal-error">
|
||||||
|
{this.state.error}
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={() => this.setState({ error: null })}
|
||||||
|
className="maputnik-modal-error-close"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return <Modal
|
return <Modal
|
||||||
isOpen={this.props.isOpen}
|
isOpen={this.props.isOpen}
|
||||||
@@ -137,12 +160,13 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
|
|||||||
data-wd-key="modal:add-layer"
|
data-wd-key="modal:add-layer"
|
||||||
className="maputnik-add-modal"
|
className="maputnik-add-modal"
|
||||||
>
|
>
|
||||||
|
{errorElement}
|
||||||
<div className="maputnik-add-layer">
|
<div className="maputnik-add-layer">
|
||||||
<FieldId
|
<FieldId
|
||||||
value={this.state.id}
|
value={this.state.id}
|
||||||
wdKey="add-layer.layer-id"
|
wdKey="add-layer.layer-id"
|
||||||
onChange={(v: string) => {
|
onChange={(v: string) => {
|
||||||
this.setState({ id: v })
|
this.setState({ id: v, error: null })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<FieldType
|
<FieldType
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
"Collapse": "Einklappen",
|
"Collapse": "Einklappen",
|
||||||
"Expand": "Ausklappen",
|
"Expand": "Ausklappen",
|
||||||
"Add Layer": "Ebene hinzufügen",
|
"Add Layer": "Ebene hinzufügen",
|
||||||
|
"Layer ID already exists": "Layer-ID existiert bereits",
|
||||||
"Search": "Suche",
|
"Search": "Suche",
|
||||||
"Zoom:": "Zoom:",
|
"Zoom:": "Zoom:",
|
||||||
"Close popup": "Popup schließen",
|
"Close popup": "Popup schließen",
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
"Collapse": "Réduire",
|
"Collapse": "Réduire",
|
||||||
"Expand": "Développer",
|
"Expand": "Développer",
|
||||||
"Add Layer": "Ajouter un calque",
|
"Add Layer": "Ajouter un calque",
|
||||||
|
"Layer ID already exists": "L'identifiant du calque existe déjà",
|
||||||
"Search": "Recherche",
|
"Search": "Recherche",
|
||||||
"Zoom:": "Zoom :",
|
"Zoom:": "Zoom :",
|
||||||
"Close popup": "Fermer la fenêtre",
|
"Close popup": "Fermer la fenêtre",
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
"Collapse": "הקטנה",
|
"Collapse": "הקטנה",
|
||||||
"Expand": "הגדלה",
|
"Expand": "הגדלה",
|
||||||
"Add Layer": "הוספת שכבה",
|
"Add Layer": "הוספת שכבה",
|
||||||
|
"Layer ID already exists": "מזהה השכבה כבר קיים",
|
||||||
"Search": "חיפוש",
|
"Search": "חיפוש",
|
||||||
"Zoom:": "זום:",
|
"Zoom:": "זום:",
|
||||||
"Close popup": "סגירת החלון",
|
"Close popup": "סגירת החלון",
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
"Collapse": "Coprimi",
|
"Collapse": "Coprimi",
|
||||||
"Expand": "Espandi",
|
"Expand": "Espandi",
|
||||||
"Add Layer": "Aggiungi Livello",
|
"Add Layer": "Aggiungi Livello",
|
||||||
|
"Layer ID already exists": "L'ID del layer esiste già",
|
||||||
"Search": "Cerca",
|
"Search": "Cerca",
|
||||||
"Zoom:": "Zoom:",
|
"Zoom:": "Zoom:",
|
||||||
"Close popup": "Chiudi popup",
|
"Close popup": "Chiudi popup",
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
"Collapse": "畳む",
|
"Collapse": "畳む",
|
||||||
"Expand": "展開",
|
"Expand": "展開",
|
||||||
"Add Layer": "レイヤー追加",
|
"Add Layer": "レイヤー追加",
|
||||||
|
"Layer ID already exists": "レイヤーIDは既に存在します",
|
||||||
"Search": "検索",
|
"Search": "検索",
|
||||||
"Zoom:": "ズーム:",
|
"Zoom:": "ズーム:",
|
||||||
"Close popup": "ポップアップを閉じる",
|
"Close popup": "ポップアップを閉じる",
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
"Collapse": "折叠",
|
"Collapse": "折叠",
|
||||||
"Expand": "展开",
|
"Expand": "展开",
|
||||||
"Add Layer": "添加图层",
|
"Add Layer": "添加图层",
|
||||||
|
"Layer ID already exists": "图层ID已存在",
|
||||||
"Search": "搜索",
|
"Search": "搜索",
|
||||||
"Zoom:": "缩放:",
|
"Zoom:": "缩放:",
|
||||||
"Close popup": "关闭弹出窗口",
|
"Close popup": "关闭弹出窗口",
|
||||||
|
|||||||
Reference in New Issue
Block a user