Refactor driver for E2E (#841)

Added shellygo package.
Refactor driver and e2e tests.
Added data-wd attributes to missing places.

---------

Co-authored-by: shelly_goldblit <shelly_goldblit@dell.com>
This commit is contained in:
ShellyDCMS
2023-12-19 21:37:58 +02:00
committed by GitHub
parent 8eabfa5519
commit 84adbe6eb2
16 changed files with 4585 additions and 5293 deletions
+29 -31
View File
@@ -1,41 +1,39 @@
import driver from "./driver"; import MaputnikDriver from "./driver";
describe("accessibility", () => { describe("accessibility", () => {
// skipped due to the following issue with cypress: https://github.com/cypress-io/cypress/issues/299 let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
describe.skip("skip links", () => { beforeAndAfter();
describe("skip links", () => {
beforeEach(() => { beforeEach(() => {
driver.beforeEach(); when.setStyle("layer");
driver.setStyle("layer");
}); });
it("skip link to layer list", () => { it("skip link to layer list", () => {
const selector = driver.getDataAttribute("root:skip:layer-list"); const selector = "root:skip:layer-list";
driver.isExists(selector); should.isExists(selector);
driver.typeKeys('{tab}'); when.tab();
driver.isFocused(selector); should.beFocused(selector);
driver.click(selector); when.click(selector);
should.beFocused("skip-target-layer-list");
driver.isFocused("#skip-target-layer-list");
}); });
it("skip link to layer editor", () => { it("skip link to layer editor", () => {
const selector = driver.getDataAttribute("root:skip:layer-editor"); const selector = "root:skip:layer-editor";
driver.isExists(selector); should.isExists(selector);
driver.typeKeys('{tab}{tab}'); when.tab().tab();
driver.isFocused(selector); should.beFocused(selector);
driver.click(selector); when.click(selector);
should.beFocused("skip-target-layer-editor");
driver.isFocused("#skip-target-layer-editor");
}); });
it("skip link to map view", () => { it("skip link to map view", () => {
const selector = driver.getDataAttribute("root:skip:map-view"); const selector = "root:skip:map-view";
driver.isExists(selector); should.isExists(selector);
driver.typeKeys('{tab}{tab}{tab}'); when.tab().tab().tab();
driver.isFocused(selector); should.beFocused(selector);
driver.click(selector); when.click(selector);
should.canvasBeFocused();
driver.isFocused(".maplibregl-canvas");
}); });
}); });
}) });
+189 -161
View File
@@ -1,170 +1,198 @@
import {v1 as uuid} from "uuid"; import { CypressHelper } from "@shellygo/cypress-test-utils";
import { v1 as uuid } from "uuid";
export default class MaputnikDriver {
private helper = new CypressHelper({ defaultDataAttribute: "data-wd-key" });
public beforeAndAfter = () => {
beforeEach(() => {
this.given.setupInterception();
this.when.setStyle("both");
});
};
export default { public given = {
isMac() { setupInterception: () => {
return Cypress.platform === "darwin"; cy.intercept("GET", "http://localhost:8888/example-style.json", {
fixture: "example-style.json",
}).as("example-style.json");
cy.intercept("GET", "http://localhost:8888/example-layer-style.json", {
fixture: "example-layer-style.json",
});
cy.intercept("GET", "http://localhost:8888/geojson-style.json", {
fixture: "geojson-style.json",
});
cy.intercept("GET", "http://localhost:8888/raster-style.json", {
fixture: "raster-style.json",
});
cy.intercept("GET", "http://localhost:8888/geojson-raster-style.json", {
fixture: "geojson-raster-style.json",
});
cy.intercept({ method: "GET", url: "*example.local/*" }, []);
cy.intercept({ method: "GET", url: "*example.com/*" }, []);
}, },
};
beforeEach() { public when = {
this.setupInterception(); within: (selector: string, fn: () => void) => {
this.setStyle('both'); this.helper.when.within(fn, selector);
}, },
tab: () => cy.get("body").tab(),
setupInterception() { waitForExampleFileRequset: () => {
cy.intercept('GET', 'http://localhost:8888/example-style.json', { fixture: 'example-style.json' }).as('example-style.json'); this.helper.when.waitForResponse("example-style.json");
cy.intercept('GET', 'http://localhost:8888/example-layer-style.json', { fixture: 'example-layer-style.json' });
cy.intercept('GET', 'http://localhost:8888/geojson-style.json', { fixture: 'geojson-style.json' });
cy.intercept('GET', 'http://localhost:8888/raster-style.json', { fixture: 'raster-style.json' });
cy.intercept('GET', 'http://localhost:8888/geojson-raster-style.json', { fixture: 'geojson-raster-style.json' });
cy.intercept({method: 'GET', url: '*example.local/*' }, []);
cy.intercept({method: 'GET', url: '*example.com/*' }, []);
}, },
chooseExampleFile: () => {
setStyle(styleProperties: 'geojson' | 'raster' | 'both' | 'layer' | '', zoom? : number) { cy.get("input[type='file']").selectFile(
let url = "?debug"; "cypress/fixtures/example-style.json",
switch (styleProperties) { { force: true }
case "geojson": );
url += "&style=http://localhost:8888/geojson-style.json"; },
break; setStyle: (
case "raster": styleProperties: "geojson" | "raster" | "both" | "layer" | "",
url += "&style=http://localhost:8888/raster-style.json"; zoom?: number
break; ) => {
case "both": let url = "?debug";
url += "&style=http://localhost:8888/geojson-raster-style.json"; switch (styleProperties) {
break; case "geojson":
case "layer": url += "&style=http://localhost:8888/geojson-style.json";
url += "&style=http://localhost:8888/example-layer-style.json"; break;
break; case "raster":
} url += "&style=http://localhost:8888/raster-style.json";
if (zoom) { break;
url += "#" + zoom + "/41.3805/2.1635"; case "both":
} url += "&style=http://localhost:8888/geojson-raster-style.json";
cy.visit("http://localhost:8888/" + url); break;
if (styleProperties) { case "layer":
cy.on('window:confirm', () => true) url += "&style=http://localhost:8888/example-layer-style.json";
} break;
cy.get(".maputnik-toolbar-link").should("be.visible"); }
}, if (zoom) {
url += "#" + zoom + "/41.3805/2.1635";
getDataAttribute(key: string, selector?: string) { }
return `*[data-wd-key='${key}'] ${selector || ''}`; cy.visit("http://localhost:8888/" + url);
}, if (styleProperties) {
cy.on("window:confirm", () => true);
closeModal(key: string) { }
const selector = this.getDataAttribute(key); cy.get(".maputnik-toolbar-link").should("be.visible");
},
this.isDisplayedInViewport(selector); fillLayersModal: (opts: any) => {
var type = opts.type;
this.click(this.getDataAttribute(key + ".close-modal")); var layer = opts.layer;
var id;
this.doesNotExists(selector); if (opts.id) {
}, id = opts.id;
} else {
openLayersModal() { id = `${type}:${uuid()}`;
cy.get(this.getDataAttribute('layer-list:add-layer')).click();
cy.get(this.getDataAttribute('modal:add-layer')).should('exist');
cy.get(this.getDataAttribute('modal:add-layer')).should('be.visible');
},
getStyleFromWindow(win: Window) {
const styleId = win.localStorage.getItem("maputnik:latest_style");
const styleItem = win.localStorage.getItem(`maputnik:style:${styleId}`)
const obj = JSON.parse(styleItem || "");
return obj;
},
isStyleStoreEqual(getter: (obj:any) => any, styleObj: any) {
cy.window().then((win: any) => {
const obj = this.getStyleFromWindow(win);
assert.deepEqual(getter(obj), styleObj);
});
},
isStyleStoreEqualToExampleFileData() {
cy.window().then((win: any) => {
const obj = this.getStyleFromWindow(win);
cy.fixture('example-style.json').should('deep.equal', obj);
});
},
fillLayersModal(opts: any) {
var type = opts.type;
var layer = opts.layer;
var id;
if(opts.id) {
id = opts.id
}
else {
id = `${type}:${uuid()}`;
}
cy.get(this.getDataAttribute('add-layer.layer-type', "select")).select(type);
cy.get(this.getDataAttribute("add-layer.layer-id", "input")).type(id);
if(layer) {
cy.get(this.getDataAttribute("add-layer.layer-source-block", "input")).type(layer);
}
cy.get(this.getDataAttribute("add-layer")).click();
return id;
},
typeKeys(keys: string) {
cy.get('body').type(keys);
},
click(selector: string) {
cy.get(selector).click();
},
select(selector: string, value: string) {
cy.get(selector).select(value);
},
isSelected(selector: string, value: string) {
cy.get(selector).find(`option[value="${value}"]`).should("be.selected");
},
focus(selector: string) {
cy.get(selector).focus();
},
isFocused(selector: string) {
cy.get(selector).should('have.focus');
},
isDisplayedInViewport(selector: string) {
cy.get(selector).should('be.visible');
},
isNotDisplayedInViewport(selector: string) {
cy.get(selector).should('not.be.visible');
},
setValue(selector: string, text: string) {
cy.get(selector).clear().type(text, {parseSpecialCharSequences: false});
},
isExists(selector: string) {
cy.get(selector).should('exist');
},
doesNotExists(selector: string) {
cy.get(selector).should('not.exist');
},
chooseExampleFile() {
cy.get("input[type='file']").selectFile('cypress/fixtures/example-style.json', {force: true});
},
getExampleFileUrl() {
return "http://localhost:8888/example-style.json";
},
waitForExampleFileRequset() {
cy.wait('@example-style.json');
} }
cy.get(
this.get.getDataAttribute("add-layer.layer-type", "select")
).select(type);
cy.get(this.get.getDataAttribute("add-layer.layer-id", "input")).type(id);
if (layer) {
cy.get(
this.get.getDataAttribute("add-layer.layer-source-block", "input")
).type(layer);
}
this.when.click("add-layer");
return id;
},
typeKeys: (keys: string) => {
cy.get("body").type(keys);
},
click: (selector: string) => {
this.helper.when.click(selector);
// cy.get(selector).click({ force: true });
},
select: (selector: string, value: string) => {
cy.get(selector).select(value);
},
focus: (selector: string) => {
this.helper.when.focus(selector);
},
setValue: (selector: string, text: string) => {
cy.get(selector).clear().type(text, { parseSpecialCharSequences: false });
},
closeModal: (key: string) => {
this.helper.when.waitUntil(() => this.helper.get.element(key));
this.when.click(key + ".close-modal");
},
openLayersModal: () => {
this.helper.when.click("layer-list:add-layer");
cy.get(this.get.getDataAttribute("modal:add-layer")).should("exist");
cy.get(this.get.getDataAttribute("modal:add-layer")).should("be.visible");
},
};
public get = {
isMac: () => {
return Cypress.platform === "darwin";
},
getStyleFromWindow: (win: Window) => {
const styleId = win.localStorage.getItem("maputnik:latest_style");
const styleItem = win.localStorage.getItem(`maputnik:style:${styleId}`);
const obj = JSON.parse(styleItem || "");
return obj;
},
getExampleFileUrl: () => {
return "http://localhost:8888/example-style.json";
},
getDataAttribute: (key: string, selector?: string): string => {
return `*[data-wd-key='${key}'] ${selector || ""}`;
},
};
public should = {
canvasBeFocused: () => {
this.when.within("maplibre:map", () => {
cy.get("canvas").should("be.focused");
});
},
notExist: (selector: string) => {
cy.get(selector).should("not.exist");
},
beFocused: (selector: string) => {
this.helper.get.element(selector).should("have.focus");
},
notBeFocused: (selector: string) => {
this.helper.get.element(selector).should("not.have.focus");
},
beVisible: (selector: string) => {
this.helper.get.element(selector).should("be.visible");
},
notBeVisible: (selector: string) => {
this.helper.get.element(selector).should("not.be.visible");
},
equalStyleStore: (getter: (obj: any) => any, styleObj: any) => {
cy.window().then((win: any) => {
const obj = this.get.getStyleFromWindow(win);
assert.deepEqual(getter(obj), styleObj);
});
},
isStyleStoreEqualToExampleFileData: () => {
cy.window().then((win: any) => {
const obj = this.get.getStyleFromWindow(win);
cy.fixture("example-style.json").should("deep.equal", obj);
});
},
isExists: (selector: string) => {
this.helper.get.element(selector).should("exist");
},
isSelected: (selector: string, value: string) => {
cy.get(selector).find(`option[value="${value}"]`).should("be.selected");
},
};
} }
+76 -59
View File
@@ -1,80 +1,97 @@
import driver from "./driver"; import MaputnikDriver from "./driver";
describe("history", () => { describe("history", () => {
let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
beforeAndAfter();
let undoKeyCombo: string; let undoKeyCombo: string;
let redoKeyCombo: string; let redoKeyCombo: string;
before(() => { before(() => {
const isMac = driver.isMac(); const isMac = get.isMac();
undoKeyCombo = isMac ? '{meta}z' : '{ctrl}z'; undoKeyCombo = isMac ? "{meta}z" : "{ctrl}z";
redoKeyCombo = isMac ? '{meta}{shift}z' : '{ctrl}y'; redoKeyCombo = isMac ? "{meta}{shift}z" : "{ctrl}y";
driver.beforeEach();
}); });
it("undo/redo", () => { it("undo/redo", () => {
driver.setStyle('geojson'); when.setStyle("geojson");
driver.openLayersModal(); when.openLayersModal();
driver.isStyleStoreEqual((a: any) => a.layers, []); should.equalStyleStore((a: any) => a.layers, []);
driver.fillLayersModal({ when.fillLayersModal({
id: "step 1", id: "step 1",
type: "background" type: "background",
}) });
driver.isStyleStoreEqual((a: any) => a.layers, [ should.equalStyleStore(
{ (a: any) => a.layers,
"id": "step 1", [
"type": 'background' {
} id: "step 1",
]); type: "background",
},
]
);
driver.openLayersModal(); when.openLayersModal();
driver.fillLayersModal({ when.fillLayersModal({
id: "step 2", id: "step 2",
type: "background" type: "background",
}) });
driver.isStyleStoreEqual((a: any) => a.layers, [ should.equalStyleStore(
{ (a: any) => a.layers,
"id": "step 1", [
"type": 'background' {
}, id: "step 1",
{ type: "background",
"id": "step 2", },
"type": 'background' {
} id: "step 2",
]); type: "background",
},
]
);
driver.typeKeys(undoKeyCombo); when.typeKeys(undoKeyCombo);
driver.isStyleStoreEqual((a: any) => a.layers, [ should.equalStyleStore(
{ (a: any) => a.layers,
"id": "step 1", [
"type": 'background' {
} id: "step 1",
]); type: "background",
},
]
);
driver.typeKeys(undoKeyCombo) when.typeKeys(undoKeyCombo);
driver.isStyleStoreEqual((a: any) => a.layers, []); should.equalStyleStore((a: any) => a.layers, []);
driver.typeKeys(redoKeyCombo) when.typeKeys(redoKeyCombo);
driver.isStyleStoreEqual((a: any) => a.layers, [ should.equalStyleStore(
{ (a: any) => a.layers,
"id": "step 1", [
"type": 'background' {
} id: "step 1",
]); type: "background",
},
]
);
driver.typeKeys(redoKeyCombo) when.typeKeys(redoKeyCombo);
driver.isStyleStoreEqual((a: any) => a.layers, [ should.equalStyleStore(
{ (a: any) => a.layers,
"id": "step 1", [
"type": 'background' {
}, id: "step 1",
{ type: "background",
"id": "step 2", },
"type": 'background' {
} id: "step 2",
]); type: "background",
},
]
);
}); });
}) });
+27 -26
View File
@@ -1,60 +1,61 @@
import driver from "./driver"; import { default as MaputnikDriver } from "./driver";
describe("keyboard", () => { describe("keyboard", () => {
let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
beforeAndAfter();
describe("shortcuts", () => { describe("shortcuts", () => {
beforeEach(() => { beforeEach(() => {
driver.setupInterception(); given.setupInterception();
driver.setStyle(''); when.setStyle("");
}) });
it("ESC should unfocus", () => { it("ESC should unfocus", () => {
const targetSelector = driver.getDataAttribute("nav:inspect") + " select"; const targetSelector = "maputnik-select";
driver.focus(targetSelector); when.focus(targetSelector);
driver.isFocused(targetSelector); should.beFocused(targetSelector);
//driver.typeKeys("{esc}"); when.typeKeys("{esc}");
//driver.isFocused('body'); expect(should.notBeFocused(targetSelector));
}); });
it("'?' should show shortcuts modal", () => { it("'?' should show shortcuts modal", () => {
driver.typeKeys("?"); when.typeKeys("?");
driver.isDisplayedInViewport(driver.getDataAttribute("modal:shortcuts")); should.beVisible("modal:shortcuts");
}); });
it("'o' should show open modal", () => { it("'o' should show open modal", () => {
driver.typeKeys("o"); when.typeKeys("o");
driver.isDisplayedInViewport(driver.getDataAttribute("modal:open")); should.beVisible("modal:open");
}); });
it("'e' should show export modal", () => { it("'e' should show export modal", () => {
driver.typeKeys("e"); when.typeKeys("e");
driver.isDisplayedInViewport(driver.getDataAttribute("modal:export")); should.beVisible("modal:export");
}); });
it("'d' should show sources modal", () => { it("'d' should show sources modal", () => {
driver.typeKeys("d"); when.typeKeys("d");
driver.isDisplayedInViewport(driver.getDataAttribute("modal:sources")); should.beVisible("modal:sources");
}); });
it("'s' should show settings modal", () => { it("'s' should show settings modal", () => {
driver.typeKeys("s"); when.typeKeys("s");
driver.isDisplayedInViewport(driver.getDataAttribute("modal:settings")); should.beVisible("modal:settings");
}); });
it("'i' should change map to inspect mode", () => { it("'i' should change map to inspect mode", () => {
driver.typeKeys("i"); when.typeKeys("i");
driver.isSelected(driver.getDataAttribute("nav:inspect"), "inspect"); should.isSelected(get.getDataAttribute("nav:inspect"), "inspect");
}); });
it("'m' should focus map", () => { it("'m' should focus map", () => {
driver.typeKeys("m"); when.typeKeys("m");
driver.isFocused(".maplibregl-canvas"); should.beFocused(".maplibregl-canvas");
}); });
it("'!' should show debug modal", () => { it("'!' should show debug modal", () => {
driver.typeKeys("!"); when.typeKeys("!");
driver.isDisplayedInViewport(driver.getDataAttribute("modal:debug")); should.beVisible("modal:debug");
}); });
}); });
}); });
+307 -237
View File
@@ -1,112 +1,132 @@
var assert = require("assert"); var assert = require("assert");
import driver from "./driver"; import { v1 as uuid } from "uuid";
import { v1 as uuid } from 'uuid'; import MaputnikDriver from "./driver";
describe("layers", () => { describe("layers", () => {
let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
beforeAndAfter();
beforeEach(() => { beforeEach(() => {
driver.beforeEach(); when.setStyle("both");
driver.setStyle('both'); when.openLayersModal();
driver.openLayersModal();
}); });
describe("ops", () => { describe("ops", () => {
it("delete", () => { it("delete", () => {
var id = driver.fillLayersModal({ var id = when.fillLayersModal({
type: "background" type: "background",
}) });
driver.isStyleStoreEqual((a: any) => a.layers, [ should.equalStyleStore(
{ (a: any) => a.layers,
"id": id, [
"type": 'background' {
}, id: id,
]); type: "background",
},
]
);
driver.click(driver.getDataAttribute("layer-list-item:"+id+":delete", "")) when.click("layer-list-item:" + id + ":delete");
driver.isStyleStoreEqual((a: any) => a.layers, []); should.equalStyleStore((a: any) => a.layers, []);
}); });
it("duplicate", () => { it("duplicate", () => {
var styleObj; var styleObj;
var id = driver.fillLayersModal({ var id = when.fillLayersModal({
type: "background" type: "background",
}) });
driver.isStyleStoreEqual((a: any) => a.layers, [ should.equalStyleStore(
{ (a: any) => a.layers,
"id": id, [
"type": 'background' {
}, id: id,
]); type: "background",
},
]
);
driver.click(driver.getDataAttribute("layer-list-item:"+id+":copy", "")); when.click("layer-list-item:" + id + ":copy");
driver.isStyleStoreEqual((a: any) => a.layers, [ should.equalStyleStore(
{ (a: any) => a.layers,
"id": id+"-copy", [
"type": "background" {
}, id: id + "-copy",
{ type: "background",
"id": id, },
"type": "background" {
}, id: id,
]); type: "background",
},
]
);
}); });
it("hide", () => { it("hide", () => {
var styleObj; var styleObj;
var id = driver.fillLayersModal({ var id = when.fillLayersModal({
type: "background" type: "background",
}) });
driver.isStyleStoreEqual((a: any) => a.layers, [ should.equalStyleStore(
{ (a: any) => a.layers,
"id": id, [
"type": 'background' {
}, id: id,
]); type: "background",
},
]
);
driver.click(driver.getDataAttribute("layer-list-item:"+id+":toggle-visibility", "")); when.click("layer-list-item:" + id + ":toggle-visibility");
driver.isStyleStoreEqual((a: any) => a.layers, [ should.equalStyleStore(
{ (a: any) => a.layers,
"id": id, [
"type": "background", {
"layout": { id: id,
"visibility": "none" type: "background",
} layout: {
}, visibility: "none",
]); },
},
]
);
driver.click(driver.getDataAttribute("layer-list-item:"+id+":toggle-visibility", "")); when.click("layer-list-item:" + id + ":toggle-visibility");
driver.isStyleStoreEqual((a: any) => a.layers, [ should.equalStyleStore(
{ (a: any) => a.layers,
"id": id, [
"type": "background", {
"layout": { id: id,
"visibility": "visible" type: "background",
} layout: {
}, visibility: "visible",
]); },
}) },
}) ]
);
});
describe('background', () => { });
describe("background", () => {
it("add", () => { it("add", () => {
var id = driver.fillLayersModal({ var id = when.fillLayersModal({
type: "background" type: "background",
}) });
driver.isStyleStoreEqual((a: any) => a.layers, [ should.equalStyleStore(
{ (a: any) => a.layers,
"id": id, [
"type": 'background' {
} id: id,
]); type: "background",
},
]
);
}); });
describe("modify", () => { describe("modify", () => {
@@ -114,17 +134,26 @@ describe("layers", () => {
// Setup // Setup
var id = uuid(); var id = uuid();
driver.select(driver.getDataAttribute("add-layer.layer-type", "select"), "background"); when.select(
driver.setValue(driver.getDataAttribute("add-layer.layer-id", "input"), "background:"+id); get.getDataAttribute("add-layer.layer-type", "select"),
"background"
);
when.setValue(
get.getDataAttribute("add-layer.layer-id", "input"),
"background:" + id
);
driver.click(driver.getDataAttribute("add-layer")); when.click("add-layer");
driver.isStyleStoreEqual((a: any) => a.layers, [ should.equalStyleStore(
{ (a: any) => a.layers,
"id": 'background:'+id, [
"type": 'background' {
} id: "background:" + id,
]); type: "background",
},
]
);
return id; return id;
} }
@@ -134,35 +163,47 @@ describe("layers", () => {
it("id", () => { it("id", () => {
var bgId = createBackground(); var bgId = createBackground();
driver.click(driver.getDataAttribute("layer-list-item:background:"+bgId)); when.click("layer-list-item:background:" + bgId);
var id = uuid(); var id = uuid();
driver.setValue(driver.getDataAttribute("layer-editor.layer-id", "input"), "foobar:"+id) when.setValue(
driver.click(driver.getDataAttribute("min-zoom")); get.getDataAttribute("layer-editor.layer-id", "input"),
"foobar:" + id
);
when.click("min-zoom");
driver.isStyleStoreEqual((a: any) => a.layers, [ should.equalStyleStore(
{ (a: any) => a.layers,
"id": 'foobar:'+id, [
"type": 'background' {
} id: "foobar:" + id,
]); type: "background",
},
]
);
}); });
it("min-zoom", () => { it("min-zoom", () => {
var bgId = createBackground(); var bgId = createBackground();
driver.click(driver.getDataAttribute("layer-list-item:background:"+bgId)); when.click("layer-list-item:background:" + bgId);
driver.setValue(driver.getDataAttribute("min-zoom", 'input[type="text"]'), "1"); when.setValue(
get.getDataAttribute("min-zoom", 'input[type="text"]'),
"1"
);
driver.click(driver.getDataAttribute("layer-editor.layer-id", "input")); when.click("layer-editor.layer-id");
driver.isStyleStoreEqual((a: any) => a.layers, [ should.equalStyleStore(
{ (a: any) => a.layers,
"id": 'background:'+bgId, [
"type": 'background', {
"minzoom": 1 id: "background:" + bgId,
} type: "background",
]); minzoom: 1,
},
]
);
// AND RESET! // AND RESET!
// driver.setValue(driver.getDataAttribute("min-zoom", "input"), "") // driver.setValue(driver.getDataAttribute("min-zoom", "input"), "")
@@ -179,38 +220,47 @@ describe("layers", () => {
it("max-zoom", () => { it("max-zoom", () => {
var bgId = createBackground(); var bgId = createBackground();
driver.click(driver.getDataAttribute("layer-list-item:background:"+bgId)); when.click("layer-list-item:background:" + bgId);
driver.setValue(driver.getDataAttribute("max-zoom", 'input[type="text"]'), "1") when.setValue(
get.getDataAttribute("max-zoom", 'input[type="text"]'),
"1"
);
driver.click(driver.getDataAttribute("layer-editor.layer-id", "input")); when.click("layer-editor.layer-id");
driver.isStyleStoreEqual((a: any) => a.layers, [ should.equalStyleStore(
{ (a: any) => a.layers,
"id": 'background:'+bgId, [
"type": 'background', {
"maxzoom": 1 id: "background:" + bgId,
} type: "background",
]); maxzoom: 1,
},
]
);
}); });
it("comments", () => { it("comments", () => {
var bgId = createBackground(); var bgId = createBackground();
var id = uuid(); var id = uuid();
driver.click(driver.getDataAttribute("layer-list-item:background:"+bgId)); when.click("layer-list-item:background:" + bgId);
driver.setValue(driver.getDataAttribute("layer-comment", "textarea"), id); when.setValue(get.getDataAttribute("layer-comment", "textarea"), id);
driver.click(driver.getDataAttribute("layer-editor.layer-id", "input")); when.click("layer-editor.layer-id");
driver.isStyleStoreEqual((a: any) => a.layers, [ should.equalStyleStore(
{ (a: any) => a.layers,
"id": 'background:'+bgId, [
"type": 'background', {
metadata: { id: "background:" + bgId,
'maputnik:comment': id type: "background",
} metadata: {
} "maputnik:comment": id,
]); },
},
]
);
// Unset it again. // Unset it again.
// TODO: This fails // TODO: This fails
@@ -228,31 +278,33 @@ describe("layers", () => {
it("color", () => { it("color", () => {
var bgId = createBackground(); var bgId = createBackground();
driver.click(driver.getDataAttribute("layer-list-item:background:"+bgId)); when.click("layer-list-item:background:" + bgId);
driver.click(driver.getDataAttribute("spec-field:background-color", "input")); when.click("spec-field:background-color");
driver.isStyleStoreEqual((a: any) => a.layers, [ should.equalStyleStore(
{ (a: any) => a.layers,
"id": 'background:'+bgId, [
"type": 'background' {
} id: "background:" + bgId,
]); type: "background",
},
}) ]
}) );
});
});
describe("filter", () => { describe("filter", () => {
it("expand/collapse"); it("expand/collapse");
it("compound filter"); it("compound filter");
}) });
describe("paint", () => { describe("paint", () => {
it("expand/collapse"); it("expand/collapse");
it("color"); it("color");
it("pattern"); it("pattern");
it("opacity"); it("opacity");
}) });
// <===== // <=====
describe("json-editor", () => { describe("json-editor", () => {
@@ -263,165 +315,183 @@ describe("layers", () => {
it.skip("parse error", () => { it.skip("parse error", () => {
var bgId = createBackground(); var bgId = createBackground();
driver.click(driver.getDataAttribute("layer-list-item:background:"+bgId)); when.click("layer-list-item:background:" + bgId);
var errorSelector = ".CodeMirror-lint-marker-error"; var errorSelector = ".CodeMirror-lint-marker-error";
driver.doesNotExists(errorSelector); should.notExist(errorSelector);
driver.click(".CodeMirror"); when.click(".CodeMirror");
driver.typeKeys("\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013 {"); when.typeKeys(
driver.isExists(errorSelector); "\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013 {"
);
should.isExists(errorSelector);
driver.click(driver.getDataAttribute("layer-editor.layer-id")); when.click("layer-editor.layer-id");
}); });
}); });
}) });
}); });
describe('fill', () => { describe("fill", () => {
it("add", () => { it("add", () => {
var id = when.fillLayersModal({
var id = driver.fillLayersModal({
type: "fill", type: "fill",
layer: "example" layer: "example",
}); });
driver.isStyleStoreEqual((a: any) => a.layers, [ should.equalStyleStore(
{ (a: any) => a.layers,
"id": id, [
"type": 'fill', {
"source": "example" id: id,
} type: "fill",
]); source: "example",
}) },
]
);
});
// TODO: Change source // TODO: Change source
it("change source") it("change source");
}); });
describe('line', () => { describe("line", () => {
it("add", () => { it("add", () => {
var id = driver.fillLayersModal({ var id = when.fillLayersModal({
type: "line", type: "line",
layer: "example" layer: "example",
}); });
driver.isStyleStoreEqual((a: any) => a.layers, [ should.equalStyleStore(
{ (a: any) => a.layers,
"id": id, [
"type": "line", {
"source": "example", id: id,
} type: "line",
]); source: "example",
},
]
);
}); });
it("groups", () => { it("groups", () => {
// TODO // TODO
// Click each of the layer groups. // Click each of the layer groups.
}) });
}); });
describe('symbol', () => { describe("symbol", () => {
it("add", () => { it("add", () => {
var id = driver.fillLayersModal({ var id = when.fillLayersModal({
type: "symbol", type: "symbol",
layer: "example" layer: "example",
}); });
driver.isStyleStoreEqual((a: any) => a.layers, [ should.equalStyleStore(
{ (a: any) => a.layers,
"id": id, [
"type": "symbol", {
"source": "example", id: id,
} type: "symbol",
]); source: "example",
},
]
);
}); });
}); });
describe('raster', () => { describe("raster", () => {
it("add", () => { it("add", () => {
var id = driver.fillLayersModal({ var id = when.fillLayersModal({
type: "raster", type: "raster",
layer: "raster" layer: "raster",
}); });
driver.isStyleStoreEqual((a: any) => a.layers, [ should.equalStyleStore(
{ (a: any) => a.layers,
"id": id, [
"type": "raster", {
"source": "raster", id: id,
} type: "raster",
]); source: "raster",
},
]
);
}); });
}); });
describe('circle', () => { describe("circle", () => {
it("add", () => { it("add", () => {
var id = driver.fillLayersModal({ var id = when.fillLayersModal({
type: "circle", type: "circle",
layer: "example" layer: "example",
}); });
driver.isStyleStoreEqual((a: any) => a.layers, [ should.equalStyleStore(
{ (a: any) => a.layers,
"id": id, [
"type": "circle", {
"source": "example", id: id,
} type: "circle",
]); source: "example",
},
]
);
}); });
}); });
describe('fill extrusion', () => { describe("fill extrusion", () => {
it("add", () => { it("add", () => {
var id = driver.fillLayersModal({ var id = when.fillLayersModal({
type: "fill-extrusion", type: "fill-extrusion",
layer: "example" layer: "example",
}); });
driver.isStyleStoreEqual((a: any) => a.layers, [ should.equalStyleStore(
{ (a: any) => a.layers,
"id": id, [
"type": 'fill-extrusion', {
"source": "example" id: id,
} type: "fill-extrusion",
]); source: "example",
},
]
);
}); });
}); });
describe("groups", () => { describe("groups", () => {
it("simple", () => { it("simple", () => {
driver.setStyle("geojson"); when.setStyle("geojson");
driver.openLayersModal(); when.openLayersModal();
driver.fillLayersModal({ when.fillLayersModal({
id: "foo", id: "foo",
type: "background" type: "background",
}) });
driver.openLayersModal(); when.openLayersModal();
driver.fillLayersModal({ when.fillLayersModal({
id: "foo_bar", id: "foo_bar",
type: "background" type: "background",
}) });
driver.openLayersModal(); when.openLayersModal();
driver.fillLayersModal({ when.fillLayersModal({
id: "foo_bar_baz", id: "foo_bar_baz",
type: "background" type: "background",
}) });
driver.isDisplayedInViewport(driver.getDataAttribute("layer-list-item:foo")); should.beVisible("layer-list-item:foo");
driver.isNotDisplayedInViewport(driver.getDataAttribute("layer-list-item:foo_bar"));
driver.isNotDisplayedInViewport(driver.getDataAttribute("layer-list-item:foo_bar_baz"));
driver.click(driver.getDataAttribute("layer-list-group:foo-0")); should.notBeVisible("layer-list-item:foo_bar");
should.notBeVisible("layer-list-item:foo_bar_baz");
driver.isDisplayedInViewport(driver.getDataAttribute("layer-list-item:foo")); when.click("layer-list-group:foo-0");
driver.isDisplayedInViewport(driver.getDataAttribute("layer-list-item:foo_bar"));
driver.isDisplayedInViewport(driver.getDataAttribute("layer-list-item:foo_bar_baz")); should.beVisible("layer-list-item:foo");
}) should.beVisible("layer-list-item:foo_bar");
}) should.beVisible("layer-list-item:foo_bar_baz");
});
});
}); });
+23 -23
View File
@@ -1,25 +1,25 @@
import driver from "./driver"; import MaputnikDriver from "./driver";
describe("map", () => { describe("map", () => {
describe("zoom level", () => { let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
beforeEach(() => { beforeAndAfter();
driver.beforeEach(); describe("zoom level", () => {
}); it("via url", () => {
it("via url", () => { var zoomLevel = 12.37;
var zoomLevel = 12.37; when.setStyle("geojson", zoomLevel);
driver.setStyle("geojson", zoomLevel); should.beVisible("maplibre:ctrl-zoom");
driver.isDisplayedInViewport(".maplibregl-ctrl-zoom"); // HM TODO
// HM TODO //driver.getText(".maplibregl-ctrl-zoom") === "Zoom "+(zoomLevel);
//driver.getText(".maplibregl-ctrl-zoom") === "Zoom "+(zoomLevel); });
})
it("via map controls", () => { it("via map controls", () => {
var zoomLevel = 12.37; var zoomLevel = 12.37;
driver.setStyle("geojson", zoomLevel); when.setStyle("geojson", zoomLevel);
driver.click(".maplibregl-ctrl-zoom-in"); when.click("maplibre:ctrl-zoom");
driver.isDisplayedInViewport(".maplibregl-ctrl-zoom"); should.beVisible("maplibre:ctrl-zoom");
// HM TODO // HM TODO
//driver.getText(".maplibregl-ctrl-zoom") === "Zoom "+(zoomLevel + 1); //driver.getText(".maplibregl-ctrl-zoom") === "Zoom "+(zoomLevel + 1);
}) });
}) });
}) });
+94 -71
View File
@@ -1,137 +1,160 @@
import driver from "./driver"; import MaputnikDriver from "./driver";
describe("modals", () => { describe("modals", () => {
let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
beforeAndAfter();
beforeEach(() => { beforeEach(() => {
driver.beforeEach(); when.setStyle("");
driver.setStyle('');
}); });
describe("open", () => { describe("open", () => {
beforeEach(() => { beforeEach(() => {
driver.click(driver.getDataAttribute("nav:open")); when.click("nav:open");
}); });
it("close", () => { it("close", () => {
driver.closeModal("modal:open"); when.closeModal("modal:open");
should.notExist("modal:open");
}); });
it.skip("upload", () => { it.skip("upload", () => {
// HM: I was not able to make the following choose file actually to select a file and close the modal... // HM: I was not able to make the following choose file actually to select a file and close the modal...
driver.chooseExampleFile(); when.chooseExampleFile();
driver.isStyleStoreEqualToExampleFileData(); should.isStyleStoreEqualToExampleFileData();
}); });
it("load from url", () => { it("load from url", () => {
var styleFileUrl = driver.getExampleFileUrl(); var styleFileUrl = get.getExampleFileUrl();
driver.setValue(driver.getDataAttribute("modal:open.url.input"), styleFileUrl); when.setValue(get.getDataAttribute("modal:open.url.input"), styleFileUrl);
driver.click(driver.getDataAttribute("modal:open.url.button")) when.click("modal:open.url.button");
driver.waitForExampleFileRequset(); when.waitForExampleFileRequset();
driver.isStyleStoreEqualToExampleFileData(); should.isStyleStoreEqualToExampleFileData();
}); });
}) });
describe("shortcuts", () => { describe("shortcuts", () => {
it("open/close", () => { it("open/close", () => {
driver.setStyle(''); when.setStyle("");
when.typeKeys("?");
driver.typeKeys("?"); when.closeModal("modal:shortcuts");
should.notExist("modal:shortcuts");
driver.isDisplayedInViewport(driver.getDataAttribute("modal:shortcuts"));
driver.closeModal("modal:shortcuts");
}); });
}); });
describe("export", () => { describe("export", () => {
beforeEach(() => { beforeEach(() => {
driver.click(driver.getDataAttribute("nav:export")); when.click("nav:export");
}); });
it("close", () => { it("close", () => {
driver.closeModal("modal:export"); when.closeModal("modal:export");
should.notExist("modal:export");
}); });
// TODO: Work out how to download a file and check the contents // TODO: Work out how to download a file and check the contents
it("download") it("download");
});
})
describe("sources", () => { describe("sources", () => {
it("active sources") it("active sources");
it("public source") it("public source");
it("add new source") it("add new source");
}) });
describe("inspect", () => { describe("inspect", () => {
it("toggle", () => { it("toggle", () => {
driver.setStyle('geojson'); when.setStyle("geojson");
driver.select(driver.getDataAttribute("nav:inspect", "select"), "inspect"); when.select(get.getDataAttribute("nav:inspect", "select"), "inspect");
}) });
}) });
describe("style settings", () => { describe("style settings", () => {
beforeEach(() => { beforeEach(() => {
driver.click(driver.getDataAttribute("nav:settings")); when.click("nav:settings");
}); });
it("name", () => { it("name", () => {
driver.setValue(driver.getDataAttribute("modal:settings.name"), "foobar"); when.setValue(get.getDataAttribute("modal:settings.name"), "foobar");
driver.click(driver.getDataAttribute("modal:settings.owner")); when.click("modal:settings.owner");
driver.isStyleStoreEqual((obj) => obj.name, "foobar"); should.equalStyleStore((obj) => obj.name, "foobar");
}) });
it("owner", () => { it("owner", () => {
driver.setValue(driver.getDataAttribute("modal:settings.owner"), "foobar") when.setValue(get.getDataAttribute("modal:settings.owner"), "foobar");
driver.click(driver.getDataAttribute("modal:settings.name")); when.click("modal:settings.name");
driver.isStyleStoreEqual((obj) => obj.owner, "foobar"); should.equalStyleStore((obj) => obj.owner, "foobar");
}) });
it("sprite url", () => { it("sprite url", () => {
driver.setValue(driver.getDataAttribute("modal:settings.sprite"), "http://example.com") when.setValue(
driver.click(driver.getDataAttribute("modal:settings.name")); get.getDataAttribute("modal:settings.sprite"),
"http://example.com"
);
when.click("modal:settings.name");
driver.isStyleStoreEqual((obj) => obj.sprite, "http://example.com"); should.equalStyleStore((obj) => obj.sprite, "http://example.com");
}) });
it("glyphs url", () => { it("glyphs url", () => {
var glyphsUrl = "http://example.com/{fontstack}/{range}.pbf" var glyphsUrl = "http://example.com/{fontstack}/{range}.pbf";
driver.setValue(driver.getDataAttribute("modal:settings.glyphs"), glyphsUrl); when.setValue(get.getDataAttribute("modal:settings.glyphs"), glyphsUrl);
driver.click(driver.getDataAttribute("modal:settings.name")); when.click("modal:settings.name");
driver.isStyleStoreEqual((obj) => obj.glyphs, glyphsUrl); should.equalStyleStore((obj) => obj.glyphs, glyphsUrl);
}) });
it("maptiler access token", () => { it("maptiler access token", () => {
var apiKey = "testing123"; var apiKey = "testing123";
driver.setValue(driver.getDataAttribute("modal:settings.maputnik:openmaptiles_access_token"), apiKey); when.setValue(
driver.click(driver.getDataAttribute("modal:settings.name")); get.getDataAttribute(
"modal:settings.maputnik:openmaptiles_access_token"
),
apiKey
);
when.click("modal:settings.name");
driver.isStyleStoreEqual((obj) => obj.metadata["maputnik:openmaptiles_access_token"], apiKey); should.equalStyleStore(
}) (obj) => obj.metadata["maputnik:openmaptiles_access_token"],
apiKey
);
});
it("thunderforest access token", () => { it("thunderforest access token", () => {
var apiKey = "testing123"; var apiKey = "testing123";
driver.setValue(driver.getDataAttribute("modal:settings.maputnik:thunderforest_access_token"), apiKey); when.setValue(
driver.click(driver.getDataAttribute("modal:settings.name")); get.getDataAttribute(
"modal:settings.maputnik:thunderforest_access_token"
),
apiKey
);
when.click("modal:settings.name");
driver.isStyleStoreEqual((obj) => obj.metadata["maputnik:thunderforest_access_token"], apiKey); should.equalStyleStore(
}) (obj) => obj.metadata["maputnik:thunderforest_access_token"],
apiKey
);
});
it("style renderer", () => { it("style renderer", () => {
cy.on('uncaught:exception', () => false); // this is due to the fact that this is an invalid style for openlayers cy.on("uncaught:exception", () => false); // this is due to the fact that this is an invalid style for openlayers
driver.select(driver.getDataAttribute("modal:settings.maputnik:renderer"), "ol"); when.select(
driver.isSelected(driver.getDataAttribute("modal:settings.maputnik:renderer"), "ol"); get.getDataAttribute("modal:settings.maputnik:renderer"),
"ol"
driver.click(driver.getDataAttribute("modal:settings.name")); );
should.isSelected(
get.getDataAttribute("modal:settings.maputnik:renderer"),
"ol"
);
driver.isStyleStoreEqual((obj) => obj.metadata["maputnik:renderer"], "ol"); when.click("modal:settings.name");
})
}) should.equalStyleStore((obj) => obj.metadata["maputnik:renderer"], "ol");
});
});
describe("sources", () => { describe("sources", () => {
it("toggle") it("toggle");
}) });
}) });
+3 -2
View File
@@ -14,7 +14,8 @@
// *********************************************************** // ***********************************************************
// Import commands.js using ES2015 syntax: // Import commands.js using ES2015 syntax:
import './commands' import "cypress-plugin-tab";
import "./commands";
// Alternatively you can use CommonJS syntax: // Alternatively you can use CommonJS syntax:
// require('./commands') // require('./commands')
+2596 -3649
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -86,6 +86,7 @@
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-replace": "^5.0.5", "@rollup/plugin-replace": "^5.0.5",
"@shellygo/cypress-test-utils": "^2.0.9",
"@storybook/addon-a11y": "^7.6.5", "@storybook/addon-a11y": "^7.6.5",
"@storybook/addon-actions": "^7.6.5", "@storybook/addon-actions": "^7.6.5",
"@storybook/addon-links": "^7.6.5", "@storybook/addon-links": "^7.6.5",
+480 -414
View File
File diff suppressed because it is too large Load Diff
+201 -149
View File
@@ -1,24 +1,33 @@
import React from 'react' import classnames from "classnames";
import PropTypes from 'prop-types' import { detect } from "detect-browser";
import classnames from 'classnames' import PropTypes from "prop-types";
import {detect} from 'detect-browser'; import React from "react";
import {MdFileDownload, MdOpenInBrowser, MdSettings, MdLayers, MdHelpOutline, MdFindInPage, MdAssignmentTurnedIn} from 'react-icons/md' import {
import pkgJson from '../../package.json' MdAssignmentTurnedIn,
MdFileDownload,
MdFindInPage,
MdHelpOutline,
MdLayers,
MdOpenInBrowser,
MdSettings,
} from "react-icons/md";
import logoImage from "maputnik-design/logos/logo-color.svg";
import pkgJson from "../../package.json";
// This is required because of <https://stackoverflow.com/a/49846426>, there isn't another way to detect support that I'm aware of. // This is required because of <https://stackoverflow.com/a/49846426>, there isn't another way to detect support that I'm aware of.
const browser = detect(); const browser = detect();
const colorAccessibilityFiltersEnabled = ['chrome', 'firefox'].indexOf(browser.name) > -1; const colorAccessibilityFiltersEnabled =
["chrome", "firefox"].indexOf(browser.name) > -1;
class IconText extends React.Component { class IconText extends React.Component {
static propTypes = { static propTypes = {
children: PropTypes.node, children: PropTypes.node,
} };
render() { render() {
return <span className="maputnik-icon-text">{this.props.children}</span> return <span className="maputnik-icon-text">{this.props.children}</span>;
} }
} }
@@ -28,17 +37,19 @@ class ToolbarLink extends React.Component {
children: PropTypes.node, children: PropTypes.node,
href: PropTypes.string, href: PropTypes.string,
onToggleModal: PropTypes.func, onToggleModal: PropTypes.func,
} };
render() { render() {
return <a return (
className={classnames('maputnik-toolbar-link', this.props.className)} <a
href={this.props.href} className={classnames("maputnik-toolbar-link", this.props.className)}
rel="noopener noreferrer" href={this.props.href}
target="_blank" rel="noopener noreferrer"
> target="_blank"
{this.props.children} >
</a> {this.props.children}
</a>
);
} }
} }
@@ -47,36 +58,41 @@ class ToolbarLinkHighlighted extends React.Component {
className: PropTypes.string, className: PropTypes.string,
children: PropTypes.node, children: PropTypes.node,
href: PropTypes.string, href: PropTypes.string,
onToggleModal: PropTypes.func onToggleModal: PropTypes.func,
} };
render() { render() {
return <a return (
className={classnames('maputnik-toolbar-link', "maputnik-toolbar-link--highlighted", this.props.className)} <a
href={this.props.href} className={classnames(
rel="noopener noreferrer" "maputnik-toolbar-link",
target="_blank" "maputnik-toolbar-link--highlighted",
> this.props.className
<span className="maputnik-toolbar-link-wrapper"> )}
{this.props.children} href={this.props.href}
</span> rel="noopener noreferrer"
</a> target="_blank"
>
<span className="maputnik-toolbar-link-wrapper">
{this.props.children}
</span>
</a>
);
} }
} }
class ToolbarSelect extends React.Component { class ToolbarSelect extends React.Component {
static propTypes = { static propTypes = {
children: PropTypes.node, children: PropTypes.node,
wdKey: PropTypes.string wdKey: PropTypes.string,
} };
render() { render() {
return <div return (
className='maputnik-toolbar-select' <div className="maputnik-toolbar-select" data-wd-key={this.props.wdKey}>
data-wd-key={this.props.wdKey} {this.props.children}
> </div>
{this.props.children} );
</div>
} }
} }
@@ -84,17 +100,19 @@ class ToolbarAction extends React.Component {
static propTypes = { static propTypes = {
children: PropTypes.node, children: PropTypes.node,
onClick: PropTypes.func, onClick: PropTypes.func,
wdKey: PropTypes.string wdKey: PropTypes.string,
} };
render() { render() {
return <button return (
className='maputnik-toolbar-action' <button
data-wd-key={this.props.wdKey} className="maputnik-toolbar-action"
onClick={this.props.onClick} data-wd-key={this.props.wdKey}
> onClick={this.props.onClick}
{this.props.children} >
</button> {this.props.children}
</button>
);
} }
} }
@@ -112,7 +130,7 @@ export default class AppToolbar extends React.Component {
onSetMapState: PropTypes.func, onSetMapState: PropTypes.func,
mapState: PropTypes.string, mapState: PropTypes.string,
renderer: PropTypes.string, renderer: PropTypes.string,
} };
state = { state = {
isOpen: { isOpen: {
@@ -121,8 +139,8 @@ export default class AppToolbar extends React.Component {
open: false, open: false,
add: false, add: false,
export: false, export: false,
} },
} };
handleSelection(val) { handleSelection(val) {
this.props.onSetMapState(val); this.props.onSetMapState(val);
@@ -131,12 +149,11 @@ export default class AppToolbar extends React.Component {
onSkip = (target) => { onSkip = (target) => {
if (target === "map") { if (target === "map") {
document.querySelector(".maplibregl-canvas").focus(); document.querySelector(".maplibregl-canvas").focus();
} } else {
else { const el = document.querySelector("#skip-target-" + target);
const el = document.querySelector("#skip-target-"+target);
el.focus(); el.focus();
} }
} };
render() { render() {
const views = [ const views = [
@@ -149,7 +166,7 @@ export default class AppToolbar extends React.Component {
id: "inspect", id: "inspect",
group: "general", group: "general",
title: "Inspect", title: "Inspect",
disabled: this.props.renderer === 'ol', disabled: this.props.renderer === "ol",
}, },
{ {
id: "filter-deuteranopia", id: "filter-deuteranopia",
@@ -181,102 +198,137 @@ export default class AppToolbar extends React.Component {
return view.id === this.props.mapState; return view.id === this.props.mapState;
}); });
return <nav className='maputnik-toolbar'> return (
<div className="maputnik-toolbar__inner"> <nav className="maputnik-toolbar">
<div <div className="maputnik-toolbar__inner">
className="maputnik-toolbar-logo-container" <div className="maputnik-toolbar-logo-container">
> {/* Keyboard accessible quick links */}
{/* Keyboard accessible quick links */} <button
<button data-wd-key="root:skip:layer-list"
data-wd-key="root:skip:layer-list" className="maputnik-toolbar-skip"
className="maputnik-toolbar-skip" onClick={(e) => this.onSkip("layer-list")}
onClick={e => this.onSkip("layer-list")} >
Layers list
</button>
<button
data-wd-key="root:skip:layer-editor"
className="maputnik-toolbar-skip"
onClick={(e) => this.onSkip("layer-editor")}
>
Layer editor
</button>
<button
data-wd-key="root:skip:map-view"
className="maputnik-toolbar-skip"
onClick={(e) => this.onSkip("map")}
>
Map view
</button>
<a
className="maputnik-toolbar-logo"
target="blank"
rel="noreferrer noopener"
href="https://github.com/maputnik/editor"
>
<span dangerouslySetInnerHTML={{ __html: logoImage }} />
<h1>
<span className="maputnik-toolbar-name">{pkgJson.name}</span>
<span className="maputnik-toolbar-version">
v{pkgJson.version}
</span>
</h1>
</a>
</div>
<div
className="maputnik-toolbar__actions"
role="navigation"
aria-label="Toolbar"
> >
Layers list <ToolbarAction
</button> wdKey="nav:open"
<button onClick={this.props.onToggleModal.bind(this, "open")}
data-wd-key="root:skip:layer-editor" >
className="maputnik-toolbar-skip" <MdOpenInBrowser />
onClick={e => this.onSkip("layer-editor")} <IconText>Open</IconText>
> </ToolbarAction>
Layer editor <ToolbarAction
</button> wdKey="nav:export"
<button onClick={this.props.onToggleModal.bind(this, "export")}
data-wd-key="root:skip:map-view" >
className="maputnik-toolbar-skip" <MdFileDownload />
onClick={e => this.onSkip("map")} <IconText>Export</IconText>
> </ToolbarAction>
Map view <ToolbarAction
</button> wdKey="nav:sources"
<a onClick={this.props.onToggleModal.bind(this, "sources")}
className="maputnik-toolbar-logo" >
target="blank" <MdLayers />
rel="noreferrer noopener" <IconText>Data Sources</IconText>
href="https://github.com/maputnik/editor" </ToolbarAction>
> <ToolbarAction
<img src="node_modules/maputnik-design/logos/logo-color.svg" /> wdKey="nav:settings"
<h1> onClick={this.props.onToggleModal.bind(this, "settings")}
<span className="maputnik-toolbar-name">{pkgJson.name}</span> >
<span className="maputnik-toolbar-version">v{pkgJson.version}</span> <MdSettings />
</h1> <IconText>Style Settings</IconText>
</a> </ToolbarAction>
</div>
<div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar">
<ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}>
<MdOpenInBrowser />
<IconText>Open</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, 'export')}>
<MdFileDownload />
<IconText>Export</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, 'sources')}>
<MdLayers />
<IconText>Data Sources</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}>
<MdSettings />
<IconText>Style Settings</IconText>
</ToolbarAction>
<ToolbarSelect wdKey="nav:inspect"> <ToolbarSelect wdKey="nav:inspect">
<MdFindInPage /> <MdFindInPage />
<label>View <label>
<select View
className="maputnik-select" <select
onChange={(e) => this.handleSelection(e.target.value)} className="maputnik-select"
value={currentView.id} data-wd-key="maputnik-select"
> onChange={(e) => this.handleSelection(e.target.value)}
{views.filter(v => v.group === "general").map((item) => { value={currentView.id}
return ( >
<option key={item.id} value={item.id} disabled={item.disabled}> {views
{item.title} .filter((v) => v.group === "general")
</option> .map((item) => {
); return (
})} <option
<optgroup label="Color accessibility"> key={item.id}
{views.filter(v => v.group === "color-accessibility").map((item) => { value={item.id}
return ( disabled={item.disabled}
<option key={item.id} value={item.id} disabled={item.disabled}> data-wd-key={item.id}
{item.title} >
</option> {item.title}
); </option>
})} );
</optgroup> })}
</select> <optgroup label="Color accessibility">
</label> {views
</ToolbarSelect> .filter((v) => v.group === "color-accessibility")
.map((item) => {
return (
<option
key={item.id}
value={item.id}
disabled={item.disabled}
>
{item.title}
</option>
);
})}
</optgroup>
</select>
</label>
</ToolbarSelect>
<ToolbarLink href={"https://github.com/maputnik/editor/wiki"}> <ToolbarLink href={"https://github.com/maputnik/editor/wiki"}>
<MdHelpOutline /> <MdHelpOutline />
<IconText>Help</IconText> <IconText>Help</IconText>
</ToolbarLink> </ToolbarLink>
<ToolbarLinkHighlighted href={"https://gregorywolanski.typeform.com/to/cPgaSY"}> <ToolbarLinkHighlighted
<MdAssignmentTurnedIn /> href={"https://gregorywolanski.typeform.com/to/cPgaSY"}
<IconText>Take the Maputnik Survey</IconText> >
</ToolbarLinkHighlighted> <MdAssignmentTurnedIn />
<IconText>Take the Maputnik Survey</IconText>
</ToolbarLinkHighlighted>
</div>
</div> </div>
</div> </nav>
</nav> );
} }
} }
+249 -207
View File
@@ -1,47 +1,46 @@
import React from 'react' import PropTypes from "prop-types";
import PropTypes from 'prop-types' import React from "react";
import { Wrapper, Button, Menu, MenuItem } from 'react-aria-menubutton' import { Button, Menu, MenuItem, Wrapper } from "react-aria-menubutton";
import FieldJson from './FieldJson' import { Accordion } from "react-accessible-accordion";
import FilterEditor from './FilterEditor' import FieldComment from "./FieldComment";
import PropertyGroup from './PropertyGroup' import FieldId from "./FieldId";
import LayerEditorGroup from './LayerEditorGroup' import FieldJson from "./FieldJson";
import FieldType from './FieldType' import FieldMaxZoom from "./FieldMaxZoom";
import FieldId from './FieldId' import FieldMinZoom from "./FieldMinZoom";
import FieldMinZoom from './FieldMinZoom' import FieldSource from "./FieldSource";
import FieldMaxZoom from './FieldMaxZoom' import FieldSourceLayer from "./FieldSourceLayer";
import FieldComment from './FieldComment' import FieldType from "./FieldType";
import FieldSource from './FieldSource' import FilterEditor from "./FilterEditor";
import FieldSourceLayer from './FieldSourceLayer' import LayerEditorGroup from "./LayerEditorGroup";
import {Accordion} from 'react-accessible-accordion'; import PropertyGroup from "./PropertyGroup";
import {MdMoreVert} from 'react-icons/md' import { MdMoreVert } from "react-icons/md";
import { changeType, changeProperty } from '../libs/layer' import layout from "../config/layout.json";
import layout from '../config/layout.json' import { changeProperty, changeType } from "../libs/layer";
import {formatLayerId} from '../util/format'; import { formatLayerId } from "../util/format";
function getLayoutForType(type) {
function getLayoutForType (type) {
return layout[type] ? layout[type] : layout.invalid; return layout[type] ? layout[type] : layout.invalid;
} }
function layoutGroups(layerType) { function layoutGroups(layerType) {
const layerGroup = { const layerGroup = {
title: 'Layer', title: "Layer",
type: 'layer' type: "layer",
} };
const filterGroup = { const filterGroup = {
title: 'Filter', title: "Filter",
type: 'filter' type: "filter",
} };
const editorGroup = { const editorGroup = {
title: 'JSON Editor', title: "JSON Editor",
type: 'jsoneditor' type: "jsoneditor",
} };
return [layerGroup, filterGroup] return [layerGroup, filterGroup]
.concat(getLayoutForType(layerType).groups) .concat(getLayoutForType(layerType).groups)
.concat([editorGroup]) .concat([editorGroup]);
} }
/** Layer editor supporting multiple types of layers. */ /** Layer editor supporting multiple types of layers. */
@@ -61,277 +60,320 @@ export default class LayerEditor extends React.Component {
isLastLayer: PropTypes.bool, isLastLayer: PropTypes.bool,
layerIndex: PropTypes.number, layerIndex: PropTypes.number,
errors: PropTypes.array, errors: PropTypes.array,
} };
static defaultProps = { static defaultProps = {
onLayerChanged: () => {}, onLayerChanged: () => {},
onLayerIdChange: () => {}, onLayerIdChange: () => {},
onLayerDestroyed: () => {}, onLayerDestroyed: () => {},
} };
static childContextTypes = { static childContextTypes = {
reactIconBase: PropTypes.object reactIconBase: PropTypes.object,
} };
constructor(props) { constructor(props) {
super(props) super(props);
//TODO: Clean this up and refactor into function //TODO: Clean this up and refactor into function
const editorGroups = {} const editorGroups = {};
layoutGroups(this.props.layer.type).forEach(group => { layoutGroups(this.props.layer.type).forEach((group) => {
editorGroups[group.title] = true editorGroups[group.title] = true;
}) });
this.state = { editorGroups } this.state = { editorGroups };
} }
static getDerivedStateFromProps(props, state) { static getDerivedStateFromProps(props, state) {
const additionalGroups = { ...state.editorGroups } const additionalGroups = { ...state.editorGroups };
getLayoutForType(props.layer.type).groups.forEach(group => { getLayoutForType(props.layer.type).groups.forEach((group) => {
if(!(group.title in additionalGroups)) { if (!(group.title in additionalGroups)) {
additionalGroups[group.title] = true additionalGroups[group.title] = true;
} }
}) });
return { return {
editorGroups: additionalGroups editorGroups: additionalGroups,
}; };
} }
getChildContext () { getChildContext() {
return { return {
reactIconBase: { reactIconBase: {
size: 14, size: 14,
color: '#8e8e8e', color: "#8e8e8e",
} },
} };
} }
changeProperty(group, property, newValue) { changeProperty(group, property, newValue) {
this.props.onLayerChanged( this.props.onLayerChanged(
this.props.layerIndex, this.props.layerIndex,
changeProperty(this.props.layer, group, property, newValue) changeProperty(this.props.layer, group, property, newValue)
) );
} }
onGroupToggle(groupTitle, active) { onGroupToggle(groupTitle, active) {
const changedActiveGroups = { const changedActiveGroups = {
...this.state.editorGroups, ...this.state.editorGroups,
[groupTitle]: active, [groupTitle]: active,
} };
this.setState({ this.setState({
editorGroups: changedActiveGroups editorGroups: changedActiveGroups,
}) });
} }
renderGroupType(type, fields) { renderGroupType(type, fields) {
let comment = "" let comment = "";
if(this.props.layer.metadata) { if (this.props.layer.metadata) {
comment = this.props.layer.metadata['maputnik:comment'] comment = this.props.layer.metadata["maputnik:comment"];
} }
const {errors, layerIndex} = this.props; const { errors, layerIndex } = this.props;
const errorData = {}; const errorData = {};
errors.forEach(error => { errors.forEach((error) => {
if ( if (
error.parsed && error.parsed &&
error.parsed.type === "layer" && error.parsed.type === "layer" &&
error.parsed.data.index == layerIndex error.parsed.data.index == layerIndex
) { ) {
errorData[error.parsed.data.key] = { errorData[error.parsed.data.key] = {
message: error.parsed.data.message message: error.parsed.data.message,
}; };
} }
}) });
let sourceLayerIds; let sourceLayerIds;
if(this.props.sources.hasOwnProperty(this.props.layer.source)) { if (this.props.sources.hasOwnProperty(this.props.layer.source)) {
sourceLayerIds = this.props.sources[this.props.layer.source].layers; sourceLayerIds = this.props.sources[this.props.layer.source].layers;
} }
switch(type) { switch (type) {
case 'layer': return <div> case "layer":
<FieldId return (
value={this.props.layer.id} <div>
wdKey="layer-editor.layer-id" <FieldId
error={errorData.id} value={this.props.layer.id}
onChange={newId => this.props.onLayerIdChange(this.props.layerIndex, this.props.layer.id, newId)} wdKey="layer-editor.layer-id"
/> error={errorData.id}
<FieldType onChange={(newId) =>
disabled={true} this.props.onLayerIdChange(
error={errorData.type} this.props.layerIndex,
value={this.props.layer.type} this.props.layer.id,
onChange={newType => this.props.onLayerChanged( newId
this.props.layerIndex, )
changeType(this.props.layer, newType) }
)} />
/> <FieldType
{this.props.layer.type !== 'background' && <FieldSource disabled={true}
error={errorData.source} error={errorData.type}
sourceIds={Object.keys(this.props.sources)} value={this.props.layer.type}
value={this.props.layer.source} onChange={(newType) =>
onChange={v => this.changeProperty(null, 'source', v)} this.props.onLayerChanged(
/> this.props.layerIndex,
} changeType(this.props.layer, newType)
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.props.layer.type) < 0 && )
<FieldSourceLayer }
error={errorData['source-layer']} />
sourceLayerIds={sourceLayerIds} {this.props.layer.type !== "background" && (
value={this.props.layer['source-layer']} <FieldSource
onChange={v => this.changeProperty(null, 'source-layer', v)} error={errorData.source}
/> sourceIds={Object.keys(this.props.sources)}
} value={this.props.layer.source}
<FieldMinZoom onChange={(v) => this.changeProperty(null, "source", v)}
error={errorData.minzoom} />
value={this.props.layer.minzoom} )}
onChange={v => this.changeProperty(null, 'minzoom', v)} {["background", "raster", "hillshade", "heatmap"].indexOf(
/> this.props.layer.type
<FieldMaxZoom ) < 0 && (
error={errorData.maxzoom} <FieldSourceLayer
value={this.props.layer.maxzoom} error={errorData["source-layer"]}
onChange={v => this.changeProperty(null, 'maxzoom', v)} sourceLayerIds={sourceLayerIds}
/> value={this.props.layer["source-layer"]}
<FieldComment onChange={(v) => this.changeProperty(null, "source-layer", v)}
error={errorData.comment} />
value={comment} )}
onChange={v => this.changeProperty('metadata', 'maputnik:comment', v == "" ? undefined : v)} <FieldMinZoom
/> error={errorData.minzoom}
</div> value={this.props.layer.minzoom}
case 'filter': return <div> onChange={(v) => this.changeProperty(null, "minzoom", v)}
<div className="maputnik-filter-editor-wrapper"> />
<FilterEditor <FieldMaxZoom
error={errorData.maxzoom}
value={this.props.layer.maxzoom}
onChange={(v) => this.changeProperty(null, "maxzoom", v)}
/>
<FieldComment
error={errorData.comment}
value={comment}
onChange={(v) =>
this.changeProperty(
"metadata",
"maputnik:comment",
v == "" ? undefined : v
)
}
/>
</div>
);
case "filter":
return (
<div>
<div className="maputnik-filter-editor-wrapper">
<FilterEditor
errors={errorData}
filter={this.props.layer.filter}
properties={
this.props.vectorLayers[this.props.layer["source-layer"]]
}
onChange={(f) => this.changeProperty(null, "filter", f)}
/>
</div>
</div>
);
case "properties":
return (
<PropertyGroup
errors={errorData} errors={errorData}
filter={this.props.layer.filter} layer={this.props.layer}
properties={this.props.vectorLayers[this.props.layer['source-layer']]} groupFields={fields}
onChange={f => this.changeProperty(null, 'filter', f)} spec={this.props.spec}
onChange={this.changeProperty.bind(this)}
/> />
</div> );
</div> case "jsoneditor":
case 'properties': return (
return <PropertyGroup <FieldJson
errors={errorData} layer={this.props.layer}
layer={this.props.layer} onChange={(layer) => {
groupFields={fields} this.props.onLayerChanged(this.props.layerIndex, layer);
spec={this.props.spec} }}
onChange={this.changeProperty.bind(this)} />
/> );
case 'jsoneditor':
return <FieldJson
layer={this.props.layer}
onChange={(layer) => {
this.props.onLayerChanged(
this.props.layerIndex,
layer
);
}}
/>
} }
} }
moveLayer(offset) { moveLayer(offset) {
this.props.onMoveLayer({ this.props.onMoveLayer({
oldIndex: this.props.layerIndex, oldIndex: this.props.layerIndex,
newIndex: this.props.layerIndex+offset newIndex: this.props.layerIndex + offset,
}) });
} }
render() { render() {
const groupIds = []; const groupIds = [];
const layerType = this.props.layer.type const layerType = this.props.layer.type;
const groups = layoutGroups(layerType).filter(group => { const groups = layoutGroups(layerType)
return !(layerType === 'background' && group.type === 'source') .filter((group) => {
}).map(group => { return !(layerType === "background" && group.type === "source");
const groupId = group.title.replace(/ /g, "_"); })
groupIds.push(groupId); .map((group) => {
return <LayerEditorGroup const groupId = group.title.replace(/ /g, "_");
data-wd-key={group.title} groupIds.push(groupId);
id={groupId} return (
key={group.title} <LayerEditorGroup
title={group.title} data-wd-key={group.title}
isActive={this.state.editorGroups[group.title]} id={groupId}
onActiveToggle={this.onGroupToggle.bind(this, group.title)} key={group.title}
> title={group.title}
{this.renderGroupType(group.type, group.fields)} isActive={this.state.editorGroups[group.title]}
</LayerEditorGroup> onActiveToggle={this.onGroupToggle.bind(this, group.title)}
}) >
{this.renderGroupType(group.type, group.fields)}
</LayerEditorGroup>
);
});
const layout = this.props.layer.layout || {} const layout = this.props.layer.layout || {};
const items = { const items = {
delete: { delete: {
text: "Delete", text: "Delete",
handler: () => this.props.onLayerDestroy(this.props.layerIndex) handler: () => this.props.onLayerDestroy(this.props.layerIndex),
}, },
duplicate: { duplicate: {
text: "Duplicate", text: "Duplicate",
handler: () => this.props.onLayerCopy(this.props.layerIndex) handler: () => this.props.onLayerCopy(this.props.layerIndex),
}, },
hide: { hide: {
text: (layout.visibility === "none") ? "Show" : "Hide", text: layout.visibility === "none" ? "Show" : "Hide",
handler: () => this.props.onLayerVisibilityToggle(this.props.layerIndex) handler: () =>
this.props.onLayerVisibilityToggle(this.props.layerIndex),
}, },
moveLayerUp: { moveLayerUp: {
text: "Move layer up", text: "Move layer up",
// Not actually used... // Not actually used...
disabled: this.props.isFirstLayer, disabled: this.props.isFirstLayer,
handler: () => this.moveLayer(-1) handler: () => this.moveLayer(-1),
}, },
moveLayerDown: { moveLayerDown: {
text: "Move layer down", text: "Move layer down",
// Not actually used... // Not actually used...
disabled: this.props.isLastLayer, disabled: this.props.isLastLayer,
handler: () => this.moveLayer(+1) handler: () => this.moveLayer(+1),
} },
} };
function handleSelection(id, event) { function handleSelection(id, event) {
event.stopPropagation; event.stopPropagation;
items[id].handler(); items[id].handler();
} }
return <section className="maputnik-layer-editor" return (
role="main" <section
aria-label="Layer editor" className="maputnik-layer-editor"
> role="main"
<header> aria-label="Layer editor"
<div className="layer-header">
<h2 className="layer-header__title">
Layer: {formatLayerId(this.props.layer.id)}
</h2>
<div className="layer-header__info">
<Wrapper
className='more-menu'
onSelection={handleSelection}
closeOnSelection={false}
>
<Button id="skip-target-layer-editor" className='more-menu__button' title="Layer options">
<MdMoreVert className="more-menu__button__svg" />
</Button>
<Menu>
<ul className="more-menu__menu">
{Object.keys(items).map((id, idx) => {
const item = items[id];
return <li key={id}>
<MenuItem value={id} className='more-menu__menu__item'>
{item.text}
</MenuItem>
</li>
})}
</ul>
</Menu>
</Wrapper>
</div>
</div>
</header>
<Accordion
allowMultipleExpanded={true}
allowZeroExpanded={true}
preExpanded={groupIds}
> >
{groups} <header>
</Accordion> <div className="layer-header">
</section> <h2 className="layer-header__title">
Layer: {formatLayerId(this.props.layer.id)}
</h2>
<div className="layer-header__info">
<Wrapper
className="more-menu"
onSelection={handleSelection}
closeOnSelection={false}
>
<Button
data-wd-key="skip-target-layer-editor"
id="skip-target-layer-editor"
className="more-menu__button"
title="Layer options"
>
<MdMoreVert className="more-menu__button__svg" />
</Button>
<Menu>
<ul className="more-menu__menu">
{Object.keys(items).map((id, idx) => {
const item = items[id];
return (
<li key={id}>
<MenuItem
value={id}
className="more-menu__menu__item"
>
{item.text}
</MenuItem>
</li>
);
})}
</ul>
</Menu>
</Wrapper>
</div>
</div>
</header>
<Accordion
allowMultipleExpanded={true}
allowZeroExpanded={true}
preExpanded={groupIds}
>
{groups}
</Accordion>
</section>
);
} }
} }
+175 -154
View File
@@ -1,13 +1,13 @@
import React from 'react' import classnames from "classnames";
import PropTypes from 'prop-types' import lodash from "lodash";
import classnames from 'classnames' import PropTypes from "prop-types";
import lodash from 'lodash'; import React from "react";
import LayerListGroup from './LayerListGroup' import LayerListGroup from "./LayerListGroup";
import LayerListItem from './LayerListItem' import LayerListItem from "./LayerListItem";
import ModalAdd from './ModalAdd' import ModalAdd from "./ModalAdd";
import {SortableContainer} from 'react-sortable-hoc'; import { SortableContainer } from "react-sortable-hoc";
const layerListPropTypes = { const layerListPropTypes = {
layers: PropTypes.array.isRequired, layers: PropTypes.array.isRequired,
@@ -15,34 +15,34 @@ const layerListPropTypes = {
onLayersChange: PropTypes.func.isRequired, onLayersChange: PropTypes.func.isRequired,
onLayerSelect: PropTypes.func, onLayerSelect: PropTypes.func,
sources: PropTypes.object.isRequired, sources: PropTypes.object.isRequired,
} };
function layerPrefix(name) { function layerPrefix(name) {
return name.replace(' ', '-').replace('_', '-').split('-')[0] return name.replace(" ", "-").replace("_", "-").split("-")[0];
} }
function findClosestCommonPrefix(layers, idx) { function findClosestCommonPrefix(layers, idx) {
const currentLayerPrefix = layerPrefix(layers[idx].id) const currentLayerPrefix = layerPrefix(layers[idx].id);
let closestIdx = idx let closestIdx = idx;
for (let i = idx; i > 0; i--) { for (let i = idx; i > 0; i--) {
const previousLayerPrefix = layerPrefix(layers[i-1].id) const previousLayerPrefix = layerPrefix(layers[i - 1].id);
if(previousLayerPrefix === currentLayerPrefix) { if (previousLayerPrefix === currentLayerPrefix) {
closestIdx = i - 1 closestIdx = i - 1;
} else { } else {
return closestIdx return closestIdx;
} }
} }
return closestIdx return closestIdx;
} }
let UID = 0; let UID = 0;
// List of collapsible layer editors // List of collapsible layer editors
class LayerListContainer extends React.Component { class LayerListContainer extends React.Component {
static propTypes = {...layerListPropTypes} static propTypes = { ...layerListPropTypes };
static defaultProps = { static defaultProps = {
onLayerSelect: () => {}, onLayerSelect: () => {},
} };
constructor(props) { constructor(props) {
super(props); super(props);
@@ -56,8 +56,8 @@ class LayerListContainer extends React.Component {
}, },
isOpen: { isOpen: {
add: false, add: false,
} },
} };
} }
toggleModal(modalName) { toggleModal(modalName) {
@@ -68,79 +68,82 @@ class LayerListContainer extends React.Component {
}, },
isOpen: { isOpen: {
...this.state.isOpen, ...this.state.isOpen,
[modalName]: !this.state.isOpen[modalName] [modalName]: !this.state.isOpen[modalName],
} },
}) });
} }
toggleLayers = () => { toggleLayers = () => {
let idx=0 let idx = 0;
let newGroups=[] let newGroups = [];
this.groupedLayers().forEach(layers => {
const groupPrefix = layerPrefix(layers[0].id)
const lookupKey = [groupPrefix, idx].join('-')
this.groupedLayers().forEach((layers) => {
const groupPrefix = layerPrefix(layers[0].id);
const lookupKey = [groupPrefix, idx].join("-");
if (layers.length > 1) { if (layers.length > 1) {
newGroups[lookupKey] = this.state.areAllGroupsExpanded newGroups[lookupKey] = this.state.areAllGroupsExpanded;
} }
layers.forEach((layer) => { layers.forEach((layer) => {
idx += 1 idx += 1;
}) });
}); });
this.setState({ this.setState({
collapsedGroups: newGroups, collapsedGroups: newGroups,
areAllGroupsExpanded: !this.state.areAllGroupsExpanded areAllGroupsExpanded: !this.state.areAllGroupsExpanded,
}) });
} };
groupedLayers() { groupedLayers() {
const groups = [] const groups = [];
const layerIdCount = new Map(); const layerIdCount = new Map();
for (let i = 0; i < this.props.layers.length; i++) { for (let i = 0; i < this.props.layers.length; i++) {
const origLayer = this.props.layers[i]; const origLayer = this.props.layers[i];
const previousLayer = this.props.layers[i-1] const previousLayer = this.props.layers[i - 1];
layerIdCount.set(origLayer.id, layerIdCount.set(
origLayer.id,
layerIdCount.has(origLayer.id) ? layerIdCount.get(origLayer.id) + 1 : 0 layerIdCount.has(origLayer.id) ? layerIdCount.get(origLayer.id) + 1 : 0
); );
const layer = { const layer = {
...origLayer, ...origLayer,
key: `layers-list-${origLayer.id}-${layerIdCount.get(origLayer.id)}`, key: `layers-list-${origLayer.id}-${layerIdCount.get(origLayer.id)}`,
} };
if(previousLayer && layerPrefix(previousLayer.id) == layerPrefix(layer.id)) { if (
const lastGroup = groups[groups.length - 1] previousLayer &&
lastGroup.push(layer) layerPrefix(previousLayer.id) == layerPrefix(layer.id)
) {
const lastGroup = groups[groups.length - 1];
lastGroup.push(layer);
} else { } else {
groups.push([layer]) groups.push([layer]);
} }
} }
return groups return groups;
} }
toggleLayerGroup(groupPrefix, idx) { toggleLayerGroup(groupPrefix, idx) {
const lookupKey = [groupPrefix, idx].join('-') const lookupKey = [groupPrefix, idx].join("-");
const newGroups = { ...this.state.collapsedGroups } const newGroups = { ...this.state.collapsedGroups };
if(lookupKey in this.state.collapsedGroups) { if (lookupKey in this.state.collapsedGroups) {
newGroups[lookupKey] = !this.state.collapsedGroups[lookupKey] newGroups[lookupKey] = !this.state.collapsedGroups[lookupKey];
} else { } else {
newGroups[lookupKey] = false newGroups[lookupKey] = false;
} }
this.setState({ this.setState({
collapsedGroups: newGroups collapsedGroups: newGroups,
}) });
} }
isCollapsed(groupPrefix, idx) { isCollapsed(groupPrefix, idx) {
const collapsed = this.state.collapsedGroups[[groupPrefix, idx].join('-')] const collapsed = this.state.collapsedGroups[[groupPrefix, idx].join("-")];
return collapsed === undefined ? true : collapsed return collapsed === undefined ? true : collapsed;
} }
shouldComponentUpdate (nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
// Always update on state change // Always update on state change
if (this.state !== nextState) { if (this.state !== nextState) {
return true; return true;
@@ -148,28 +151,28 @@ class LayerListContainer extends React.Component {
// This component tree only requires id and visibility from the layers // This component tree only requires id and visibility from the layers
// objects // objects
function getRequiredProps (layer) { function getRequiredProps(layer) {
const out = { const out = {
id: layer.id, id: layer.id,
}; };
if (layer.layout) { if (layer.layout) {
out.layout = { out.layout = {
visibility: layer.layout.visibility visibility: layer.layout.visibility,
}; };
} }
return out; return out;
} }
const layersEqual = lodash.isEqual( const layersEqual = lodash.isEqual(
nextProps.layers.map(getRequiredProps), nextProps.layers.map(getRequiredProps),
this.props.layers.map(getRequiredProps), this.props.layers.map(getRequiredProps)
); );
function withoutLayers (props) { function withoutLayers(props) {
const out = { const out = {
...props ...props,
}; };
delete out['layers']; delete out["layers"];
return out; return out;
} }
@@ -184,16 +187,16 @@ class LayerListContainer extends React.Component {
return propsChanged; return propsChanged;
} }
componentDidUpdate (prevProps) { componentDidUpdate(prevProps) {
if (prevProps.selectedLayerIndex !== this.props.selectedLayerIndex) { if (prevProps.selectedLayerIndex !== this.props.selectedLayerIndex) {
const selectedItemNode = this.selectedItemRef.current; const selectedItemNode = this.selectedItemRef.current;
if (selectedItemNode && selectedItemNode.node) { if (selectedItemNode && selectedItemNode.node) {
const target = selectedItemNode.node; const target = selectedItemNode.node;
const options = { const options = {
root: this.scrollContainerRef.current, root: this.scrollContainerRef.current,
threshold: 1.0 threshold: 1.0,
} };
const observer = new IntersectionObserver(entries => { const observer = new IntersectionObserver((entries) => {
observer.unobserve(target); observer.unobserve(target);
if (entries.length > 0 && entries[0].intersectionRatio < 1) { if (entries.length > 0 && entries[0].intersectionRatio < 1) {
target.scrollIntoView(); target.scrollIntoView();
@@ -206,28 +209,32 @@ class LayerListContainer extends React.Component {
} }
render() { render() {
const listItems = [];
const listItems = [] let idx = 0;
let idx = 0
const layersByGroup = this.groupedLayers(); const layersByGroup = this.groupedLayers();
layersByGroup.forEach(layers => { layersByGroup.forEach((layers) => {
const groupPrefix = layerPrefix(layers[0].id) const groupPrefix = layerPrefix(layers[0].id);
if(layers.length > 1) { if (layers.length > 1) {
const grp = <LayerListGroup const grp = (
data-wd-key={[groupPrefix, idx].join('-')} <LayerListGroup
aria-controls={layers.map(l => l.key).join(" ")} data-wd-key={[groupPrefix, idx].join("-")}
key={`group-${groupPrefix}-${idx}`} aria-controls={layers.map((l) => l.key).join(" ")}
title={groupPrefix} key={`group-${groupPrefix}-${idx}`}
isActive={!this.isCollapsed(groupPrefix, idx) || idx === this.props.selectedLayerIndex} title={groupPrefix}
onActiveToggle={this.toggleLayerGroup.bind(this, groupPrefix, idx)} isActive={
/> !this.isCollapsed(groupPrefix, idx) ||
listItems.push(grp) idx === this.props.selectedLayerIndex
}
onActiveToggle={this.toggleLayerGroup.bind(this, groupPrefix, idx)}
/>
);
listItems.push(grp);
} }
layers.forEach((layer, idxInGroup) => { layers.forEach((layer, idxInGroup) => {
const groupIdx = findClosestCommonPrefix(this.props.layers, idx) const groupIdx = findClosestCommonPrefix(this.props.layers, idx);
const layerError = this.props.errors.find(error => { const layerError = this.props.errors.find((error) => {
return ( return (
error.parsed && error.parsed &&
error.parsed.type === "layer" && error.parsed.type === "layer" &&
@@ -240,93 +247,107 @@ class LayerListContainer extends React.Component {
additionalProps.ref = this.selectedItemRef; additionalProps.ref = this.selectedItemRef;
} }
const listItem = <LayerListItem const listItem = (
className={classnames({ <LayerListItem
'maputnik-layer-list-item-collapsed': layers.length > 1 && this.isCollapsed(groupPrefix, groupIdx) && idx !== this.props.selectedLayerIndex, className={classnames({
'maputnik-layer-list-item-group-last': idxInGroup == layers.length - 1 && layers.length > 1, "maputnik-layer-list-item-collapsed":
'maputnik-layer-list-item--error': !!layerError layers.length > 1 &&
})} this.isCollapsed(groupPrefix, groupIdx) &&
index={idx} idx !== this.props.selectedLayerIndex,
key={layer.key} "maputnik-layer-list-item-group-last":
id={layer.key} idxInGroup == layers.length - 1 && layers.length > 1,
layerId={layer.id} "maputnik-layer-list-item--error": !!layerError,
layerIndex={idx} })}
layerType={layer.type} index={idx}
visibility={(layer.layout || {}).visibility} key={layer.key}
isSelected={idx === this.props.selectedLayerIndex} id={layer.key}
onLayerSelect={this.props.onLayerSelect} layerId={layer.id}
onLayerDestroy={this.props.onLayerDestroy.bind(this)} layerIndex={idx}
onLayerCopy={this.props.onLayerCopy.bind(this)} layerType={layer.type}
onLayerVisibilityToggle={this.props.onLayerVisibilityToggle.bind(this)} visibility={(layer.layout || {}).visibility}
{...additionalProps} isSelected={idx === this.props.selectedLayerIndex}
/> onLayerSelect={this.props.onLayerSelect}
listItems.push(listItem) onLayerDestroy={this.props.onLayerDestroy.bind(this)}
idx += 1 onLayerCopy={this.props.onLayerCopy.bind(this)}
}) onLayerVisibilityToggle={this.props.onLayerVisibilityToggle.bind(
}) this
)}
{...additionalProps}
/>
);
listItems.push(listItem);
idx += 1;
});
});
return <section return (
className="maputnik-layer-list" <section
role="complementary" className="maputnik-layer-list"
aria-label="Layers list" role="complementary"
ref={this.scrollContainerRef} aria-label="Layers list"
> ref={this.scrollContainerRef}
<ModalAdd >
<ModalAdd
key={this.state.keys.add} key={this.state.keys.add}
layers={this.props.layers} layers={this.props.layers}
sources={this.props.sources} sources={this.props.sources}
isOpen={this.state.isOpen.add} isOpen={this.state.isOpen.add}
onOpenToggle={this.toggleModal.bind(this, 'add')} onOpenToggle={this.toggleModal.bind(this, "add")}
onLayersChange={this.props.onLayersChange} onLayersChange={this.props.onLayersChange}
/> />
<header className="maputnik-layer-list-header"> <header className="maputnik-layer-list-header">
<span className="maputnik-layer-list-header-title">Layers</span> <span className="maputnik-layer-list-header-title">Layers</span>
<span className="maputnik-space" /> <span className="maputnik-space" />
<div className="maputnik-default-property"> <div className="maputnik-default-property">
<div className="maputnik-multibutton"> <div className="maputnik-multibutton">
<button <button
id="skip-target-layer-list" id="skip-target-layer-list"
onClick={this.toggleLayers} data-wd-key="skip-target-layer-list"
className="maputnik-button"> onClick={this.toggleLayers}
{this.state.areAllGroupsExpanded === true ? "Collapse" : "Expand"} className="maputnik-button"
</button> >
{this.state.areAllGroupsExpanded === true
? "Collapse"
: "Expand"}
</button>
</div>
</div> </div>
</div> <div className="maputnik-default-property">
<div className="maputnik-default-property"> <div className="maputnik-multibutton">
<div className="maputnik-multibutton"> <button
<button onClick={this.toggleModal.bind(this, "add")}
onClick={this.toggleModal.bind(this, 'add')} data-wd-key="layer-list:add-layer"
data-wd-key="layer-list:add-layer" className="maputnik-button maputnik-button-selected"
className="maputnik-button maputnik-button-selected"> >
Add Layer Add Layer
</button> </button>
</div>
</div> </div>
</header>
<div role="navigation" aria-label="Layers list">
<ul className="maputnik-layer-list-container">{listItems}</ul>
</div> </div>
</header> </section>
<div );
role="navigation"
aria-label="Layers list"
>
<ul className="maputnik-layer-list-container">
{listItems}
</ul>
</div>
</section>
} }
} }
const LayerListContainerSortable = SortableContainer((props) => <LayerListContainer {...props} />) const LayerListContainerSortable = SortableContainer((props) => (
<LayerListContainer {...props} />
));
export default class LayerList extends React.Component { export default class LayerList extends React.Component {
static propTypes = {...layerListPropTypes} static propTypes = { ...layerListPropTypes };
render() { render() {
return <LayerListContainerSortable return (
{...this.props} <LayerListContainerSortable
helperClass='sortableHelper' {...this.props}
onSortEnd={this.props.onMoveLayer.bind(this)} helperClass="sortableHelper"
useDragHandle={true} onSortEnd={this.props.onMoveLayer.bind(this)}
shouldCancelStart={() => false} useDragHandle={true}
/> shouldCancelStart={() => false}
/>
);
} }
} }
+123 -100
View File
@@ -1,19 +1,17 @@
import React from 'react' import Color from "color";
import PropTypes from 'prop-types' import MapboxInspect from "mapbox-gl-inspect";
import ReactDOM from 'react-dom' import colors from "mapbox-gl-inspect/lib/colors";
import MapLibreGl from 'maplibre-gl' import MapLibreGl from "maplibre-gl";
import MapboxInspect from 'mapbox-gl-inspect' import "maplibre-gl/dist/maplibre-gl.css";
import MapMaplibreGlLayerPopup from './MapMaplibreGlLayerPopup' import PropTypes from "prop-types";
import MapMaplibreGlFeaturePropertyPopup from './MapMaplibreGlFeaturePropertyPopup' import React from "react";
import tokens from '../config/tokens.json' import ReactDOM from "react-dom";
import colors from 'mapbox-gl-inspect/lib/colors' import { colorHighlightedLayer } from "../libs/highlight";
import Color from 'color' import "../libs/maplibre-rtl";
import ZoomControl from '../libs/zoomcontrol' import ZoomControl from "../libs/zoomcontrol";
import { colorHighlightedLayer } from '../libs/highlight' import "../maplibregl.css";
import 'maplibre-gl/dist/maplibre-gl.css' import MapMaplibreGlFeaturePropertyPopup from "./MapMaplibreGlFeaturePropertyPopup";
import '../maplibregl.css' import MapMaplibreGlLayerPopup from "./MapMaplibreGlLayerPopup";
import '../libs/maplibre-rtl'
const IS_SUPPORTED = MapLibreGl.supported(); const IS_SUPPORTED = MapLibreGl.supported();
@@ -24,32 +22,32 @@ function renderPopup(popup, mountNode) {
function buildInspectStyle(originalMapStyle, coloredLayers, highlightedLayer) { function buildInspectStyle(originalMapStyle, coloredLayers, highlightedLayer) {
const backgroundLayer = { const backgroundLayer = {
"id": "background", id: "background",
"type": "background", type: "background",
"paint": { paint: {
"background-color": '#1c1f24', "background-color": "#1c1f24",
} },
};
const layer = colorHighlightedLayer(highlightedLayer);
if (layer) {
coloredLayers.push(layer);
} }
const layer = colorHighlightedLayer(highlightedLayer) const sources = {};
if(layer) { Object.keys(originalMapStyle.sources).forEach((sourceId) => {
coloredLayers.push(layer) const source = originalMapStyle.sources[sourceId];
} if (source.type !== "raster" && source.type !== "raster-dem") {
sources[sourceId] = source;
const sources = {}
Object.keys(originalMapStyle.sources).forEach(sourceId => {
const source = originalMapStyle.sources[sourceId]
if(source.type !== 'raster' && source.type !== 'raster-dem') {
sources[sourceId] = source
} }
}) });
const inspectStyle = { const inspectStyle = {
...originalMapStyle, ...originalMapStyle,
sources: sources, sources: sources,
layers: [backgroundLayer].concat(coloredLayers) layers: [backgroundLayer].concat(coloredLayers),
} };
return inspectStyle return inspectStyle;
} }
export default class MapMaplibreGl extends React.Component { export default class MapMaplibreGl extends React.Component {
@@ -62,7 +60,7 @@ export default class MapMaplibreGl extends React.Component {
options: PropTypes.object, options: PropTypes.object,
replaceAccessTokens: PropTypes.func.isRequired, replaceAccessTokens: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
} };
static defaultProps = { static defaultProps = {
onMapLoaded: () => {}, onMapLoaded: () => {},
@@ -70,51 +68,55 @@ export default class MapMaplibreGl extends React.Component {
onLayerSelect: () => {}, onLayerSelect: () => {},
onChange: () => {}, onChange: () => {},
options: {}, options: {},
} };
constructor(props) { constructor(props) {
super(props) super(props);
this.state = { this.state = {
map: null, map: null,
inspect: null, inspect: null,
} };
} }
updateMapFromProps(props) { updateMapFromProps(props) {
if(!IS_SUPPORTED) return; if (!IS_SUPPORTED) return;
if(!this.state.map) return if (!this.state.map) return;
//Maplibre GL now does diffing natively so we don't need to calculate //Maplibre GL now does diffing natively so we don't need to calculate
//the necessary operations ourselves! //the necessary operations ourselves!
this.state.map.setStyle( this.state.map.setStyle(this.props.replaceAccessTokens(props.mapStyle), {
this.props.replaceAccessTokens(props.mapStyle), diff: true,
{diff: true} });
)
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
let should = false; let should = false;
try { try {
should = JSON.stringify(this.props) !== JSON.stringify(nextProps) || JSON.stringify(this.state) !== JSON.stringify(nextState); should =
} catch(e) { JSON.stringify(this.props) !== JSON.stringify(nextProps) ||
JSON.stringify(this.state) !== JSON.stringify(nextState);
} catch (e) {
// no biggie, carry on // no biggie, carry on
} }
return should; return should;
} }
componentDidUpdate(prevProps, prevState, snapshot) { componentDidUpdate(prevProps, prevState, snapshot) {
if(!IS_SUPPORTED) return; if (!IS_SUPPORTED) return;
const map = this.state.map; const map = this.state.map;
this.updateMapFromProps(this.props); this.updateMapFromProps(this.props);
if(this.state.inspect && this.props.inspectModeEnabled !== this.state.inspect._showInspectMap) { if (
this.state.inspect &&
this.props.inspectModeEnabled !== this.state.inspect._showInspectMap
) {
// HACK: Fix for <https://github.com/maputnik/editor/issues/576>, while we wait for a proper fix. // HACK: Fix for <https://github.com/maputnik/editor/issues/576>, while we wait for a proper fix.
// eslint-disable-next-line // eslint-disable-next-line
this.state.inspect._popupBlocked = false; this.state.inspect._popupBlocked = false;
this.state.inspect.toggleInspector() this.state.inspect.toggleInspector();
} }
if (map) { if (map) {
if (this.props.inspectModeEnabled) { if (this.props.inspectModeEnabled) {
@@ -123,7 +125,7 @@ export default class MapMaplibreGl extends React.Component {
// mapbox-gl-inspect. // mapbox-gl-inspect.
try { try {
this.state.inspect.render(); this.state.inspect.render();
} catch(err) { } catch (err) {
console.error("FIXME: Caught error", err); console.error("FIXME: Caught error", err);
} }
} }
@@ -135,40 +137,40 @@ export default class MapMaplibreGl extends React.Component {
} }
componentDidMount() { componentDidMount() {
if(!IS_SUPPORTED) return; if (!IS_SUPPORTED) return;
const mapOpts = { const mapOpts = {
...this.props.options, ...this.props.options,
container: this.container, container: this.container,
style: this.props.mapStyle, style: this.props.mapStyle,
hash: true, hash: true,
maxZoom: 24 maxZoom: 24,
} };
const map = new MapLibreGl.Map(mapOpts); const map = new MapLibreGl.Map(mapOpts);
const mapViewChange = () => { const mapViewChange = () => {
const center = map.getCenter(); const center = map.getCenter();
const zoom = map.getZoom(); const zoom = map.getZoom();
this.props.onChange({center, zoom}); this.props.onChange({ center, zoom });
} };
mapViewChange(); mapViewChange();
map.showTileBoundaries = mapOpts.showTileBoundaries; map.showTileBoundaries = mapOpts.showTileBoundaries;
map.showCollisionBoxes = mapOpts.showCollisionBoxes; map.showCollisionBoxes = mapOpts.showCollisionBoxes;
map.showOverdrawInspector = mapOpts.showOverdrawInspector; map.showOverdrawInspector = mapOpts.showOverdrawInspector;
const zoomControl = new ZoomControl; const zoomControl = new ZoomControl();
map.addControl(zoomControl, 'top-right'); map.addControl(zoomControl, "top-right");
const nav = new MapLibreGl.NavigationControl({visualizePitch:true}); const nav = new MapLibreGl.NavigationControl({ visualizePitch: true });
map.addControl(nav, 'top-right'); map.addControl(nav, "top-right");
const tmpNode = document.createElement('div'); const tmpNode = document.createElement("div");
const inspect = new MapboxInspect({ const inspect = new MapboxInspect({
popup: new MapLibreGl.Popup({ popup: new MapLibreGl.Popup({
closeOnClick: false closeOnClick: false,
}), }),
showMapPopup: true, showMapPopup: true,
showMapPopupOnHover: false, showMapPopupOnHover: false,
@@ -176,41 +178,58 @@ export default class MapMaplibreGl extends React.Component {
showInspectButton: false, showInspectButton: false,
blockHoverPopupOnClick: true, blockHoverPopupOnClick: true,
assignLayerColor: (layerId, alpha) => { assignLayerColor: (layerId, alpha) => {
return Color(colors.brightColor(layerId, alpha)).desaturate(0.5).string() return Color(colors.brightColor(layerId, alpha))
.desaturate(0.5)
.string();
}, },
buildInspectStyle: (originalMapStyle, coloredLayers) => buildInspectStyle(originalMapStyle, coloredLayers, this.props.highlightedLayer), buildInspectStyle: (originalMapStyle, coloredLayers) =>
renderPopup: features => { buildInspectStyle(
if(this.props.inspectModeEnabled) { originalMapStyle,
return renderPopup(<MapMaplibreGlFeaturePropertyPopup features={features} />, tmpNode); coloredLayers,
this.props.highlightedLayer
),
renderPopup: (features) => {
if (this.props.inspectModeEnabled) {
return renderPopup(
<MapMaplibreGlFeaturePropertyPopup features={features} />,
tmpNode
);
} else { } else {
return renderPopup(<MapMaplibreGlLayerPopup features={features} onLayerSelect={this.onLayerSelectById} zoom={this.state.zoom} />, tmpNode); return renderPopup(
<MapMaplibreGlLayerPopup
features={features}
onLayerSelect={this.onLayerSelectById}
zoom={this.state.zoom}
/>,
tmpNode
);
} }
} },
}) });
map.addControl(inspect) map.addControl(inspect);
map.on("style.load", () => { map.on("style.load", () => {
this.setState({ this.setState({
map, map,
inspect, inspect,
zoom: map.getZoom() zoom: map.getZoom(),
}); });
}) });
map.on("data", e => { map.on("data", (e) => {
if(e.dataType !== 'tile') return if (e.dataType !== "tile") return;
this.props.onDataChange({ this.props.onDataChange({
map: this.state.map map: this.state.map,
}) });
}) });
map.on("error", e => { map.on("error", (e) => {
console.log("ERROR", e); console.log("ERROR", e);
}) });
map.on("zoom", e => { map.on("zoom", (e) => {
this.setState({ this.setState({
zoom: map.getZoom() zoom: map.getZoom(),
}); });
}); });
@@ -219,28 +238,32 @@ export default class MapMaplibreGl extends React.Component {
} }
onLayerSelectById = (id) => { onLayerSelectById = (id) => {
const index = this.props.mapStyle.layers.findIndex(layer => layer.id === id); const index = this.props.mapStyle.layers.findIndex(
(layer) => layer.id === id
);
this.props.onLayerSelect(index); this.props.onLayerSelect(index);
} };
render() { render() {
if(IS_SUPPORTED) { if (IS_SUPPORTED) {
return <div return (
className="maputnik-map__map" <div
role="region" className="maputnik-map__map"
aria-label="Map view" role="region"
ref={x => this.container = x} aria-label="Map view"
></div> ref={(x) => (this.container = x)}
} data-wd-key="maplibre:map"
else { ></div>
return <div );
className="maputnik-map maputnik-map--error" } else {
> return (
<div className="maputnik-map__error-message"> <div className="maputnik-map maputnik-map--error">
Error: Cannot load MaplibreGL, WebGL is either unsupported or disabled <div className="maputnik-map__error-message">
Error: Cannot load MaplibreGL, WebGL is either unsupported or
disabled
</div>
</div> </div>
</div> );
} }
} }
} }
+12 -10
View File
@@ -1,26 +1,28 @@
export default class ZoomControl { export default class ZoomControl {
onAdd(map) { onAdd(map) {
this._map = map; this._map = map;
this._container = document.createElement('div'); this._container = document.createElement("div");
this._container.className = 'maplibregl-ctrl maplibregl-ctrl-group maplibregl-ctrl-zoom'; this._container.className =
"maplibregl-ctrl maplibregl-ctrl-group maplibregl-ctrl-zoom";
this._container.setAttribute("data-wd-key", "maplibre:ctrl-zoom");
this._container.innerHTML = ` this._container.innerHTML = `
Zoom: <span></span> Zoom: <span></span>
`; `;
this._textEl = this._container.querySelector("span"); this._textEl = this._container.querySelector("span");
this.addEventListeners(); this.addEventListeners();
return this._container; return this._container;
} }
updateZoomLevel() { updateZoomLevel() {
this._textEl.innerHTML = this._map.getZoom().toFixed(2); this._textEl.innerHTML = this._map.getZoom().toFixed(2);
} }
addEventListeners (){ addEventListeners() {
this._map.on('render', this.updateZoomLevel.bind(this) ); this._map.on("render", this.updateZoomLevel.bind(this));
this._map.on('zoomIn', this.updateZoomLevel.bind(this) ); this._map.on("zoomIn", this.updateZoomLevel.bind(this));
this._map.on('zoomOut', this.updateZoomLevel.bind(this) ); this._map.on("zoomOut", this.updateZoomLevel.bind(this));
} }
onRemove() { onRemove() {