mirror of
https://github.com/maputnik/editor.git
synced 2025-12-07 14:50:02 +00:00
Replacing react-sortable-hoc with dnd-kit (#1359)
## Launch Checklist This PR replace react-sortable-hoc which is unmaintained with dnd-kit - Resolves #1016 - Replaces the following PR: #1259 I've tested it locally to make sure it does what it should. I'll see if I can add a test... - [x] Briefly describe the changes in this PR. - [x] Link to related issues. - [x] Write tests for all new functionality. - [x] Add an entry to `CHANGELOG.md` under the `## main` section. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
- Replace react-autocomplete with Downshift in the autocomplete component
|
||||
- Add LocationIQ as supported map provider with access token field and gallery style
|
||||
- Revmove support for `debug` and `localport` url parameters
|
||||
- Replace react-sortable-hoc with dnd-kit to avoid react console warnings and also use a maintained library
|
||||
- _...Add new stuff here..._
|
||||
|
||||
### 🐞 Bug fixes
|
||||
|
||||
@@ -378,8 +378,57 @@ describe("layers", () => {
|
||||
});
|
||||
|
||||
it("groups", () => {
|
||||
// TODO
|
||||
// Click each of the layer groups.
|
||||
when.modal.open();
|
||||
const id1 = when.modal.fillLayers({
|
||||
id: "aa",
|
||||
type: "line",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
when.modal.open();
|
||||
const id2 = when.modal.fillLayers({
|
||||
id: "aa-2",
|
||||
type: "line",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
when.modal.open();
|
||||
const id3 = when.modal.fillLayers({
|
||||
id: "b",
|
||||
type: "line",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
then(get.elementByTestId("layer-list-item:" + id1)).shouldBeVisible();
|
||||
then(get.elementByTestId("layer-list-item:" + id2)).shouldNotBeVisible();
|
||||
then(get.elementByTestId("layer-list-item:" + id3)).shouldBeVisible();
|
||||
when.click("layer-list-group:aa-0");
|
||||
then(get.elementByTestId("layer-list-item:" + id1)).shouldBeVisible();
|
||||
then(get.elementByTestId("layer-list-item:" + id2)).shouldBeVisible();
|
||||
then(get.elementByTestId("layer-list-item:" + id3)).shouldBeVisible();
|
||||
when.click("layer-list-item:" + id2);
|
||||
when.click("skip-target-layer-editor");
|
||||
when.click("menu-move-layer-down");
|
||||
then(get.elementByTestId("layer-list-group:aa-0")).shouldNotExist();
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "aa",
|
||||
type: "line",
|
||||
source: "example",
|
||||
},
|
||||
{
|
||||
id: "b",
|
||||
type: "line",
|
||||
source: "example",
|
||||
},
|
||||
{
|
||||
id: "aa-2",
|
||||
type: "line",
|
||||
source: "example",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -495,9 +544,7 @@ describe("layers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("layereditor jsonlint should error", ()=>{
|
||||
|
||||
it("add", () => {
|
||||
const id = when.modal.fillLayers({
|
||||
type: "circle",
|
||||
@@ -523,4 +570,42 @@ describe("layers", () => {
|
||||
error.should('exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe("drag and drop", () => {
|
||||
it("move layer should update local storage", () => {
|
||||
when.modal.open();
|
||||
const firstId = when.modal.fillLayers({
|
||||
id: "a",
|
||||
type: "background",
|
||||
});
|
||||
when.modal.open();
|
||||
const secondId = when.modal.fillLayers({
|
||||
id: "b",
|
||||
type: "background",
|
||||
});
|
||||
when.modal.open();
|
||||
const thirdId = when.modal.fillLayers({
|
||||
id: "c",
|
||||
type: "background",
|
||||
});
|
||||
when.dragAndDrop(get.elementByTestId("layer-list-item:" + firstId), get.elementByTestId("layer-list-item:" + thirdId));
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: secondId,
|
||||
type: "background",
|
||||
},
|
||||
{
|
||||
id: thirdId,
|
||||
type: "background",
|
||||
},
|
||||
{
|
||||
id: firstId,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -94,8 +94,8 @@ export class MaputnikDriver {
|
||||
public when = {
|
||||
...this.helper.when,
|
||||
modal: this.modalDriver.when,
|
||||
within: (selector: string, fn: () => void) => {
|
||||
this.helper.when.within(fn, selector);
|
||||
doWithin: (selector: string, fn: () => void) => {
|
||||
this.helper.when.doWithin(fn, selector);
|
||||
},
|
||||
tab: () => this.helper.get.element("body").tab(),
|
||||
waitForExampleFileResponse: () => {
|
||||
@@ -145,7 +145,7 @@ export class MaputnikDriver {
|
||||
},
|
||||
|
||||
selectWithin: (selector: string, value: string) => {
|
||||
this.when.within(selector, () => {
|
||||
this.when.doWithin(selector, () => {
|
||||
this.helper.get.element("select").select(value);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -20,8 +20,8 @@ export default class ModalDriver {
|
||||
this.helper.when.type("add-layer.layer-id.input", id);
|
||||
|
||||
if (layer) {
|
||||
this.helper.when.within(() => {
|
||||
this.helper.get.element("input").type(layer!);
|
||||
this.helper.when.doWithin(() => {
|
||||
this.helper.get.element("input").clear().type(layer!);
|
||||
}, "add-layer.layer-source-block");
|
||||
}
|
||||
this.helper.when.click("add-layer");
|
||||
|
||||
81
package-lock.json
generated
81
package-lock.json
generated
@@ -9,6 +9,9 @@
|
||||
"version": "2.1.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@mapbox/mapbox-gl-rtl-text": "^0.3.0",
|
||||
"@maplibre/maplibre-gl-geocoder": "^1.9.0",
|
||||
"@maplibre/maplibre-gl-inspect": "^1.7.1",
|
||||
@@ -54,7 +57,6 @@
|
||||
"react-i18next": "^15.7.3",
|
||||
"react-icon-base": "^2.1.2",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-sortable-hoc": "^2.0.0",
|
||||
"reconnecting-websocket": "^4.4.0",
|
||||
"slugify": "^1.6.6",
|
||||
"string-hash": "^1.1.3",
|
||||
@@ -698,6 +700,59 @@
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/accessibility": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
||||
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/core": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dnd-kit/accessibility": "^3.1.1",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/sortable": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
|
||||
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dnd-kit/core": "^6.3.0",
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/utilities": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
||||
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dual-bundle/import-meta-resolve": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz",
|
||||
@@ -3779,6 +3834,7 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/array-move/-/array-move-4.0.0.tgz",
|
||||
"integrity": "sha512-+RY54S8OuVvg94THpneQvFRmqWdAHeqtMzgMW6JNurHxe8rsS07cHQdfGkXnTUXiBcyZ0j3SiDIxxj0RPiqCkQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
@@ -7569,14 +7625,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/invariant": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-array-buffer": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||
@@ -10899,21 +10947,6 @@
|
||||
"react-dom": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-sortable-hoc": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-sortable-hoc/-/react-sortable-hoc-2.0.0.tgz",
|
||||
"integrity": "sha512-JZUw7hBsAHXK7PTyErJyI7SopSBFRcFHDjWW5SWjcugY0i6iH7f+eJkY8cJmGMlZ1C9xz1J3Vjz0plFpavVeRg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.2.0",
|
||||
"invariant": "^2.2.4",
|
||||
"prop-types": "^15.5.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prop-types": "^15.5.7",
|
||||
"react": "^16.3.0 || ^17.0.0",
|
||||
"react-dom": "^16.3.0 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/reactcss": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz",
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/maplibre/maputnik#readme",
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@mapbox/mapbox-gl-rtl-text": "^0.3.0",
|
||||
"@maplibre/maplibre-gl-geocoder": "^1.9.0",
|
||||
"@maplibre/maplibre-gl-inspect": "^1.7.1",
|
||||
@@ -36,6 +39,7 @@
|
||||
"codemirror": "^5.65.20",
|
||||
"color": "^5.0.0",
|
||||
"detect-browser": "^5.3.0",
|
||||
"downshift": "^9.0.10",
|
||||
"events": "^3.3.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"i18next": "^25.5.2",
|
||||
@@ -60,7 +64,6 @@
|
||||
"react-accessible-accordion": "^5.0.1",
|
||||
"react-aria-menubutton": "^7.0.3",
|
||||
"react-aria-modal": "^5.0.2",
|
||||
"downshift": "^9.0.10",
|
||||
"react-collapse": "^5.1.1",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "^18.2.0",
|
||||
@@ -68,7 +71,6 @@
|
||||
"react-i18next": "^15.7.3",
|
||||
"react-icon-base": "^2.1.2",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-sortable-hoc": "^2.0.0",
|
||||
"reconnecting-websocket": "^4.4.0",
|
||||
"slugify": "^1.6.6",
|
||||
"string-hash": "^1.1.3",
|
||||
|
||||
@@ -33,7 +33,6 @@ import { RevisionStore } from '../libs/revisions'
|
||||
import LayerWatcher from '../libs/layerwatcher'
|
||||
import tokens from '../config/tokens.json'
|
||||
import isEqual from 'lodash.isequal'
|
||||
import { SortEnd } from 'react-sortable-hoc';
|
||||
import { MapOptions } from 'maplibre-gl';
|
||||
import { OnStyleChangedOpts, StyleSpecificationWithId } from '../libs/definitions'
|
||||
|
||||
@@ -436,7 +435,7 @@ export default class App extends React.Component<any, AppState> {
|
||||
if (errors.length > 0) {
|
||||
dirtyMapStyle = cloneDeep(newStyle);
|
||||
|
||||
errors.forEach(error => {
|
||||
for (const error of errors) {
|
||||
const {message} = error;
|
||||
if (message) {
|
||||
try {
|
||||
@@ -446,10 +445,10 @@ export default class App extends React.Component<any, AppState> {
|
||||
unset(dirtyMapStyle, unsetPath);
|
||||
}
|
||||
catch (err) {
|
||||
console.warn(err);
|
||||
console.warn(message + " " + err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
|
||||
@@ -496,7 +495,7 @@ export default class App extends React.Component<any, AppState> {
|
||||
})
|
||||
}
|
||||
|
||||
onMoveLayer = (move: SortEnd) => {
|
||||
onMoveLayer = (move: {oldIndex: number; newIndex: number}) => {
|
||||
let { oldIndex, newIndex } = move;
|
||||
let layers = this.state.mapStyle.layers;
|
||||
oldIndex = clamp(oldIndex, 0, layers.length-1);
|
||||
|
||||
@@ -13,28 +13,30 @@ type FieldSourceInternalProps = {
|
||||
error?: {message: string}
|
||||
} & WithTranslation;
|
||||
|
||||
const FieldSourceInternal: React.FC<FieldSourceInternalProps> = (props) => {
|
||||
const t = props.t;
|
||||
const FieldSourceInternal: React.FC<FieldSourceInternalProps> = ({
|
||||
onChange = () => {},
|
||||
sourceIds = [],
|
||||
wdKey,
|
||||
value,
|
||||
error,
|
||||
t
|
||||
}) => {
|
||||
return (
|
||||
<Block
|
||||
label={t('Source')}
|
||||
fieldSpec={latest.layer.source}
|
||||
error={props.error}
|
||||
data-wd-key={props.wdKey}
|
||||
error={error}
|
||||
data-wd-key={wdKey}
|
||||
>
|
||||
<InputAutocomplete
|
||||
value={props.value}
|
||||
onChange={props.onChange}
|
||||
options={props.sourceIds?.map((src) => [src, src])}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={sourceIds?.map((src) => [src, src])}
|
||||
/>
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
|
||||
FieldSourceInternal.defaultProps = {
|
||||
onChange: () => {},
|
||||
sourceIds: [],
|
||||
};
|
||||
|
||||
const FieldSource = withTranslation()(FieldSourceInternal);
|
||||
export default FieldSource;
|
||||
|
||||
@@ -9,33 +9,31 @@ type FieldSourceLayerInternalProps = {
|
||||
value?: string
|
||||
onChange?(...args: unknown[]): unknown
|
||||
sourceLayerIds?: unknown[]
|
||||
isFixed?: boolean
|
||||
error?: {message: string}
|
||||
} & WithTranslation;
|
||||
|
||||
const FieldSourceLayerInternal: React.FC<FieldSourceLayerInternalProps> = (props) => {
|
||||
const t = props.t;
|
||||
const FieldSourceLayerInternal: React.FC<FieldSourceLayerInternalProps> = ({
|
||||
onChange = () => {},
|
||||
sourceLayerIds = [],
|
||||
value,
|
||||
error,
|
||||
t
|
||||
}) => {
|
||||
return (
|
||||
<Block
|
||||
label={t('Source Layer')}
|
||||
fieldSpec={latest.layer['source-layer']}
|
||||
data-wd-key="layer-source-layer"
|
||||
error={props.error}
|
||||
error={error}
|
||||
>
|
||||
<InputAutocomplete
|
||||
value={props.value}
|
||||
onChange={props.onChange}
|
||||
options={props.sourceLayerIds?.map((l) => [l, l])}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={sourceLayerIds?.map((l) => [l, l])}
|
||||
/>
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
|
||||
FieldSourceLayerInternal.defaultProps = {
|
||||
onChange: () => {},
|
||||
sourceLayerIds: [],
|
||||
isFixed: false,
|
||||
};
|
||||
|
||||
const FieldSourceLayer = withTranslation()(FieldSourceLayerInternal);
|
||||
export default FieldSourceLayer;
|
||||
|
||||
@@ -22,6 +22,7 @@ import {formatLayerId} from '../libs/format';
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import { TFunction } from 'i18next';
|
||||
import { NON_SOURCE_LAYERS } from '../libs/non-source-layers';
|
||||
import { OnMoveLayerCallback } from '../libs/definitions';
|
||||
|
||||
type MaputnikLayoutGroup = {
|
||||
id: string;
|
||||
@@ -120,7 +121,7 @@ type LayerEditorInternalProps = {
|
||||
spec: object
|
||||
onLayerChanged(...args: unknown[]): unknown
|
||||
onLayerIdChange(...args: unknown[]): unknown
|
||||
onMoveLayer(...args: unknown[]): unknown
|
||||
onMoveLayer: OnMoveLayerCallback
|
||||
onLayerDestroy(...args: unknown[]): unknown
|
||||
onLayerCopy(...args: unknown[]): unknown
|
||||
onLayerVisibilityToggle(...args: unknown[]): unknown
|
||||
@@ -322,30 +323,38 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
||||
|
||||
const layout = this.props.layer.layout || {}
|
||||
|
||||
const items: {[key: string]: {text: string, handler: () => void, disabled?: boolean}} = {
|
||||
const items: {[key: string]: {
|
||||
text: string,
|
||||
handler: () => void,
|
||||
disabled?: boolean,
|
||||
wdKey?: string
|
||||
}} = {
|
||||
delete: {
|
||||
text: t("Delete"),
|
||||
handler: () => this.props.onLayerDestroy(this.props.layerIndex)
|
||||
handler: () => this.props.onLayerDestroy(this.props.layerIndex),
|
||||
wdKey: "menu-delete-layer"
|
||||
},
|
||||
duplicate: {
|
||||
text: t("Duplicate"),
|
||||
handler: () => this.props.onLayerCopy(this.props.layerIndex)
|
||||
handler: () => this.props.onLayerCopy(this.props.layerIndex),
|
||||
wdKey: "menu-duplicate-layer"
|
||||
},
|
||||
hide: {
|
||||
text: (layout.visibility === "none") ? t("Show") : t("Hide"),
|
||||
handler: () => this.props.onLayerVisibilityToggle(this.props.layerIndex)
|
||||
handler: () => this.props.onLayerVisibilityToggle(this.props.layerIndex),
|
||||
wdKey: "menu-hide-layer"
|
||||
},
|
||||
moveLayerUp: {
|
||||
text: t("Move layer up"),
|
||||
// Not actually used...
|
||||
disabled: this.props.isFirstLayer,
|
||||
handler: () => this.moveLayer(-1)
|
||||
handler: () => this.moveLayer(-1),
|
||||
wdKey: "menu-move-layer-up"
|
||||
},
|
||||
moveLayerDown: {
|
||||
text: t("Move layer down"),
|
||||
// Not actually used...
|
||||
disabled: this.props.isLastLayer,
|
||||
handler: () => this.moveLayer(+1)
|
||||
handler: () => this.moveLayer(+1),
|
||||
wdKey: "menu-move-layer-down"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,7 +391,7 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
||||
{Object.keys(items).map((id) => {
|
||||
const item = items[id];
|
||||
return <li key={id}>
|
||||
<MenuItem value={id} className='more-menu__menu__item'>
|
||||
<MenuItem value={id} className='more-menu__menu__item' data-wd-key={item.wdKey}>
|
||||
{item.text}
|
||||
</MenuItem>
|
||||
</li>
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
import React, {type JSX} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import lodash from 'lodash';
|
||||
import {
|
||||
DndContext,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
closestCenter,
|
||||
DragEndEvent,
|
||||
} from '@dnd-kit/core';
|
||||
import {
|
||||
SortableContext,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable';
|
||||
|
||||
import LayerListGroup from './LayerListGroup'
|
||||
import LayerListItem from './LayerListItem'
|
||||
import ModalAdd from './ModalAdd'
|
||||
|
||||
import {SortEndHandler, SortableContainer} from 'react-sortable-hoc';
|
||||
import type {LayerSpecification, SourceSpecification} from 'maplibre-gl';
|
||||
import generateUniqueId from '../libs/document-uid';
|
||||
import { findClosestCommonPrefix, layerPrefix } from '../libs/layer';
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import { OnMoveLayerCallback } from '../libs/definitions';
|
||||
|
||||
type LayerListContainerProps = {
|
||||
layers: LayerSpecification[]
|
||||
@@ -242,7 +254,6 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
||||
'maputnik-layer-list-item-group-last': idxInGroup == layers.length - 1 && layers.length > 1,
|
||||
'maputnik-layer-list-item--error': !!layerError
|
||||
})}
|
||||
index={idx}
|
||||
key={layer.key}
|
||||
id={layer.key}
|
||||
layerId={layer.id}
|
||||
@@ -319,20 +330,35 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
||||
}
|
||||
|
||||
const LayerListContainer = withTranslation()(LayerListContainerInternal);
|
||||
const LayerListContainerSortable = SortableContainer((props: LayerListContainerProps) => <LayerListContainer {...props} />)
|
||||
|
||||
type LayerListProps = LayerListContainerProps & {
|
||||
onMoveLayer: SortEndHandler
|
||||
onMoveLayer: OnMoveLayerCallback
|
||||
};
|
||||
|
||||
export default class LayerList extends React.Component<LayerListProps> {
|
||||
render() {
|
||||
return <LayerListContainerSortable
|
||||
{...this.props}
|
||||
helperClass='sortableHelper'
|
||||
onSortEnd={this.props.onMoveLayer.bind(this)}
|
||||
useDragHandle={true}
|
||||
shouldCancelStart={() => false}
|
||||
/>
|
||||
}
|
||||
const LayerList: React.FC<LayerListProps> = (props) => {
|
||||
const sensors = useSensors(useSensor(PointerSensor));
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const {active, over} = event;
|
||||
if (!over) return;
|
||||
|
||||
const oldIndex = props.layers.findIndex(layer => layer.id === active.id);
|
||||
const newIndex = props.layers.findIndex(layer => layer.id === over.id);
|
||||
|
||||
if (oldIndex !== -1 && newIndex !== -1 && oldIndex !== newIndex) {
|
||||
props.onMoveLayer({oldIndex, newIndex});
|
||||
}
|
||||
};
|
||||
|
||||
const layerIds = props.layers.map(layer => layer.id);
|
||||
|
||||
return (
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<SortableContext items={layerIds} strategy={verticalListSortingStrategy}>
|
||||
<LayerListContainer {...props} />
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
}
|
||||
|
||||
export default LayerList;
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import React from 'react'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import {MdContentCopy, MdVisibility, MdVisibilityOff, MdDelete} from 'react-icons/md'
|
||||
import { IconContext } from 'react-icons'
|
||||
import {useSortable} from '@dnd-kit/sortable'
|
||||
import {CSS} from '@dnd-kit/utilities'
|
||||
|
||||
import IconLayer from './IconLayer'
|
||||
import {SortableElement, SortableHandle} from 'react-sortable-hoc'
|
||||
|
||||
|
||||
type DraggableLabelProps = {
|
||||
layerId: string
|
||||
layerType: string
|
||||
dragAttributes?: React.HTMLAttributes<HTMLElement>
|
||||
dragListeners?: React.HTMLAttributes<HTMLElement>
|
||||
};
|
||||
|
||||
const DraggableLabel = SortableHandle((props: DraggableLabelProps) => {
|
||||
return <div className="maputnik-layer-list-item-handle">
|
||||
const DraggableLabel: React.FC<DraggableLabelProps> = (props) => {
|
||||
const {dragAttributes, dragListeners} = props;
|
||||
return <div className="maputnik-layer-list-item-handle" {...dragAttributes} {...dragListeners}>
|
||||
<IconLayer
|
||||
className="layer-handle__icon"
|
||||
type={props.layerType}
|
||||
@@ -23,7 +26,7 @@ const DraggableLabel = SortableHandle((props: DraggableLabelProps) => {
|
||||
{props.layerId}
|
||||
</button>
|
||||
</div>
|
||||
});
|
||||
};
|
||||
|
||||
type IconActionProps = {
|
||||
action: string
|
||||
@@ -82,55 +85,80 @@ type LayerListItemProps = {
|
||||
onLayerVisibilityToggle?(...args: unknown[]): unknown
|
||||
};
|
||||
|
||||
class LayerListItem extends React.Component<LayerListItemProps> {
|
||||
static defaultProps = {
|
||||
isSelected: false,
|
||||
visibility: 'visible',
|
||||
onLayerCopy: () => {},
|
||||
onLayerDestroy: () => {},
|
||||
onLayerVisibilityToggle: () => {},
|
||||
}
|
||||
const LayerListItem = React.forwardRef<HTMLLIElement, LayerListItemProps>((props, ref) => {
|
||||
const {
|
||||
isSelected = false,
|
||||
visibility = 'visible',
|
||||
onLayerCopy = () => {},
|
||||
onLayerDestroy = () => {},
|
||||
onLayerVisibilityToggle = () => {},
|
||||
} = props;
|
||||
|
||||
render() {
|
||||
const visibilityAction = this.props.visibility === 'visible' ? 'show' : 'hide';
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({id: props.layerId});
|
||||
|
||||
return <IconContext.Provider value={{size: '14px'}}>
|
||||
<li
|
||||
id={this.props.id}
|
||||
key={this.props.layerId}
|
||||
onClick={_e => this.props.onLayerSelect(this.props.layerIndex)}
|
||||
data-wd-key={"layer-list-item:"+this.props.layerId}
|
||||
className={classnames({
|
||||
"maputnik-layer-list-item": true,
|
||||
"maputnik-layer-list-item-selected": this.props.isSelected,
|
||||
[this.props.className!]: true,
|
||||
})}>
|
||||
<DraggableLabel {...this.props} />
|
||||
<span style={{flexGrow: 1}} />
|
||||
<IconAction
|
||||
wdKey={"layer-list-item:"+this.props.layerId+":delete"}
|
||||
action={'delete'}
|
||||
classBlockName="delete"
|
||||
onClick={_e => this.props.onLayerDestroy!(this.props.layerIndex)}
|
||||
/>
|
||||
<IconAction
|
||||
wdKey={"layer-list-item:"+this.props.layerId+":copy"}
|
||||
action={'duplicate'}
|
||||
classBlockName="duplicate"
|
||||
onClick={_e => this.props.onLayerCopy!(this.props.layerIndex)}
|
||||
/>
|
||||
<IconAction
|
||||
wdKey={"layer-list-item:"+this.props.layerId+":toggle-visibility"}
|
||||
action={visibilityAction}
|
||||
classBlockName="visibility"
|
||||
classBlockModifier={visibilityAction}
|
||||
onClick={_e => this.props.onLayerVisibilityToggle!(this.props.layerIndex)}
|
||||
/>
|
||||
</li>
|
||||
</IconContext.Provider>
|
||||
}
|
||||
}
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
};
|
||||
|
||||
const LayerListItemSortable = SortableElement<LayerListItemProps>((props: LayerListItemProps) => <LayerListItem {...props} />);
|
||||
const visibilityAction = visibility === 'visible' ? 'show' : 'hide';
|
||||
|
||||
export default LayerListItemSortable;
|
||||
// Cast ref to MutableRefObject since we know from the codebase that's what's always passed
|
||||
const refObject = ref as React.MutableRefObject<HTMLLIElement | null> | null;
|
||||
|
||||
return <IconContext.Provider value={{size: '14px'}}>
|
||||
<li
|
||||
ref={(node) => {
|
||||
setNodeRef(node);
|
||||
if (refObject) {
|
||||
refObject.current = node;
|
||||
}
|
||||
}}
|
||||
style={style}
|
||||
id={props.id}
|
||||
onClick={_e => props.onLayerSelect(props.layerIndex)}
|
||||
data-wd-key={"layer-list-item:" + props.layerId}
|
||||
className={classnames({
|
||||
"maputnik-layer-list-item": true,
|
||||
"maputnik-layer-list-item-selected": isSelected,
|
||||
[props.className!]: true,
|
||||
})}>
|
||||
<DraggableLabel
|
||||
layerId={props.layerId}
|
||||
layerType={props.layerType}
|
||||
dragAttributes={attributes}
|
||||
dragListeners={listeners}
|
||||
/>
|
||||
<span style={{flexGrow: 1}} />
|
||||
<IconAction
|
||||
wdKey={"layer-list-item:" + props.layerId+":delete"}
|
||||
action={'delete'}
|
||||
classBlockName="delete"
|
||||
onClick={_e => onLayerDestroy!(props.layerIndex)}
|
||||
/>
|
||||
<IconAction
|
||||
wdKey={"layer-list-item:" + props.layerId+":copy"}
|
||||
action={'duplicate'}
|
||||
classBlockName="duplicate"
|
||||
onClick={_e => onLayerCopy!(props.layerIndex)}
|
||||
/>
|
||||
<IconAction
|
||||
wdKey={"layer-list-item:"+props.layerId+":toggle-visibility"}
|
||||
action={visibilityAction}
|
||||
classBlockName="visibility"
|
||||
classBlockModifier={visibilityAction}
|
||||
onClick={_e => onLayerVisibilityToggle!(props.layerIndex)}
|
||||
/>
|
||||
</li>
|
||||
</IconContext.Provider>
|
||||
});
|
||||
|
||||
export default LayerListItem;
|
||||
|
||||
@@ -175,7 +175,6 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
|
||||
}
|
||||
{!NON_SOURCE_LAYERS.includes(this.state.type) &&
|
||||
<FieldSourceLayer
|
||||
isFixed={true}
|
||||
sourceLayerIds={layers}
|
||||
value={this.state['source-layer']}
|
||||
onChange={(v: string) => this.setState({ 'source-layer': v })}
|
||||
|
||||
2
src/libs/definitions.d.ts
vendored
2
src/libs/definitions.d.ts
vendored
@@ -10,6 +10,8 @@ export type OnStyleChangedOpts = {
|
||||
|
||||
export type OnStyleChangedCallback = (newStyle: StyleSpecificationWithId, opts: OnStyleChangedOpts={}) => void;
|
||||
|
||||
export type OnMoveLayerCallback = (move: {oldIndex: number; newIndex: number}) => void;
|
||||
|
||||
export interface IStyleStore {
|
||||
getLatestStyle(): Promise<StyleSpecificationWithId>;
|
||||
save(mapStyle: StyleSpecificationWithId): StyleSpecificationWithId;
|
||||
|
||||
Reference in New Issue
Block a user