Compare commits

...

15 Commits

Author SHA1 Message Date
HarelM 854e3eeb45 Rebuild lock file 2025-09-17 00:35:29 +03:00
HarelM 7c112d7c10 Added missing dependencies? 2025-09-17 00:32:24 +03:00
HarelM c870fefb14 Fix build 2025-09-17 00:22:32 +03:00
pre-commit-ci[bot] df64f5c1aa [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-09-16 21:17:25 +00:00
HarelM d4d161ccbe Updated changelog 2025-09-17 00:17:12 +03:00
pre-commit-ci[bot] 9d35c3d650 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-09-16 21:15:47 +00:00
HarelM 176e1e4ae8 Final fixes 2025-09-17 00:12:14 +03:00
HarelM b70b6f16b7 Merge branch 'main' into codemirror-upgrade 2025-09-16 21:07:10 +03:00
Harel M 3c3fcadbb6 Added back errors panel (#1384)
## Launch Checklist

This PR adds back the error panel which was under the map for some
reason.
It also highlights problematic layers in the layers list (which already
worked).
It also highlights the field that has an error related to it.
It fixes the error types throughout the code.

Before:
<img width="1141" height="665" alt="image"
src="https://github.com/user-attachments/assets/c0593d6c-8f14-41b3-8a51-bc359446656d"
/>


After:
<img width="1141" height="665" alt="image"
src="https://github.com/user-attachments/assets/1ffeebb7-31ea-4ed5-97f4-fc5f907a6aea"
/>


 - [x] Briefly describe the changes in this PR.
- [x] Include before/after visuals or gifs if this PR includes visual
changes.
 - [x] Write tests for all new functionality.
 - [x] Add an entry to `CHANGELOG.md` under the `## main` section.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-09-16 15:42:07 +02:00
dependabot[bot] 174548944f chore(deps-dev): Bump typescript-eslint from 8.43.0 to 8.44.0 (#1385)
Bumps
[typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint)
from 8.43.0 to 8.44.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/releases">typescript-eslint's
releases</a>.</em></p>
<blockquote>
<h2>v8.44.0</h2>
<h2>8.44.0 (2025-09-15)</h2>
<h3>🚀 Features</h3>
<ul>
<li><strong>eslint-plugin:</strong> [await-thenable] report invalid
(non-promise) values passed to promise aggregator methods (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/11267">#11267</a>)</li>
</ul>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>deps:</strong> update dependency
<code>@​eslint-community/eslint-utils</code> to v4.8.0 (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/11589">#11589</a>)</li>
<li><strong>eslint-plugin:</strong> [no-unnecessary-type-conversion]
ignore enum members (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/11490">#11490</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Moses Odutusin <a
href="https://github.com/thebolarin"><code>@​thebolarin</code></a></li>
<li>Ronen Amiel</li>
</ul>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md">typescript-eslint's
changelog</a>.</em></p>
<blockquote>
<h2>8.44.0 (2025-09-15)</h2>
<p>This was a version bump only for typescript-eslint to align it with
other projects, there were no code changes.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/typescript-eslint/typescript-eslint/commit/77056f77e6fdca54b66ec692e5cefbd9f7a626dd"><code>77056f7</code></a>
chore(release): publish 8.44.0</li>
<li>See full diff in <a
href="https://github.com/typescript-eslint/typescript-eslint/commits/v8.44.0/packages/typescript-eslint">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=typescript-eslint&package-manager=npm_and_yarn&previous-version=8.43.0&new-version=8.44.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-16 13:16:45 +00:00
dependabot[bot] c57d7d3a89 chore(deps-dev): Bump @types/react from 19.1.12 to 19.1.13 (#1383)
Bumps
[@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react)
from 19.1.12 to 19.1.13.
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@types/react&package-manager=npm_and_yarn&previous-version=19.1.12&new-version=19.1.13)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Harel M <harel.mazor@gmail.com>
2025-09-16 12:58:55 +00:00
dependabot[bot] 58ff17998d chore(deps): Bump color from 5.0.0 to 5.0.2 (#1382)
Bumps [color](https://github.com/Qix-/color) from 5.0.0 to 5.0.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/Qix-/color/releases">color's
releases</a>.</em></p>
<blockquote>
<h2>5.0.2</h2>
<p>Functionally identical release to <code>5.0.0</code>.</p>
<p>Version <code>5.0.1</code> is <strong>compromised</strong>. Please
see <a
href="https://redirect.github.com/debug-js/debug/issues/1005">debug-js/debug#1005</a>.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/Qix-/color/commit/02a615f268c1c98bea928986b8fba8fdf18c0cc9"><code>02a615f</code></a>
5.0.2</li>
<li>See full diff in <a
href="https://github.com/Qix-/color/compare/5.0.0...5.0.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=color&package-manager=npm_and_yarn&previous-version=5.0.0&new-version=5.0.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-15 13:37:11 +00:00
Harel M b42afd0027 Fix firefox ability to load file, add firefox tests back (#1380)
## Launch Checklist

This PR fixes the problem with firefox not able to load a file when
clicking the open file button.
It does so by removing a very old dependency: `react-file-reader-input`
It uses the "regular" `<label><a/><input type="file" /><label>`
approach.
It adds tests for both firefox and chrome although I'm not sure the
firefox tests will stay...

 - [x] Briefly describe the changes in this PR.
 - [x] Write tests for all new functionality.
 - [x] Add an entry to `CHANGELOG.md` under the `## main` section.
2025-09-15 05:15:00 +00:00
Harel M 5312d61598 Add globe support in Maputnik UI (#1379)
## Launch Checklist

Add a small drop down to select mercator or globe.
This isn't a fully covered field as one can set an expression there, but
I believe this is good enough for most cases.

Before:
<img width="645" height="254" alt="image"
src="https://github.com/user-attachments/assets/19a7ec50-a0bb-4ea3-b9fc-3abc5572c47e"
/>
After:
<img width="770" height="462" alt="image"
src="https://github.com/user-attachments/assets/f4774020-1cc8-45fe-88f9-f77ad7c53140"
/>


 - [x] Briefly describe the changes in this PR.
- [x] Include before/after visuals or gifs if this PR includes visual
changes.
 - [x] Write tests for all new functionality.
 - [x] Add an entry to `CHANGELOG.md` under the `## main` section.

---------

Co-authored-by: Birk Skyum <birk.skyum@pm.me>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-09-14 11:48:29 +02:00
Harel M 56cdfd23df Add react-markdown to better show the docs (#1378)
## Launch Checklist

This improves how the docs are being presented, since we added some
markdown in the docs it is better to have full support for this here as
well

Before:
<img width="645" height="254" alt="image"
src="https://github.com/user-attachments/assets/ce099ab9-eabd-4721-9550-5ea251439d93"
/>

After:
<img width="645" height="254" alt="image"
src="https://github.com/user-attachments/assets/884d1d69-4238-412e-b620-f9c0640723ca"
/>

The table below that is also stretched is fixes in the following PR:

https://github.com/maplibre/maputnik/pull/1377/files#diff-8d22a87c6893aa31e22f1db0804e1fe93534f38d9fb4433e71cd3878f8daa954

 - [x] Briefly describe the changes in this PR.
- [x] Include before/after visuals or gifs if this PR includes visual
changes.
 - [x] Write tests for all new functionality.
 - [x] Add an entry to `CHANGELOG.md` under the `## main` section.
2025-09-14 12:12:21 +03:00
32 changed files with 3567 additions and 1933 deletions
+6
View File
@@ -5,7 +5,11 @@
- Add support for hillshade's color arrays and relief-color elevation expression
- Change layers icons to make them a bit more distinct
- Remove `@mdi` packages in favor of `react-icons`
- Add ability to control the projection of the map - either globe or mercator
- Add markdown support for doc related to the style-spec fields
- Added global state modal to allow editing the global state
- Added color highlight for problematic properties
- Upgraded codemirror from version 5 to version 6
- _...Add new stuff here..._
### 🐞 Bug fixes
@@ -14,6 +18,8 @@
- Fix modal close button possition
- Fixed an issue with the generation of tranlations
- Fix missing spec info when clicking next to a property
- Fix Firefox open file that stopped working due to react upgrade
- Fix issue with missing bottom error panel
- _...Add new stuff here..._
## 3.0.0
+50 -21
View File
@@ -491,6 +491,20 @@ describe("layers", () => {
when.click("field-doc-button-Offset", 0);
then(get.elementByTestId("spec-field-doc")).shouldContainText("Offset distance");
});
it("should hide spec info when clicking a second time", () => {
when.modal.fillLayers({
type: "symbol",
layer: "example",
});
when.hover("spec-field-container:text-rotate");
then(get.elementByTestId("field-doc-button-Rotate")).shouldBeVisible();
when.click("field-doc-button-Rotate", 0);
when.wait(200);
when.click("field-doc-button-Rotate", 0);
then(get.elementByTestId("spec-field-doc")).shouldNotBeVisible();
});
});
describe("raster", () => {
@@ -684,33 +698,48 @@ describe("layers", () => {
});
});
describe("layereditor jsonlint should error", ()=>{
it("add", () => {
const id = when.modal.fillLayers({
type: "circle",
layer: "example",
describe("layers editor", () => {
describe("property fields", () => {
it("should show error", () => {
when.modal.fillLayers({
type: "circle",
layer: "invalid",
});
then(get.element(".maputnik-input-block--error .maputnik-input-block-label")).shouldHaveCss("color", "rgb(207, 74, 74)");
});
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "circle",
source: "example",
},
],
describe("jsonlint should error", ()=>{
it("add", () => {
const id = when.modal.fillLayers({
type: "circle",
layer: "example",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "circle",
source: "example",
},
],
});
const sourceText = get.elementByText('"source"');
sourceText.click();
sourceText.type("\"");
const error = get.element(".CodeMirror-lint-marker-error");
error.should("exist");
});
const sourceText = get.elementByText('"source"');
sourceText.click();
sourceText.type("\"");
const error = get.element(".CodeMirror-lint-marker-error");
error.should("exist");
});
});
describe("drag and drop", () => {
it("move layer should update local storage", () => {
when.modal.open();
+16
View File
@@ -26,6 +26,22 @@ export default class MaputnikCypressHelper {
this.helper.when.wait(200);
this.helper.get.elementByTestId(element).realMouseUp();
},
openFileByFixture: (fixture: string, buttonTestId: string, inputTestId: string) => {
cy.window().then((win) => {
const file = {
text: cy.stub().resolves(cy.fixture(fixture).then(JSON.stringify)),
};
const fileHandle = {
getFile: cy.stub().resolves(file),
};
if (!win.showOpenFilePicker) {
this.helper.get.elementByTestId(inputTestId).selectFile("cypress/fixtures/" + fixture, { force: true });
} else {
cy.stub(win, "showOpenFilePicker").resolves([fileHandle]);
this.helper.get.elementByTestId(buttonTestId).click();
}
});
},
...this.helper.when,
};
+3 -3
View File
@@ -109,9 +109,9 @@ export class MaputnikDriver {
this.helper.when.waitForResponse("example-style.json");
},
chooseExampleFile: () => {
this.helper.get
.bySelector("type", "file")
.selectFile("cypress/fixtures/example-style.json", { force: true });
this.helper.given.fixture("example-style.json", "example-style.json");
this.helper.when.openFileByFixture("example-style.json", "modal:open.file.button", "modal:open.file.input");
this.helper.when.wait(200);
},
setStyle: (
styleProperties: "geojson" | "raster" | "both" | "layer" | "rectangles" | "",
+45 -3
View File
@@ -18,10 +18,9 @@ describe("modals", () => {
then(get.elementByTestId("modal:open")).shouldNotExist();
});
it.skip("upload", () => {
// HM: I was not able to make the following choose file actually to select a file and close the modal...
it("upload", () => {
when.chooseExampleFile();
then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
then(get.fixture("example-style.json")).shouldEqualToStoredStyle();
});
describe("when click open url", () => {
@@ -236,6 +235,29 @@ describe("modals", () => {
).shouldInclude({ "maputnik:locationiq_access_token": apiKey });
});
it("style projection mercator", () => {
when.select("modal:settings.projection", "mercator");
then(
get.styleFromLocalStorage().then((style) => style.projection)
).shouldInclude({ type: "mercator" });
});
it("style projection globe", () => {
when.select("modal:settings.projection", "globe");
then(
get.styleFromLocalStorage().then((style) => style.projection)
).shouldInclude({ type: "globe" });
});
it("style projection vertical-perspective", () => {
when.select("modal:settings.projection", "vertical-perspective");
then(
get.styleFromLocalStorage().then((style) => style.projection)
).shouldInclude({ type: "vertical-perspective" });
});
it("style renderer", () => {
cy.on("uncaught:exception", () => false); // this is due to the fact that this is an invalid style for openlayers
when.select("modal:settings.maputnik:renderer", "ol");
@@ -311,6 +333,7 @@ describe("modals", () => {
it("add variable", () => {
when.click("global-state-add-variable");
when.wait(100);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
state: { key1: { default: "value" } },
});
@@ -321,6 +344,7 @@ describe("modals", () => {
when.click("global-state-add-variable");
when.click("global-state-add-variable");
when.click("global-state-add-variable");
when.wait(100);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
state: { key1: { default: "value" }, key2: { default: "value" }, key3: { default: "value" } },
});
@@ -331,6 +355,7 @@ describe("modals", () => {
when.click("global-state-add-variable");
when.click("global-state-add-variable");
when.click("global-state-remove-variable", 0);
when.wait(100);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
state: { key2: { default: "value" }, key3: { default: "value" } },
});
@@ -340,6 +365,7 @@ describe("modals", () => {
when.click("global-state-add-variable");
when.setValue("global-state-variable-key:0", "mykey");
when.typeKeys("{enter}");
when.wait(100);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
state: { mykey: { default: "value" } },
});
@@ -349,12 +375,28 @@ describe("modals", () => {
when.click("global-state-add-variable");
when.setValue("global-state-variable-value:0", "myvalue");
when.typeKeys("{enter}");
when.wait(100);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
state: { key1: { default: "myvalue" } },
});
});
});
describe("error panel", () => {
it("not visible when no errors", () => {
then(get.element("maputnik-message-panel-error")).shouldNotExist();
});
it("visible on style error", () => {
when.modal.open();
when.modal.fillLayers({
type: "circle",
layer: "invalid",
});
then(get.element(".maputnik-message-panel-error")).shouldBeVisible();
});
});
describe("Handle localStorage QuotaExceededError", () => {
it("handles quota exceeded error when opening style from URL", () => {
// Clear localStorage to start fresh
+3243 -1643
View File
File diff suppressed because it is too large Load Diff
+7 -5
View File
@@ -25,7 +25,10 @@
"homepage": "https://github.com/maplibre/maputnik#readme",
"dependencies": {
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lint": "^6.8.5",
"@codemirror/state": "^6.5.2",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.38.2",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
@@ -37,7 +40,7 @@
"buffer": "^6.0.3",
"classnames": "^2.5.1",
"codemirror": "^6.0.2",
"color": "^5.0.0",
"color": "^5.0.2",
"detect-browser": "^5.3.0",
"downshift": "^9.0.10",
"events": "^3.3.0",
@@ -67,9 +70,9 @@
"react-collapse": "^5.1.1",
"react-color": "^2.19.3",
"react-dom": "^19.1.1",
"react-file-reader-input": "^2.0.0",
"react-i18next": "^15.7.3",
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"reconnecting-websocket": "^4.4.0",
"slugify": "^1.6.6",
"string-hash": "^1.1.3",
@@ -112,13 +115,12 @@
"@types/lodash.isequal": "^4.5.8",
"@types/lodash.throttle": "^4.1.9",
"@types/randomcolor": "^0.5.9",
"@types/react": "^19.1.12",
"@types/react": "^19.1.13",
"@types/react-aria-menubutton": "^6.2.14",
"@types/react-aria-modal": "^5.0.0",
"@types/react-collapse": "^5.0.4",
"@types/react-color": "^3.0.13",
"@types/react-dom": "^19.1.9",
"@types/react-file-reader-input": "^2.0.4",
"@types/string-hash": "^1.1.3",
"@types/uuid": "^10.0.0",
"@types/wicg-file-system-access": "^2023.10.6",
@@ -140,7 +142,7 @@
"stylelint-config-recommended-scss": "^16.0.1",
"stylelint-scss": "^6.12.1",
"typescript": "^5.9.2",
"typescript-eslint": "^8.43.0",
"typescript-eslint": "^8.44.0",
"uuid": "^13.0.0",
"vite": "^7.1.5",
"vite-plugin-istanbul": "^7.1.0"
+3 -15
View File
@@ -36,7 +36,7 @@ import LayerWatcher from "../libs/layerwatcher";
import tokens from "../config/tokens.json";
import isEqual from "lodash.isequal";
import { type MapOptions } from "maplibre-gl";
import { type OnStyleChangedOpts, type StyleSpecificationWithId } from "../libs/definitions";
import { type MappedError, type OnStyleChangedOpts, type StyleSpecificationWithId } from "../libs/definitions";
// Buffer must be defined globally for @maplibre/maplibre-gl-style-spec validate() function to succeed.
window.Buffer = buffer.Buffer;
@@ -82,20 +82,8 @@ function updateRootSpec(spec: any, fieldName: string, newValues: any) {
};
}
type MappedErrors = {
message: string
parsed?: {
type: string
data: {
index: number
key: string
message: string
}
}
};
type AppState = {
errors: MappedErrors[],
errors: MappedError[],
infos: string[],
mapStyle: StyleSpecificationWithId,
dirtyMapStyle?: StyleSpecification,
@@ -383,7 +371,7 @@ export default class App extends React.Component<any, AppState> {
});
}
const mappedErrors = layerErrors.concat(errors).map(error => {
const mappedErrors: MappedError[] = layerErrors.concat(errors).map(error => {
// Special case: Duplicate layer id
const dupMatch = error.message.match(/layers\[(\d+)\]: (duplicate layer id "?(.*)"?, previously used)/);
if (dupMatch) {
+3 -2
View File
@@ -2,9 +2,10 @@ import React from "react";
import {formatLayerId} from "../libs/format";
import {type LayerSpecification, type StyleSpecification} from "maplibre-gl";
import { Trans, type WithTranslation, withTranslation } from "react-i18next";
import { type MappedError } from "../libs/definitions";
type AppMessagePanelInternalProps = {
errors?: unknown[]
errors?: MappedError[]
infos?: string[]
mapStyle?: StyleSpecification
onLayerSelect?(index: number): void;
@@ -19,7 +20,7 @@ class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalPro
render() {
const {t, selectedLayerIndex} = this.props;
const errors = this.props.errors?.map((error: any, idx) => {
const errors = this.props.errors?.map((error, idx) => {
let content;
if (error.parsed && error.parsed.type === "layer") {
const {parsed} = error;
+6 -4
View File
@@ -1,16 +1,17 @@
import React, {type PropsWithChildren, type SyntheticEvent} from "react";
import React, {type CSSProperties, type PropsWithChildren, type SyntheticEvent} from "react";
import classnames from "classnames";
import FieldDocLabel from "./FieldDocLabel";
import Doc from "./Doc";
type BlockProps = PropsWithChildren & {
export type BlockProps = PropsWithChildren & {
"data-wd-key"?: string
label?: string
action?: React.ReactElement
style?: object
style?: CSSProperties
onChange?(...args: unknown[]): unknown
fieldSpec?: object
wideMode?: boolean
error?: {message: string}
};
type BlockState = {
@@ -65,7 +66,8 @@ export default class Block extends React.Component<BlockProps, BlockState> {
className={classnames({
"maputnik-input-block": true,
"maputnik-input-block--wide": this.props.wideMode,
"maputnik-action-block": this.props.action
"maputnik-action-block": this.props.action,
"maputnik-input-block--error": this.props.error
})}
onClick={this.onLabelClick}
>
+6 -1
View File
@@ -1,4 +1,5 @@
import React from "react";
import Markdown from "react-markdown";
const headers = {
js: "JS",
@@ -48,7 +49,11 @@ export default class Doc extends React.Component<DocProps> {
<>
{doc &&
<div className="SpecDoc">
<div className="SpecDoc__doc" data-wd-key='spec-field-doc'>{doc}</div>
<div className="SpecDoc__doc" data-wd-key='spec-field-doc'>
<Markdown components={{
a: ({node: _node, href, children, ...props}) => <a href={href} target="_blank" {...props}>{children}</a>,
}}>{doc}</Markdown>
</div>
{renderValues &&
<ul className="SpecDoc__values">
{Object.entries(values).map(([key, value]) => {
+2 -1
View File
@@ -6,6 +6,7 @@ import ZoomProperty from "./_ZoomProperty";
import ExpressionProperty from "./_ExpressionProperty";
import {function as styleFunction} from "@maplibre/maplibre-gl-style-spec";
import {findDefaultFromSpec} from "../libs/spec-helper";
import { type MappedLayerErrors } from "../libs/definitions";
function isLiteralExpression(value: any) {
@@ -119,7 +120,7 @@ type FieldFunctionProps = {
fieldName: string
fieldType: string
fieldSpec: any
errors?: {[key: string]: {message: string}}
errors?: MappedLayerErrors
value?: any
};
+4 -6
View File
@@ -1,6 +1,6 @@
import Block from "./Block";
import Block, { type BlockProps } from "./Block";
import InputSpec, { type FieldSpecType, type InputSpecProps } from "./InputSpec";
import Fieldset from "./Fieldset";
import Fieldset, { type FieldsetProps } from "./Fieldset";
function getElementFromType(fieldSpec: { type?: FieldSpecType, values?: unknown[] }): typeof Fieldset | typeof Block {
switch(fieldSpec.type) {
@@ -34,15 +34,13 @@ function getElementFromType(fieldSpec: { type?: FieldSpecType, values?: unknown[
}
}
export type FieldSpecProps = InputSpecProps & {
name?: string
};
export type FieldSpecProps = InputSpecProps & BlockProps & FieldsetProps;
const FieldSpec: React.FC<FieldSpecProps> = (props) => {
const TypeBlock = getElementFromType(props.fieldSpec!);
return (
<TypeBlock label={props.label} action={props.action} fieldSpec={props.fieldSpec}>
<TypeBlock label={props.label} action={props.action} fieldSpec={props.fieldSpec} error={props.error}>
<InputSpec {...props} />
</TypeBlock>
);
+7 -2
View File
@@ -1,12 +1,14 @@
import React, { type PropsWithChildren, type ReactElement } from "react";
import classnames from "classnames";
import FieldDocLabel from "./FieldDocLabel";
import Doc from "./Doc";
import generateUniqueId from "../libs/document-uid";
type FieldsetProps = PropsWithChildren & {
export type FieldsetProps = PropsWithChildren & {
label?: string,
fieldSpec?: { doc?: string },
action?: ReactElement,
error?: {message: string}
};
@@ -30,7 +32,10 @@ const Fieldset: React.FC<FieldsetProps> = (props) => {
</div>
)}
{!props.fieldSpec && (
<div className="maputnik-input-block-label">
<div className={classnames({
"maputnik-input-block-label": true,
"maputnik-input-block--error": props.error
})}>
{props.label}
</div>
)}
+6 -4
View File
@@ -3,7 +3,8 @@ import { TbMathFunction } from "react-icons/tb";
import { PiListPlusBold } from "react-icons/pi";
import {isEqual} from "lodash";
import {type ExpressionSpecification, type LegacyFilterSpecification} from "maplibre-gl";
import {latest, migrate, convertFilter} from "@maplibre/maplibre-gl-style-spec";
import {migrate, convertFilter} from "@maplibre/maplibre-gl-style-spec";
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import {combiningFilterOps} from "../libs/filterops";
import InputSelect from "./InputSelect";
@@ -14,7 +15,7 @@ import InputButton from "./InputButton";
import Doc from "./Doc";
import ExpressionProperty from "./_ExpressionProperty";
import { type WithTranslation, withTranslation } from "react-i18next";
import type { StyleSpecificationWithId } from "../libs/definitions";
import type { MappedLayerErrors, StyleSpecificationWithId } from "../libs/definitions";
function combiningFilter(props: FilterEditorInternalProps): LegacyFilterSpecification | ExpressionSpecification {
@@ -95,8 +96,8 @@ type FilterEditorInternalProps = {
/** Properties of the vector layer and the available fields */
properties?: {[key:string]: any}
filter?: any[]
errors?: {[key:string]: any}
onChange(value: LegacyFilterSpecification | ExpressionSpecification): unknown
errors?: MappedLayerErrors
onChange(value: LegacyFilterSpecification | ExpressionSpecification): void
} & WithTranslation;
type FilterEditorState = {
@@ -293,6 +294,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
this.props.onChange(defaultFilter);
}}
fieldName="filter"
fieldSpec={fieldSpec as any}
value={filter}
errors={errors}
onChange={this.props.onChange}
+23 -65
View File
@@ -11,15 +11,10 @@ import type { StylePropertySpecification } from "maplibre-gl";
export type InputJsonProps = {
value: object
maxHeight?: number
lineNumbers?: boolean
lineWrapping?: boolean
gutters?: string[]
className?: string
onChange(object: object): void
onFocus?(...args: unknown[]): unknown
onBlur?(...args: unknown[]): unknown
onJSONValid?(): void
onJSONInvalid?(_err: Error): void
lintType: "layer" | "style" | "expression" | "json"
spec?: StylePropertySpecification | undefined
};
@@ -32,16 +27,11 @@ type InputJsonState = {
class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJsonState> {
static defaultProps = {
lineNumbers: true,
lineWrapping: false,
gutters: ["CodeMirror-lint-markers"],
onFocus: () => {},
onBlur: () => {},
onJSONInvalid: () => {},
onJSONValid: () => {},
};
_keyEvent: string;
_doc: EditorView | undefined;
_view: EditorView | undefined;
_el: HTMLDivElement | null = null;
_cancelNextChange: boolean = false;
@@ -59,37 +49,15 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
}
componentDidMount () {
this._doc = createEditor(
this._el!,
this.getPrettyJson(this.props.value),
this.props.lintType || "layer",
(value:string) => this.props.onChange(value ? JSON.parse(value) : {}),
this.props.spec
);
/*CodeMirror(this._el!, {
value: this.props.getValue!(this.props.layer),
mode: this.props.mode || {
name: "mgl",
},
lineWrapping: this.props.lineWrapping,
tabSize: 2,
theme: "maputnik",
viewportMargin: Infinity,
lineNumbers: this.props.lineNumbers,
lint: this.props.lint || {
context: "layer"
},
matchBrackets: true,
gutters: this.props.gutters,
scrollbarStyle: "null",
this._view = createEditor({
parent: this._el!,
value: this.getPrettyJson(this.props.value),
lintType: this.props.lintType || "layer",
onChange: (value:string) => this.onChange(value),
onFocus: () => this.onFocus(),
onBlur: () => this.onBlur(),
spec: this.props.spec
});
this._doc.on("change", this.onChange);
this._doc.on("focus", this.onFocus);
this._doc.on("blur", this.onBlur);
*/
this._doc.dispatch();
}
onPointerDown = () => {
@@ -100,7 +68,6 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
if (this.props.onFocus) this.props.onFocus();
this.setState({
isEditing: true,
showMessage: (this._keyEvent === "keyboard"),
});
};
@@ -109,35 +76,31 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
if (this.props.onBlur) this.props.onBlur();
this.setState({
isEditing: false,
showMessage: false,
});
};
componentWillUnMount () {
//this._doc!.off("change", this.onChange);
//this._doc!.off("focus", this.onFocus);
//this._doc!.off("blur", this.onBlur);
}
componentDidUpdate(_prevProps: InputJsonProps) {
//if (!this.state.isEditing && prevProps.layer !== this.props.layer) {
// this._cancelNextChange = true;
// this._doc!.setValue(
// this.props.getValue!(this.props.layer),
// );
//}
componentDidUpdate(prevProps: InputJsonProps) {
if (!this.state.isEditing && prevProps.value !== this.props.value) {
this._cancelNextChange = true;
this._view!.dispatch({
changes: {
from: 0,
to: this._view!.state.doc.length,
insert: this.getPrettyJson(this.props.value)
}
});
}
}
onChange = (_e: unknown) => {
/*
if (this._cancelNextChange) {
this._cancelNextChange = false;
this.setState({
prevValue: this._doc!.getValue(),
prevValue: this._view!.state.doc.toString(),
});
return;
}
const newCode = this._doc!.getValue();
const newCode = this._view!.state.doc.toString();
if (this.state.prevValue !== newCode) {
let parsedLayer, err;
@@ -148,19 +111,14 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
console.warn(_err);
}
if (err && this.props.onJSONInvalid) {
this.props.onJSONInvalid();
}
else {
if (!err) {
if (this.props.onChange) this.props.onChange(parsedLayer);
if (this.props.onJSONValid) this.props.onJSONValid();
}
}
this.setState({
prevValue: newCode,
});
*/
};
render() {
-2
View File
@@ -31,7 +31,6 @@ export type InputSpecProps = {
/** Override the style of the field */
style?: object
"aria-label"?: string
error?: unknown[]
label?: string
action?: ReactElement
};
@@ -43,7 +42,6 @@ export default class InputSpec extends React.Component<InputSpecProps> {
childNodes() {
const commonProps = {
error: this.props.error,
fieldSpec: this.props.fieldSpec,
label: this.props.label,
action: this.props.action,
+3 -3
View File
@@ -22,7 +22,7 @@ import {formatLayerId} from "../libs/format";
import { type WithTranslation, withTranslation } from "react-i18next";
import { type TFunction } from "i18next";
import { NON_SOURCE_LAYERS } from "../libs/non-source-layers";
import { type OnMoveLayerCallback } from "../libs/definitions";
import { type MappedError, type MappedLayerErrors, type OnMoveLayerCallback } from "../libs/definitions";
type MaputnikLayoutGroup = {
id: string;
@@ -128,7 +128,7 @@ type LayerEditorInternalProps = {
isFirstLayer?: boolean
isLastLayer?: boolean
layerIndex: number
errors?: any[]
errors?: MappedError[]
} & WithTranslation;
type LayerEditorState = {
@@ -193,7 +193,7 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
}
const {errors, layerIndex} = this.props;
const errorData: {[key in LayerSpecification as string]: {message: string}} = {};
const errorData: MappedLayerErrors = {};
errors!.forEach(error => {
if (
error.parsed &&
+2 -2
View File
@@ -22,7 +22,7 @@ import type {LayerSpecification, SourceSpecification} from "maplibre-gl";
import generateUniqueId from "../libs/document-uid";
import { findClosestCommonPrefix, layerPrefix } from "../libs/layer";
import { type WithTranslation, withTranslation } from "react-i18next";
import { type OnMoveLayerCallback } from "../libs/definitions";
import { type MappedError, type OnMoveLayerCallback } from "../libs/definitions";
type LayerListContainerProps = {
layers: LayerSpecification[]
@@ -33,7 +33,7 @@ type LayerListContainerProps = {
onLayerCopy(...args: unknown[]): unknown
onLayerVisibilityToggle(...args: unknown[]): unknown
sources: Record<string, SourceSpecification & {layers: string[]}>;
errors: any[]
errors: MappedError[]
};
type LayerListContainerInternalProps = LayerListContainerProps & WithTranslation;
+2 -1
View File
@@ -2,6 +2,7 @@ import React from "react";
import FieldFunction from "./FieldFunction";
import type {LayerSpecification} from "maplibre-gl";
import { type MappedLayerErrors } from "../libs/definitions";
const iconProperties = ["background-pattern", "fill-pattern", "line-pattern", "fill-extrusion-pattern", "icon-image"];
@@ -36,7 +37,7 @@ type PropertyGroupProps = {
groupFields: string[]
onChange(...args: unknown[]): unknown
spec: any
errors?: {[key: string]: {message: string}}
errors?: MappedLayerErrors
};
export default class PropertyGroup extends React.Component<PropertyGroupProps> {
+2 -1
View File
@@ -16,6 +16,7 @@ import { type WithTranslation, withTranslation } from "react-i18next";
import labelFromFieldName from "../libs/label-from-field-name";
import DeleteStopButton from "./_DeleteStopButton";
import { type MappedLayerErrors } from "../libs/definitions";
@@ -47,7 +48,7 @@ type DataPropertyInternalProps = {
fieldType?: string
fieldSpec?: object
value?: DataPropertyValue
errors?: object
errors?: MappedLayerErrors
} & WithTranslation;
type DataPropertyState = {
+9 -4
View File
@@ -7,13 +7,15 @@ import InputButton from "./InputButton";
import labelFromFieldName from "../libs/label-from-field-name";
import FieldJson from "./FieldJson";
import type { StylePropertySpecification } from "maplibre-gl";
import { type MappedLayerErrors } from "../libs/definitions";
type ExpressionPropertyInternalProps = {
fieldName: string
fieldType?: string
fieldSpec?: StylePropertySpecification
value?: any
errors?: {[key: string]: {message: string}}
errors?: MappedLayerErrors
onDelete?(...args: unknown[]): unknown
onChange(value: object): void
onUndo?(...args: unknown[]): unknown
@@ -63,12 +65,17 @@ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInter
</InputButton>
</>
);
let error = undefined;
if (this.props.errors) {
const fieldKey = this.props.fieldType ? this.props.fieldType + "." + this.props.fieldName : this.props.fieldName;
error = this.props.errors[fieldKey];
}
return <Block
fieldSpec={this.props.fieldSpec}
label={t(labelFromFieldName(this.props.fieldName))}
action={deleteStopBtn}
wideMode={true}
error={error}
>
<FieldJson
lintType="expression"
@@ -77,9 +84,7 @@ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInter
onFocus={this.props.onFocus}
onBlur={this.props.onBlur}
value={value}
lineNumbers={false}
maxHeight={200}
lineWrapping={true}
onChange={this.props.onChange}
/>
</Block>;
+2 -1
View File
@@ -15,6 +15,7 @@ import labelFromFieldName from "../libs/label-from-field-name";
import docUid from "../libs/document-uid";
import sortNumerically from "../libs/sort-numerically";
import { type MappedLayerErrors } from "../libs/definitions";
/**
@@ -59,7 +60,7 @@ type ZoomPropertyInternalProps = {
"property-type"?: string
"function-type"?: string
}
errors?: object
errors?: MappedLayerErrors
value?: ZoomWithStops
} & WithTranslation;
+9 -6
View File
@@ -1,7 +1,6 @@
import React, { type FormEvent } from "react";
import {MdFileUpload} from "react-icons/md";
import {MdAddCircleOutline} from "react-icons/md";
import FileReaderInput, { type Result } from "react-file-reader-input";
import { Trans, type WithTranslation, withTranslation } from "react-i18next";
import ModalLoading from "./ModalLoading";
@@ -171,8 +170,10 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
// it is not guaranteed that the File System Access API is available on all
// browsers. If the function is not available, a fallback behavior is used.
onFileChanged = async (_: any, files: Result[]) => {
const [, file] = files[0];
onFileChanged = (files: FileList | null) => {
if (!files) return;
if (files.length === 0) return;
const file = files[0];
const reader = new FileReader();
this.clearError();
@@ -246,13 +247,15 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
<div>
{typeof window.showOpenFilePicker === "function" ? (
<InputButton
data-wd-key="modal:open.file.button"
className="maputnik-big-button"
onClick={this.onOpenFile}><MdFileUpload/> {t("Open Style")}
</InputButton>
) : (
<FileReaderInput onChange={this.onFileChanged} tabIndex={-1} aria-label={t("Open Style")}>
<InputButton className="maputnik-upload-button"><MdFileUpload /> {t("Open Style")}</InputButton>
</FileReaderInput>
<label>
<a className="maputnik-button maputnik-upload-button" aria-label={t("Open Style")}><MdFileUpload /> {t("Open Style")}</a>
<input data-wd-key="modal:open.file.input" type="file" style={{ display: "none" }} onChange={(e) => this.onFileChanged(e.target.files)} />
</label>
)}
</div>
</section>
+54 -22
View File
@@ -1,6 +1,6 @@
import React from "react";
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import type {LightSpecification, StyleSpecification, TerrainSpecification, TransitionSpecification} from "maplibre-gl";
import type {LightSpecification, ProjectionSpecification, StyleSpecification, TerrainSpecification, TransitionSpecification} from "maplibre-gl";
import { type WithTranslation, withTranslation } from "react-i18next";
import FieldArray from "../FieldArray";
@@ -79,6 +79,24 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
});
}
changeProjectionType(value: any) {
const projection = {
...this.props.mapStyle.projection,
} as ProjectionSpecification;
if (value === undefined) {
delete projection.type;
}
else {
projection.type = value;
}
this.props.onStyleChanged({
...this.props.mapStyle,
projection,
});
}
changeStyleProperty(property: keyof StyleSpecification | "owner", value: any) {
const changedStyle = {
...this.props.mapStyle,
@@ -103,6 +121,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
const light = this.props.mapStyle.light || {};
const transition = this.props.mapStyle.transition || {};
const terrain = this.props.mapStyle.terrain || {} as TerrainSpecification;
const projection = this.props.mapStyle.projection || {} as ProjectionSpecification;
return <Modal
data-wd-key="modal:settings"
@@ -116,21 +135,21 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.$root.name}
data-wd-key="modal:settings.name"
value={this.props.mapStyle.name}
onChange={this.changeStyleProperty.bind(this, "name")}
onChange={(value) => this.changeStyleProperty("name", value)}
/>
<FieldString
label={t("Owner")}
fieldSpec={{doc: t("Owner ID of the style. Used by Mapbox or future style APIs.")}}
data-wd-key="modal:settings.owner"
value={(this.props.mapStyle as any).owner}
onChange={this.changeStyleProperty.bind(this, "owner")}
onChange={(value) => this.changeStyleProperty("owner", value)}
/>
<FieldUrl
fieldSpec={latest.$root.sprite}
label={t("Sprite URL")}
data-wd-key="modal:settings.sprite"
value={this.props.mapStyle.sprite as string}
onChange={this.changeStyleProperty.bind(this, "sprite")}
onChange={(value) => this.changeStyleProperty("sprite", value)}
/>
<FieldUrl
@@ -138,7 +157,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.$root.glyphs}
data-wd-key="modal:settings.glyphs"
value={this.props.mapStyle.glyphs as string}
onChange={this.changeStyleProperty.bind(this, "glyphs")}
onChange={(value) => this.changeStyleProperty("glyphs", value)}
/>
<FieldString
@@ -146,7 +165,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={fsa.maputnik.maptiler_access_token}
data-wd-key="modal:settings.maputnik:openmaptiles_access_token"
value={metadata["maputnik:openmaptiles_access_token"]}
onChange={onChangeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
onChange={(value) => onChangeMetadataProperty("maputnik:openmaptiles_access_token", value)}
/>
<FieldString
@@ -154,7 +173,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={fsa.maputnik.thunderforest_access_token}
data-wd-key="modal:settings.maputnik:thunderforest_access_token"
value={metadata["maputnik:thunderforest_access_token"]}
onChange={onChangeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
onChange={(value) => onChangeMetadataProperty("maputnik:thunderforest_access_token", value)}
/>
<FieldString
@@ -162,7 +181,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={fsa.maputnik.stadia_access_token}
data-wd-key="modal:settings.maputnik:stadia_access_token"
value={metadata["maputnik:stadia_access_token"]}
onChange={onChangeMetadataProperty.bind(this, "maputnik:stadia_access_token")}
onChange={(value) => onChangeMetadataProperty("maputnik:stadia_access_token", value)}
/>
<FieldString
@@ -170,7 +189,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={fsa.maputnik.locationiq_access_token}
data-wd-key="modal:settings.maputnik:locationiq_access_token"
value={metadata["maputnik:locationiq_access_token"]}
onChange={onChangeMetadataProperty.bind(this, "maputnik:locationiq_access_token")}
onChange={(value) => onChangeMetadataProperty("maputnik:locationiq_access_token", value)}
/>
<FieldArray
@@ -180,7 +199,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
type="number"
value={mapStyle.center || []}
default={[0, 0]}
onChange={this.changeStyleProperty.bind(this, "center")}
onChange={(value) => this.changeStyleProperty("center", value)}
/>
<FieldNumber
@@ -188,7 +207,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.$root.zoom}
value={mapStyle.zoom}
default={0}
onChange={this.changeStyleProperty.bind(this, "zoom")}
onChange={(value) => this.changeStyleProperty("zoom", value)}
/>
<FieldNumber
@@ -196,7 +215,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.$root.bearing}
value={mapStyle.bearing}
default={latest.$root.bearing.default}
onChange={this.changeStyleProperty.bind(this, "bearing")}
onChange={(value) => this.changeStyleProperty("bearing", value)}
/>
<FieldNumber
@@ -204,7 +223,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.$root.pitch}
value={mapStyle.pitch}
default={latest.$root.pitch.default}
onChange={this.changeStyleProperty.bind(this, "pitch")}
onChange={(value) => this.changeStyleProperty("pitch", value)}
/>
<FieldEnum
@@ -214,7 +233,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
value={light.anchor as string}
options={Object.keys(latest.light.anchor.values)}
default={latest.light.anchor.default}
onChange={this.changeLightProperty.bind(this, "anchor")}
onChange={(value) => this.changeLightProperty("anchor", value)}
/>
<FieldColor
@@ -222,7 +241,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.light.color}
value={light.color as string}
default={latest.light.color.default}
onChange={this.changeLightProperty.bind(this, "color")}
onChange={(value) => this.changeLightProperty("color", value)}
/>
<FieldNumber
@@ -230,7 +249,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.light.intensity}
value={light.intensity as number}
default={latest.light.intensity.default}
onChange={this.changeLightProperty.bind(this, "intensity")}
onChange={(value) => this.changeLightProperty("intensity", value)}
/>
<FieldArray
@@ -240,7 +259,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
length={latest.light.position.length}
value={light.position as number[]}
default={latest.light.position.default}
onChange={this.changeLightProperty.bind(this, "position")}
onChange={(value) => this.changeLightProperty("position", value)}
/>
<FieldString
@@ -248,7 +267,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.terrain.source}
data-wd-key="modal:settings.maputnik:terrain_source"
value={terrain.source}
onChange={this.changeTerrainProperty.bind(this, "source")}
onChange={(value) => this.changeTerrainProperty("source", value)}
/>
<FieldNumber
@@ -256,7 +275,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.terrain.exaggeration}
value={terrain.exaggeration}
default={latest.terrain.exaggeration.default}
onChange={this.changeTerrainProperty.bind(this, "exaggeration")}
onChange={(value) => this.changeTerrainProperty("exaggeration", value)}
/>
<FieldNumber
@@ -264,7 +283,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.transition.delay}
value={transition.delay}
default={latest.transition.delay.default}
onChange={this.changeTransitionProperty.bind(this, "delay")}
onChange={(value) => this.changeTransitionProperty("delay", value)}
/>
<FieldNumber
@@ -272,7 +291,20 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.transition.duration}
value={transition.duration}
default={latest.transition.duration.default}
onChange={this.changeTransitionProperty.bind(this, "duration")}
onChange={(value) => this.changeTransitionProperty("duration", value)}
/>
<FieldSelect
label={t("Projection")}
data-wd-key="modal:settings.projection"
options={[
["", "Undefined"],
["mercator", "Mercator"],
["globe", "Globe"],
["vertical-perspective", "Vertical Perspective"]
]}
value={projection?.type?.toString() || ""}
onChange={(value) => this.changeProjectionType(value)}
/>
<FieldSelect
@@ -284,7 +316,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
["ol", t("Open Layers (experimental)")],
]}
value={metadata["maputnik:renderer"] || "mlgljs"}
onChange={onChangeMetadataProperty.bind(this, "maputnik:renderer")}
onChange={(value) => onChangeMetadataProperty("maputnik:renderer", value)}
/>
</div>
</Modal>;
+30 -13
View File
@@ -1,5 +1,6 @@
import { basicSetup } from "codemirror";
import { EditorView } from "@codemirror/view";
import { EditorState, Compartment } from "@codemirror/state";
import { json, jsonParseLinter } from "@codemirror/lang-json";
import { linter, lintGutter, type Diagnostic } from "@codemirror/lint";
import { oneDark } from "@codemirror/theme-one-dark";
@@ -150,15 +151,17 @@ function createMaplibreExpressionLinter(spec: StylePropertySpecification) {
};
}
export function createEditor(
export function createEditor(props: {
parent: HTMLElement,
value: string,
lintType: LintType,
onChange: (value: string) => void,
onFocus: () => void,
onBlur: () => void,
spec?: StylePropertySpecification,
): EditorView {
}): EditorView {
let specificLinter: (view: EditorView) => Diagnostic[] = () => [];
switch (lintType) {
switch (props.lintType) {
case "style":
specificLinter = createMaplibreStyleLinter();
break;
@@ -166,37 +169,51 @@ export function createEditor(
specificLinter = createMaplibreLayerLinter();
break;
case "expression":
if (!spec) {
if (!props.spec) {
throw new Error("spec is required for expression mode");
}
specificLinter = createMaplibreExpressionLinter(spec);
specificLinter = createMaplibreExpressionLinter(props.spec);
break;
case "json":
specificLinter = () => [];
break;
}
return new EditorView({
doc: value,
doc: props.value,
extensions: [
basicSetup,
json(),
oneDark,
basicSetup,
json(),
oneDark,
new Compartment().of(EditorState.tabSize.of(2)),
EditorView.theme({
"&": {
fontSize: "9pt"
}
}),
EditorView.updateListener.of((update) => {
if (update.docChanged) {
const doc = update.state.doc;
const value = doc.toString();
onChange(value);
props.onChange(value);
}
if (update.focusChanged) {
if (update.view.hasFocus) {
props.onFocus();
} else {
props.onBlur();
}
}
}),
lintGutter(),
linter((view: EditorView) => {
const jsonErrors = jsonParseLinter()(view);
if (jsonErrors.length > 0) {
if (jsonErrors.length > 0) {
return jsonErrors;
}
return specificLinter(view);
})
],
parent,
parent: props.parent,
});
}
}
+16
View File
@@ -16,3 +16,19 @@ export interface IStyleStore {
getLatestStyle(): Promise<StyleSpecificationWithId>;
save(mapStyle: StyleSpecificationWithId): StyleSpecificationWithId;
}
export type MappedError = {
message: string
parsed?: {
type: "layer"
data: {
index: number
key: string
message: string
}
}
};
export type MappedLayerErrors = {
[key in LayerSpecification as string]: {message: string}
};
-100
View File
@@ -1,100 +0,0 @@
@use "vars";
.CodeMirror-lint-tooltip {
z-index: 2000 !important;
}
.codemirror-container {
max-width: 100%;
position: relative;
overflow: auto;
}
.cm-s-maputnik.CodeMirror {
height: 100%;
font-size: 12px;
background: transparent;
}
.cm-s-maputnik.CodeMirror, .cm-s-maputnik .CodeMirror-gutters {
color: #8e8e8e;
border: none;
}
.cm-s-maputnik .CodeMirror-gutters {
background: #212328;
}
.cm-s-maputnik .CodeMirror-cursor {
border-left: solid thin #f0f0f0 !important;
}
.cm-s-maputnik.CodeMirror-focused div.CodeMirror-selected {
background: rgba(255, 255, 255, 0.10);
}
.cm-s-maputnik .CodeMirror-line::selection,
.cm-s-maputnik .CodeMirror-line > span::selection,
.cm-s-maputnik .CodeMirror-line > span > span::selection {
background: rgba(255, 255, 255, 0.10);
}
.cm-s-maputnik .CodeMirror-line::-moz-selection,
.cm-s-maputnik .CodeMirror-line > span::-moz-selection,
.cm-s-maputnik .CodeMirror-line > span > span::-moz-selection {
background: rgba(255, 255, 255, 0.10);
}
.cm-s-maputnik span.cm-string, .cm-s-maputnik span.cm-string-2 {
color: #8f9d6a;
}
.cm-s-maputnik span.cm-number { color: #91675f; }
.cm-s-maputnik span.cm-property { color: #b8a077; }
.cm-s-maputnik .CodeMirror-activeline-background {
background: rgba(255,255,255,0.1);
}
.cm-s-maputnik .CodeMirror-matchingbracket {
background: hsla(223, 12%, 35%, 1);
color: vars.$color-white !important;
}
.cm-s-maputnik .CodeMirror-nonmatchingbracket {
background-color: #bb0000;
color: white !important;
}
@keyframes JSONEditor__animation-fade {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.JSONEditor__message {
position: absolute;
right: 0;
font-size: 0.85em;
z-index: 99999;
padding: 0.3em 0.5em;
background: hsla(0, 0%, 0%, 0.3);
color: vars.$color-lowgray;
border-bottom-left-radius: 2px;
transition: opacity 320ms ease;
opacity: 0;
pointer-events: none;
&--on {
opacity: 1;
animation: 320ms ease 0s JSONEditor__animation-fade;
animation-delay: 2000ms;
animation-fill-mode: forwards;
}
kbd {
font-family: monospace;
}
}
+6
View File
@@ -290,6 +290,12 @@
}
}
.maputnik-input-block--error {
.maputnik-input-block-label {
color: vars.$color-red;
}
}
.maputnik-expr-infobox {
font-size: vars.$font-size-6;
background: vars.$color-midgray;
+1 -1
View File
@@ -252,7 +252,7 @@
margin-top: vars.$margin-3;
}
.SpecDoc__values code {
.SpecDoc__values code, .SpecDoc__doc code {
background: vars.$color-midgray;
padding: 0.1em 0.3em;
border-radius: 2px;
+1 -1
View File
@@ -44,7 +44,7 @@
position: fixed;
bottom: 0;
right: 0;
z-index: 1;
z-index: 10;
width: vars.$layout-map-width;
background-color: vars.$color-black;
}
-1
View File
@@ -15,7 +15,6 @@
@use 'zoomproperty';
@use 'popup';
@use 'map';
@use 'codemirror';
@use 'react-collapse';
.maputnik-layout {