Compare commits

...

17 Commits

Author SHA1 Message Date
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
Harel M 69143ea5d6 Add global state modal (#1377)
## Launch Checklist

This adds the ability to edit the global state.
I think it deserves a modal of its own since I don't think it should be
part of other modals...
Here are some images:
<img width="1274" height="254" alt="image"
src="https://github.com/user-attachments/assets/4b6f2564-6c71-47da-9f8c-3bd2b97e1163"
/>

Initial dialog with no variable:
<img width="640" height="254" alt="image"
src="https://github.com/user-attachments/assets/b813b540-cae9-4c80-b2c0-4d965c022cb8"
/>
After you click add a few times:
<img width="640" height="254" alt="image"
src="https://github.com/user-attachments/assets/125cb978-90dc-4047-9694-b0ffc6eaa469"
/>

The state is updated as you change thing in the dialog.
I didn't complicated it to select the type of the variable, but this can
be added later of if there's a requirement to do so, I meant to keep it
simple for now.

 - [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-14 11:13:52 +03:00
Harel M a322afdcee Use different icons for different layers (#1375)
## Launch Checklist

This changes the layers icons a bit in order to make them a bit more
distinct:

After:
<img width="580" height="331" alt="image"
src="https://github.com/user-attachments/assets/f341a64e-0310-40f7-8f5a-650d7bb06b9f"
/>


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

---------

Co-authored-by: Frank Elsinga <frank@elsinga.de>
2025-09-13 22:46:28 +00:00
Harel M c6f599cc61 Unite icons (#1374)
## Launch Checklist

This removes the `@mdi` (material design icons) package and uses the
icons from `react-icons`.
The icons are not exactly the same, but have the same idea behind them.

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

Main changes can be found below.
Before:
<img width="580" height="780" alt="image"
src="https://github.com/user-attachments/assets/e3d5fc8a-bd59-48fe-bdae-31bb290749c8"
/>


After:

<img width="580" height="780" alt="image"
src="https://github.com/user-attachments/assets/bacdbdba-9e73-4bef-bd30-26fe946269c1"
/>

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-09-13 22:11:13 +02:00
dependabot[bot] 5fe38bb6ff chore(deps-dev): Bump typescript from 5.8.3 to 5.9.2 (#1301)
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.8.3
to 5.9.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/microsoft/TypeScript/releases">typescript's
releases</a>.</em></p>
<blockquote>
<h2>TypeScript 5.9</h2>
<p>Release notes pending.</p>
<!-- raw HTML omitted -->
<ul>
<li><a
href="https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93&amp;q=milestone%3A%22TypeScript+5.9.0%22+is%3Aclosed+">fixed
issues query for Typescript 5.9.0 (Beta)</a>.</li>
<li><a
href="https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93&amp;q=milestone%3A%22TypeScript+5.9.1%22+is%3Aclosed+">fixed
issues query for Typescript 5.9.1 (RC)</a>.</li>
<li>[[No specific changes for TypeScript 5.9.2 (Stable)]]</li>
</ul>
<p>Downloads are available on:</p>
<ul>
<li><a href="https://www.npmjs.com/package/typescript">npm</a></li>
</ul>
<h2>TypeScript 5.9 RC</h2>
<p>For release notes, check out the <a
href="https://devblogs.microsoft.com/typescript/announcing-typescript-5-9-rc/">release
announcement</a></p>
<ul>
<li><a
href="https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93&amp;q=milestone%3A%22TypeScript+5.9.0%22+is%3Aclosed+">fixed
issues query for Typescript 5.9.0 (Beta)</a>.</li>
<li><a
href="https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93&amp;q=milestone%3A%22TypeScript+5.9.1%22+is%3Aclosed+">fixed
issues query for Typescript 5.9.1 (RC)</a>.</li>
</ul>
<p>Downloads are available on:</p>
<ul>
<li><a href="https://www.npmjs.com/package/typescript">npm</a></li>
</ul>
<h2>TypeScript 5.9 Beta</h2>
<p>For release notes, check out the <a
href="https://devblogs.microsoft.com/typescript/announcing-typescript-5-9-beta/">release
announcement</a>.</p>
<ul>
<li><a
href="https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93&amp;q=milestone%3A%22TypeScript+5.9.0%22+is%3Aclosed+">fixed
issues query for Typescript 5.9.0 (Beta)</a>.</li>
</ul>
<p>Downloads are available on:</p>
<ul>
<li><a href="https://www.npmjs.com/package/typescript">npm</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/microsoft/TypeScript/commit/be8678315541e814da14316848a9468e8f90ab11"><code>be86783</code></a>
Give more specific errors for <code>verbatimModuleSyntax</code> (<a
href="https://redirect.github.com/microsoft/TypeScript/issues/62113">#62113</a>)</li>
<li><a
href="https://github.com/microsoft/TypeScript/commit/22ef57786f5ca99837c430439bc6003c068fe1d2"><code>22ef577</code></a>
LEGO: Pull request from
lego/hb_5378966c-b857-470a-8675-daebef4a6da1_20250714...</li>
<li><a
href="https://github.com/microsoft/TypeScript/commit/d5a414cd1dceb209fd2569e89d1096812218e8c5"><code>d5a414c</code></a>
Don't use <code>noErrorTruncation</code> when printing types with
<code>maximumLength</code> set (#...</li>
<li><a
href="https://github.com/microsoft/TypeScript/commit/f14b5c8a2f0be503ac455054a91573c63f0e5088"><code>f14b5c8</code></a>
Remove unused and confusing dom.iterable.d.ts file (<a
href="https://redirect.github.com/microsoft/TypeScript/issues/62037">#62037</a>)</li>
<li><a
href="https://github.com/microsoft/TypeScript/commit/2778e84ed88894156e2c5a17d74a25c73cc96ffe"><code>2778e84</code></a>
Restore AbortSignal.abort (<a
href="https://redirect.github.com/microsoft/TypeScript/issues/62086">#62086</a>)</li>
<li><a
href="https://github.com/microsoft/TypeScript/commit/65cb4bd2d52cd882f2c3a503681479eb2ed291ca"><code>65cb4bd</code></a>
LEGO: Pull request from
lego/hb_5378966c-b857-470a-8675-daebef4a6da1_20250710...</li>
<li><a
href="https://github.com/microsoft/TypeScript/commit/9e20e032effad965567d4a1e1c30d5433b0a3332"><code>9e20e03</code></a>
Clear out checker-level stacks on pop (<a
href="https://redirect.github.com/microsoft/TypeScript/issues/62016">#62016</a>)</li>
<li><a
href="https://github.com/microsoft/TypeScript/commit/87740bc7fe3d8d2af47d767db081491f40b8a90d"><code>87740bc</code></a>
Fix for Issue 61081 (<a
href="https://redirect.github.com/microsoft/TypeScript/issues/61221">#61221</a>)</li>
<li><a
href="https://github.com/microsoft/TypeScript/commit/833a8d492c728d606454865e8c0fee84842f9f10"><code>833a8d4</code></a>
Fix Symbol completion priority and cursor positioning (<a
href="https://redirect.github.com/microsoft/TypeScript/issues/61945">#61945</a>)</li>
<li><a
href="https://github.com/microsoft/TypeScript/commit/0018c9ff12e657b84997dc1843818fea0a2ce850"><code>0018c9f</code></a>
LEGO: Pull request from
lego/hb_5378966c-b857-470a-8675-daebef4a6da1_20250702...</li>
<li>Additional commits viewable in <a
href="https://github.com/microsoft/TypeScript/compare/v5.8.3...v5.9.2">compare
view</a></li>
</ul>
</details>
<br />


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

You can trigger a rebase of this PR 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>

> **Note**
> Automatic rebases have been disabled on this pull request as it has
been open for over 30 days.

---------

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-13 18:45:03 +00:00
dependabot[bot] 51d063ca5a chore(deps-dev): Bump stylelint-config-recommended-scss from 16.0.0 to 16.0.1 (#1373)
Bumps
[stylelint-config-recommended-scss](https://github.com/stylelint-scss/stylelint-config-recommended-scss)
from 16.0.0 to 16.0.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/stylelint-scss/stylelint-config-recommended-scss/releases">stylelint-config-recommended-scss's
releases</a>.</em></p>
<blockquote>
<h2>16.0.1</h2>
<ul>
<li>Fixed: false positive for
<code>nesting-selector-no-missing-scoping-root</code>.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/stylelint-scss/stylelint-config-recommended-scss/blob/master/CHANGELOG.md">stylelint-config-recommended-scss's
changelog</a>.</em></p>
<blockquote>
<h1>16.0.1</h1>
<ul>
<li>Fixed: false positive for
<code>nesting-selector-no-missing-scoping-root</code>.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/stylelint-scss/stylelint-config-recommended-scss/commit/a9edfb274b12b75e1319388bea3ca73e6df56907"><code>a9edfb2</code></a>
16.0.1</li>
<li><a
href="https://github.com/stylelint-scss/stylelint-config-recommended-scss/commit/02dc994e6ddc8f69e2bf68578b031f58ee1f802b"><code>02dc994</code></a>
Prepare 16.0.1</li>
<li><a
href="https://github.com/stylelint-scss/stylelint-config-recommended-scss/commit/82329b174b110bc02ce0cc0fa24dcd5ce9e6ed69"><code>82329b1</code></a>
Revert stylelint peer dep version bump (<a
href="https://redirect.github.com/stylelint-scss/stylelint-config-recommended-scss/issues/368">#368</a>)</li>
<li><a
href="https://github.com/stylelint-scss/stylelint-config-recommended-scss/commit/02e99ca3a19a1bf81747b6bdd525273f54a04e74"><code>02e99ca</code></a>
Bump eslint from 9.32.0 to 9.35.0 (<a
href="https://redirect.github.com/stylelint-scss/stylelint-config-recommended-scss/issues/367">#367</a>)</li>
<li><a
href="https://github.com/stylelint-scss/stylelint-config-recommended-scss/commit/0c168c85de633d2a84197bf0128ed99a477caa8e"><code>0c168c8</code></a>
Bump actions/checkout from 4 to 5 (<a
href="https://redirect.github.com/stylelint-scss/stylelint-config-recommended-scss/issues/363">#363</a>)</li>
<li><a
href="https://github.com/stylelint-scss/stylelint-config-recommended-scss/commit/c554a0e637101ed37403499b84b0a0cce3ee7e8c"><code>c554a0e</code></a>
Fix false positive for nesting-selector-no-missing-scoping-root (<a
href="https://redirect.github.com/stylelint-scss/stylelint-config-recommended-scss/issues/365">#365</a>)</li>
<li>See full diff in <a
href="https://github.com/stylelint-scss/stylelint-config-recommended-scss/compare/v16.0.0...v16.0.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=stylelint-config-recommended-scss&package-manager=npm_and_yarn&previous-version=16.0.0&new-version=16.0.1)](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-12 13:18:06 +00:00
Harel M 42e1273241 Color relief support and hillshading improvements. (#1371)
## Launch Checklist

This adds support for `relief-color` property so that it will create an
elevation expression when the button is pressed.
It also adds support for `colorArray` and `numberArray` types so that
the user would be able to add the relevant information.

Before:
<img width="403" height="324" alt="image"
src="https://github.com/user-attachments/assets/250abd81-6176-4711-a1ee-d33d443932d7"
/>

<img width="403" height="324" alt="image"
src="https://github.com/user-attachments/assets/6a1bb268-66db-42a1-97fc-33e5a40863b6"
/>


After:
<img width="403" height="324" alt="image"
src="https://github.com/user-attachments/assets/8ebaa1ea-4ef9-4aed-abcd-3c8b0057ea76"
/>

<img width="403" height="324" alt="image"
src="https://github.com/user-attachments/assets/e0728c92-85f9-4b86-8635-8877cf257b2f"
/>


 - [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-11 20:43:20 +03:00
Harel M d81316435b Fix missing spec info (#1370)
## Launch Checklist

This fixes an issue with missing info in the spec field due to using the
redacted spec (the one without the docs).

Before - missing button near color when hovering

<img width="397" height="202" alt="image"
src="https://github.com/user-attachments/assets/6f261407-5348-4a71-a6d8-afaff5b27e03"
/>

After:
<img width="397" height="202" alt="image"
src="https://github.com/user-attachments/assets/d2de1b2d-d4cf-4394-b995-e50b957da72c"
/>

I think I already solved this in the past, too bad I didn't add the
relevant tests back then...

 - [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-11 10:09:26 +00:00
dependabot[bot] 9f84f2989f chore(deps-dev): Bump cypress from 15.1.0 to 15.2.0 (#1369)
Bumps [cypress](https://github.com/cypress-io/cypress) from 15.1.0 to
15.2.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/cypress-io/cypress/releases">cypress's
releases</a>.</em></p>
<blockquote>
<h2>v15.2.0</h2>
<p>Changelog: <a
href="https://docs.cypress.io/app/references/changelog#15.2.0">https://docs.cypress.io/app/references/changelog#15.2.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/cypress-io/cypress/commit/058e7d26c0a0a2402a15ad5638508cd38777b350"><code>058e7d2</code></a>
chore: release 15.2.0 (<a
href="https://redirect.github.com/cypress-io/cypress/issues/32453">#32453</a>)</li>
<li><a
href="https://github.com/cypress-io/cypress/commit/14199e09052c880650331e1916839358669247c5"><code>14199e0</code></a>
chore: updating v8 snapshot cache (<a
href="https://redirect.github.com/cypress-io/cypress/issues/32451">#32451</a>)</li>
<li><a
href="https://github.com/cypress-io/cypress/commit/d3279551c8f376d5a3fe311d81ded0c6a0197ec9"><code>d327955</code></a>
chore: updating v8 snapshot cache (<a
href="https://redirect.github.com/cypress-io/cypress/issues/32450">#32450</a>)</li>
<li><a
href="https://github.com/cypress-io/cypress/commit/0f65f181f38226c18271645066dc4cd9a9b81f92"><code>0f65f18</code></a>
chore: updating v8 snapshot cache (<a
href="https://redirect.github.com/cypress-io/cypress/issues/32449">#32449</a>)</li>
<li><a
href="https://github.com/cypress-io/cypress/commit/02fb0975588ca24a26fe57c67066830b758515d4"><code>02fb097</code></a>
chore: add --glob to rimraf command (<a
href="https://redirect.github.com/cypress-io/cypress/issues/32444">#32444</a>)</li>
<li><a
href="https://github.com/cypress-io/cypress/commit/9f0e71fced38c4f1b4c0f7748109e58ed03d16d9"><code>9f0e71f</code></a>
chore: Update styles in runner to not be fractional + only update
scrollbar s...</li>
<li><a
href="https://github.com/cypress-io/cypress/commit/a07062d13e536e3c6a77160bd6657deb0fd74304"><code>a07062d</code></a>
fix(dns-cache): prevent local family cache collisions (<a
href="https://redirect.github.com/cypress-io/cypress/issues/32403">#32403</a>)</li>
<li><a
href="https://github.com/cypress-io/cypress/commit/9ebdddad9e34d51ec4a2d96f782cc201e00fca55"><code>9ebddda</code></a>
chore: Migrate <code>@​packages/electron</code> to TS (<a
href="https://redirect.github.com/cypress-io/cypress/issues/32417">#32417</a>)</li>
<li><a
href="https://github.com/cypress-io/cypress/commit/4671317eb1b9830f61e2d32daeac3b55c01bb94c"><code>4671317</code></a>
feat: (studio) support <code>@cypress/grep</code> package in Cypress
Studio (<a
href="https://redirect.github.com/cypress-io/cypress/issues/32311">#32311</a>)</li>
<li><a
href="https://github.com/cypress-io/cypress/commit/8eda0c8153ba6543450862aea5a226050370af90"><code>8eda0c8</code></a>
chore: Update v8 snapshot cache - linux (<a
href="https://redirect.github.com/cypress-io/cypress/issues/32439">#32439</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/cypress-io/cypress/compare/v15.1.0...v15.2.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=cypress&package-manager=npm_and_yarn&previous-version=15.1.0&new-version=15.2.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-10 13:16:40 +00:00
Harel M c3c6118df1 Fix popup issue (#1368)
## Launch Checklist

When clicking the map to create a popup and then clicking on the map
again without closing the popup something breaks in react.
This PR fixes it.

Steps to reproduce:
1. Click on the map
2. Click on the map but not on the popup

The following malformed popup appears:
Before:
<img width="610" height="626" alt="image"
src="https://github.com/user-attachments/assets/99cbdf9a-0b3c-436d-ab13-f219ee3c6e75"
/>

After - popup looks good.
<img width="610" height="626" alt="image"
src="https://github.com/user-attachments/assets/6b379c0d-cbbf-40ed-916d-3cc10eaf4410"
/>

I couldn't make cypress reproduce the exact scenario, but I added some
basic tests for popups.

 - [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-10 11:44:38 +00:00
Harel M 9c85883b8a Move modals and rename classes to match file names (#1367)
## Launch Checklist

I've created a folder for modals and moved them all inside.
I removed the icons which were small files with no real benefit and
moved the icons to where they were used.
I fixed an issue with the close button position in modal.
I've added missing translation to "Links" in debug modal.

Before: 

<img width="610" height="81" alt="image"
src="https://github.com/user-attachments/assets/dd7520f6-9634-4ff1-a83d-99ceae7c9144"
/>

After:
<img width="610" height="81" alt="image"
src="https://github.com/user-attachments/assets/fe3a2ccf-6c09-42ab-bf6f-dd30d3c68e13"
/>


 - [x] Briefly describe the changes in this PR.
- [x] Include before/after visuals or gifs if this PR includes visual
changes.
 - [ ] Add an entry to `CHANGELOG.md` under the `## main` section.
2025-09-10 14:37:23 +03:00
Harel M 3725f83b48 Update React to 19 (#1360)
## Launch Checklist

This updates React to 19.
It removes the unmaintained `react-icon-base` package and uses icons
from the existing one (also removes the svgs).
It also updates the other packages so that everything should be up to
date.

 - [x] Briefly describe the changes in this PR.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-09-09 22:52:52 +00:00
Harel M 7bfc3188f7 Fix layer selection (#1366)
## Launch Checklist

- This fixes the issue of layer selection that was introduced as part of
the migration to dnd-kit #1359
- Fixes #1364

This also adds the relevant test.

 - [x] Briefly describe the changes in this PR.
 - [x] Link to related issues.
 - [x] Write tests for all new functionality.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-09-10 01:46:58 +03:00
Harel M 25d6e9693d Enhance RTL support in _layer.scss (#1365)
Added alignment to item handle and adjusted SVG margins for RTL support,
and fix menu.

## Launch Checklist

Before:
<img width="610" height="465" alt="image"
src="https://github.com/user-attachments/assets/8931d5af-e545-4b37-b764-ca660b182c27"
/>
After:
<img width="610" height="465" alt="image"
src="https://github.com/user-attachments/assets/07d865c0-bd1b-4fd6-bd4f-cd3a8c17126a"
/>


 - [x] Briefly describe the changes in this PR.
- [x] Include before/after visuals or gifs if this PR includes visual
changes.
2025-09-10 01:17:18 +03:00
134 changed files with 4190 additions and 2464 deletions
+12
View File
@@ -1,10 +1,22 @@
## main ## main
### ✨ Features and improvements ### ✨ Features and improvements
- Added translation to "Links" in debug modal
- 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
- _...Add new stuff here..._ - _...Add new stuff here..._
### 🐞 Bug fixes ### 🐞 Bug fixes
- Fixed an issue when clicking on a popup and then clicking on the map again
- 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
- _...Add new stuff here..._ - _...Add new stuff here..._
## 3.0.0 ## 3.0.0
+6 -6
View File
@@ -10,12 +10,12 @@
* https://github.com/maplibre/maplibre-gl-js/blob/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build/release-notes.js * https://github.com/maplibre/maplibre-gl-js/blob/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build/release-notes.js
*/ */
import * as fs from 'fs'; import * as fs from "fs";
const changelogPath = 'CHANGELOG.md'; const changelogPath = "CHANGELOG.md";
let changelog = fs.readFileSync(changelogPath, 'utf8'); let changelog = fs.readFileSync(changelogPath, "utf8");
changelog = changelog.replace('## main', `## ${process.argv[2]}`); changelog = changelog.replace("## main", `## ${process.argv[2]}`);
changelog = changelog.replaceAll('- _...Add new stuff here..._\n', ''); changelog = changelog.replaceAll("- _...Add new stuff here..._\n", "");
changelog = `## main changelog = `## main
### ✨ Features and improvements ### ✨ Features and improvements
@@ -26,4 +26,4 @@ changelog = `## main
` + changelog; ` + changelog;
fs.writeFileSync(changelogPath, changelog, 'utf8'); fs.writeFileSync(changelogPath, changelog, "utf8");
+7 -7
View File
@@ -3,10 +3,10 @@
// Copied from maplibre/maplibre-gl-js // Copied from maplibre/maplibre-gl-js
// https://github.com/maplibre/maplibre-gl-js/blob/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build/release-notes.js // https://github.com/maplibre/maplibre-gl-js/blob/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build/release-notes.js
import * as fs from 'fs'; import * as fs from "fs";
const changelogPath = 'CHANGELOG.md'; const changelogPath = "CHANGELOG.md";
const changelog = fs.readFileSync(changelogPath, 'utf8'); const changelog = fs.readFileSync(changelogPath, "utf8");
/* /*
Parse the raw changelog text and split it into individual releases. Parse the raw changelog text and split it into individual releases.
@@ -25,8 +25,8 @@ let match;
// eslint-disable-next-line no-cond-assign // eslint-disable-next-line no-cond-assign
while (match = regex.exec(changelog)) { while (match = regex.exec(changelog)) {
releaseNotes.push({ releaseNotes.push({
'version': match[1], "version": match[1],
'changelog': match[2].trim(), "changelog": match[2].trim(),
}); });
} }
@@ -35,10 +35,10 @@ const previous = releaseNotes[1];
// Print the release notes template. // Print the release notes template.
let header = 'Changes since previous version' let header = "Changes since previous version";
if (previous) { if (previous) {
header = `https://github.com/maplibre/maputnik header = `https://github.com/maplibre/maputnik
[Changes](https://github.com/maplibre/maputnik/compare/v${previous.version}...v${latest.version}) since [Maputnik v${previous.version}](https://github.com/maplibre/maputnik/releases/tag/v${previous.version})` [Changes](https://github.com/maplibre/maputnik/compare/v${previous.version}...v${latest.version}) since [Maputnik v${previous.version}](https://github.com/maplibre/maputnik/releases/tag/v${previous.version})`;
} }
const templatedReleaseNotes = `${header} const templatedReleaseNotes = `${header}
+159 -4
View File
@@ -97,8 +97,26 @@ describe("layers", () => {
}); });
}); });
}); });
describe("when selecting a layer", () => {
let secondId: string;
beforeEach(() => {
when.modal.open();
secondId = when.modal.fillLayers({
id: "second-layer",
type: "background",
});
});
it("should show the selected layer in the editor", () => {
when.realClick("layer-list-item:" + secondId);
then(get.elementByTestId("layer-editor.layer-id.input")).shouldHaveValue(secondId);
when.realClick("layer-list-item:" + id);
then(get.elementByTestId("layer-editor.layer-id.input")).shouldHaveValue(id);
});
});
}); });
}); });
describe("background", () => { describe("background", () => {
it("add", () => { it("add", () => {
const id = when.modal.fillLayers({ const id = when.modal.fillLayers({
@@ -295,7 +313,7 @@ describe("layers", () => {
it("should revert to a valid value when focus out", () => { it("should revert to a valid value when focus out", () => {
when.click("layer-list-item:background:" + bgId); when.click("layer-list-item:background:" + bgId);
then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue('0'); then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue("0");
}); });
}); });
@@ -449,6 +467,44 @@ describe("layers", () => {
], ],
}); });
}); });
it("should show spec info when hovering and clicking single line property", () => {
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);
then(get.elementByTestId("spec-field-doc")).shouldContainText("Rotates the ");
});
it("should show spec info when hovering and clicking multi line property", () => {
when.modal.fillLayers({
type: "symbol",
layer: "example",
});
when.hover("spec-field-container:text-offset");
then(get.elementByTestId("field-doc-button-Offset")).shouldBeVisible();
when.click("field-doc-button-Offset", 0);
then(get.elementByTestId("spec-field-doc")).shouldContainText("Offset distance");
});
it.only("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", () => { describe("raster", () => {
@@ -508,6 +564,104 @@ describe("layers", () => {
}); });
}); });
describe("hillshade", () => {
it("add", () => {
const id = when.modal.fillLayers({
type: "hillshade",
layer: "example",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "hillshade",
source: "example",
},
],
});
});
it("set hillshade illumination direction array", () => {
const id = when.modal.fillLayers({
type: "hillshade",
layer: "example",
});
when.collapseGroupInLayerEditor();
when.collapseGroupInLayerEditor(1);
when.setValueToPropertyArray("spec-field:hillshade-illumination-direction", "1");
when.addValueToPropertyArray("spec-field:hillshade-illumination-direction", "2");
when.addValueToPropertyArray("spec-field:hillshade-illumination-direction", "3");
when.addValueToPropertyArray("spec-field:hillshade-illumination-direction", "4");
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "hillshade",
source: "example",
paint: {
"hillshade-illumination-direction": [ 1, 2, 3, 4 ]
}
},
],
});
});
it("set hillshade highlight color array", () => {
const id = when.modal.fillLayers({
type: "hillshade",
layer: "example",
});
when.collapseGroupInLayerEditor();
when.setValueToPropertyArray("spec-field:hillshade-highlight-color", "blue");
when.addValueToPropertyArray("spec-field:hillshade-highlight-color", "#00ff00");
when.addValueToPropertyArray("spec-field:hillshade-highlight-color", "rgba(255, 255, 0, 1)");
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "hillshade",
source: "example",
paint: {
"hillshade-highlight-color": [ "blue", "#00ff00", "rgba(255, 255, 0, 1)" ]
}
},
],
});
});
});
describe("color-relief", () => {
it("add", () => {
const id = when.modal.fillLayers({
type: "color-relief",
layer: "example",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "color-relief",
source: "example",
},
],
});
});
it("adds elevation expression when clicking the elevation button", () => {
when.modal.fillLayers({
type: "color-relief",
layer: "example",
});
when.collapseGroupInLayerEditor();
when.click("make-elevation-function");
then(get.element("[data-wd-key='spec-field-container:color-relief-color'] .CodeMirror-line")).shouldBeVisible();
});
});
describe("groups", () => { describe("groups", () => {
it("simple", () => { it("simple", () => {
when.setStyle("geojson"); when.setStyle("geojson");
@@ -566,8 +720,8 @@ describe("layers", () => {
sourceText.click(); sourceText.click();
sourceText.type("\""); sourceText.type("\"");
const error = get.element('.CodeMirror-lint-marker-error'); const error = get.element(".CodeMirror-lint-marker-error");
error.should('exist'); error.should("exist");
}); });
}); });
@@ -588,7 +742,8 @@ describe("layers", () => {
id: "c", id: "c",
type: "background", type: "background",
}); });
when.dragAndDrop(get.elementByTestId("layer-list-item:" + firstId), get.elementByTestId("layer-list-item:" + thirdId));
when.dragAndDropWithWait("layer-list-item:" + firstId, "layer-list-item:" + thirdId);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [ layers: [
+20 -1
View File
@@ -25,8 +25,27 @@ describe("map", () => {
}); });
describe("search", () => { describe("search", () => {
it('should exist', () => { it("should exist", () => {
then(get.searchControl()).shouldBeVisible(); then(get.searchControl()).shouldBeVisible();
}); });
}); });
describe("popup", () => {
beforeEach(() => {
when.setStyle("rectangles");
});
it("should open on feature click", () => {
when.clickCenter("maplibre:map");
then(get.elementByTestId("feature-layer-popup")).shouldBeVisible();
});
it("should open a second feature after closing popup", () => {
when.clickCenter("maplibre:map");
then(get.elementByTestId("feature-layer-popup")).shouldBeVisible();
when.closePopup();
then(get.elementByTestId("feature-layer-popup")).shouldNotExist();
when.clickCenter("maplibre:map");
then(get.elementByTestId("feature-layer-popup")).shouldBeVisible();
});
});
}); });
+30
View File
@@ -1,4 +1,6 @@
/// <reference types="cypress-real-events" />
import { CypressHelper } from "@shellygo/cypress-test-utils"; import { CypressHelper } from "@shellygo/cypress-test-utils";
import "cypress-real-events/support";
export default class MaputnikCypressHelper { export default class MaputnikCypressHelper {
private helper = new CypressHelper({ defaultDataAttribute: "data-wd-key" }); private helper = new CypressHelper({ defaultDataAttribute: "data-wd-key" });
@@ -12,6 +14,34 @@ export default class MaputnikCypressHelper {
}; };
public when = { public when = {
dragAndDropWithWait: (element: string, targetElement: string) => {
this.helper.get.elementByTestId(element).realMouseDown({ button: "left", position: "center" });
this.helper.get.elementByTestId(element).realMouseMove(0, 10, { position: "center" });
this.helper.get.elementByTestId(targetElement).realMouseMove(0, 0, { position: "center" });
this.helper.when.wait(1);
this.helper.get.elementByTestId(targetElement).realMouseUp();
},
clickCenter: (element: string) => {
this.helper.get.elementByTestId(element).realMouseDown({ button: "left", position: "center" });
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, ...this.helper.when,
}; };
+50 -18
View File
@@ -78,6 +78,13 @@ export class MaputnikDriver {
fixture: "geojson-raster-style.json", fixture: "geojson-raster-style.json",
}, },
}); });
this.helper.given.interceptAndMockResponse({
method: "GET",
url: baseUrl + "rectangles-style.json",
response: {
fixture: "rectangles-style.json",
},
});
this.helper.given.interceptAndMockResponse({ this.helper.given.interceptAndMockResponse({
method: "GET", method: "GET",
url: "*example.local/*", url: "*example.local/*",
@@ -102,29 +109,33 @@ export class MaputnikDriver {
this.helper.when.waitForResponse("example-style.json"); this.helper.when.waitForResponse("example-style.json");
}, },
chooseExampleFile: () => { chooseExampleFile: () => {
this.helper.get this.helper.given.fixture("example-style.json", "example-style.json");
.bySelector("type", "file") this.helper.when.openFileByFixture("example-style.json", "modal:open.file.button", "modal:open.file.input");
.selectFile("cypress/fixtures/example-style.json", { force: true }); this.helper.when.wait(200);
}, },
setStyle: ( setStyle: (
styleProperties: "geojson" | "raster" | "both" | "layer" | "", styleProperties: "geojson" | "raster" | "both" | "layer" | "rectangles" | "",
zoom?: number zoom?: number
) => { ) => {
const url = new URL(baseUrl); const url = new URL(baseUrl);
switch (styleProperties) { switch (styleProperties) {
case "geojson": case "geojson":
url.searchParams.set("style", baseUrl + "geojson-style.json"); url.searchParams.set("style", baseUrl + "geojson-style.json");
break; break;
case "raster": case "raster":
url.searchParams.set("style", baseUrl + "raster-style.json"); url.searchParams.set("style", baseUrl + "raster-style.json");
break; break;
case "both": case "both":
url.searchParams.set("style", baseUrl + "geojson-raster-style.json"); url.searchParams.set("style", baseUrl + "geojson-raster-style.json");
break; break;
case "layer": case "layer":
url.searchParams.set("style", baseUrl + "example-layer-style.json"); url.searchParams.set("style", baseUrl + "example-layer-style.json");
break; break;
case "rectangles":
url.searchParams.set("style", baseUrl + "rectangles-style.json");
break;
} }
if (zoom) { if (zoom) {
url.hash = `${zoom}/41.3805/2.1635`; url.hash = `${zoom}/41.3805/2.1635`;
} }
@@ -133,7 +144,7 @@ export class MaputnikDriver {
this.helper.when.acceptConfirm(); this.helper.when.acceptConfirm();
} }
// when methods should not include assertions // when methods should not include assertions
const toolbarLink = this.helper.get.elementByTestId("toolbar:link") const toolbarLink = this.helper.get.elementByTestId("toolbar:link");
toolbarLink.scrollIntoView(); toolbarLink.scrollIntoView();
toolbarLink.should("be.visible"); toolbarLink.should("be.visible");
}, },
@@ -164,6 +175,27 @@ export class MaputnikDriver {
.clear() .clear()
.type(text, { parseSpecialCharSequences: false }); .type(text, { parseSpecialCharSequences: false });
}, },
setValueToPropertyArray: (selector: string, value: string) => {
this.when.doWithin(selector, () => {
this.helper.get.element(".maputnik-array-block-content input").last().type("{selectall}"+value, {force: true });
});
},
addValueToPropertyArray: (selector: string, value: string) => {
this.when.doWithin(selector, () => {
this.helper.get.element(".maputnik-array-add-value").click({ force: true });
this.helper.get.element(".maputnik-array-block-content input").last().type("{selectall}"+value, {force: true });
});
},
closePopup: () => {
this.helper.get.element(".maplibregl-popup-close-button").click();
},
collapseGroupInLayerEditor: (index = 0) => {
this.helper.get.element(".maputnik-layer-editor-group__button").eq(index).realClick();
}
}; };
public get = { public get = {
@@ -183,6 +215,6 @@ export class MaputnikDriver {
skipTargetLayerEditor: () => skipTargetLayerEditor: () =>
this.helper.get.elementByTestId("skip-target-layer-editor"), this.helper.get.elementByTestId("skip-target-layer-editor"),
canvas: () => this.helper.get.element("canvas"), canvas: () => this.helper.get.element("canvas"),
searchControl: () => this.helper.get.element('.maplibregl-ctrl-geocoder') searchControl: () => this.helper.get.element(".maplibregl-ctrl-geocoder")
}; };
} }
+84 -6
View File
@@ -18,10 +18,9 @@ describe("modals", () => {
then(get.elementByTestId("modal:open")).shouldNotExist(); then(get.elementByTestId("modal:open")).shouldNotExist();
}); });
it.skip("upload", () => { it("upload", () => {
// HM: I was not able to make the following choose file actually to select a file and close the modal...
when.chooseExampleFile(); when.chooseExampleFile();
then(get.responseBody("example-style.json")).shouldEqualToStoredStyle(); then(get.fixture("example-style.json")).shouldEqualToStoredStyle();
}); });
describe("when click open url", () => { describe("when click open url", () => {
@@ -236,6 +235,29 @@ describe("modals", () => {
).shouldInclude({ "maputnik:locationiq_access_token": apiKey }); ).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", () => { 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
when.select("modal:settings.maputnik:renderer", "ol"); when.select("modal:settings.maputnik:renderer", "ol");
@@ -253,10 +275,10 @@ describe("modals", () => {
it("inlcude API key when change renderer", () => { it("inlcude API key when change renderer", () => {
when.click("modal:settings.close-modal") when.click("modal:settings.close-modal");
when.click("nav:open"); when.click("nav:open");
get.elementByAttribute('aria-label', "MapTiler Basic").should('exist').click(); get.elementByAttribute("aria-label", "MapTiler Basic").should("exist").click();
when.wait(1000); when.wait(1000);
when.click("nav:settings"); when.click("nav:settings");
@@ -304,6 +326,62 @@ describe("modals", () => {
it("toggle"); it("toggle");
}); });
describe("global state", () => {
beforeEach(() => {
when.click("nav:global-state");
});
it("add variable", () => {
when.click("global-state-add-variable");
when.wait(100);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
state: { key1: { default: "value" } },
});
});
it("add multiple variables", () => {
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" } },
});
});
it("remove variable", () => {
when.click("global-state-add-variable");
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" } },
});
});
it("edit variable key", () => {
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" } },
});
});
it("edit variable value", () => {
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("Handle localStorage QuotaExceededError", () => { describe("Handle localStorage QuotaExceededError", () => {
it("handles quota exceeded error when opening style from URL", () => { it("handles quota exceeded error when opening style from URL", () => {
// Clear localStorage to start fresh // Clear localStorage to start fresh
@@ -322,7 +400,7 @@ describe("modals", () => {
win.localStorage.setItem(key, chunk); win.localStorage.setItem(key, chunk);
} catch (e: any) { } catch (e: any) {
// Verify it's a quota error // Verify it's a quota error
if (e.name === 'QuotaExceededError') { if (e.name === "QuotaExceededError") {
if (chunkSize <= 1) return; if (chunkSize <= 1) return;
else { else {
chunkSize /= 2; chunkSize /= 2;
+92
View File
@@ -0,0 +1,92 @@
{
"version": 8,
"sources": {
"rectangles": {
"type": "geojson",
"data": {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
-130.78125,
-33.13755119234615
],
[
-130.78125,
63.548552232036414
],
[
15.468749999999998,
63.548552232036414
],
[
15.468749999999998,
-33.13755119234615
],
[
-130.78125,
-33.13755119234615
]
]
]
}
},
{
"type": "Feature",
"properties": {},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
-48.515625,
-54.97761367069625
],
[
-48.515625,
36.5978891330702
],
[
169.45312499999997,
36.5978891330702
],
[
169.45312499999997,
-54.97761367069625
],
[
-48.515625,
-54.97761367069625
]
]
]
}
}
]
}
}
},
"layers": [
{
"id": "background",
"type": "background",
"paint": {
"background-color": "white"
}
},
{
"id": "rectangles",
"type": "fill",
"source": "rectangles",
"paint": {
"fill-opacity": 0.3
}
}
]
}
+3 -3
View File
@@ -14,9 +14,9 @@
// *********************************************************** // ***********************************************************
// Import commands.js using ES2015 syntax: // Import commands.js using ES2015 syntax:
import './commands' import "./commands";
import { mount } from 'cypress/react' import { mount } from "cypress/react";
// Augment the Cypress namespace to include type definitions for // Augment the Cypress namespace to include type definitions for
// your custom command. // your custom command.
@@ -31,7 +31,7 @@ declare global {
} }
} }
Cypress.Commands.add('mount', mount) Cypress.Commands.add("mount", mount);
// Example use: // Example use:
// cy.mount(<MyComponent />) // cy.mount(<MyComponent />)
+39 -30
View File
@@ -1,56 +1,65 @@
import eslint from '@eslint/js'; import eslint from "@eslint/js";
import tseslint from 'typescript-eslint'; import {defineConfig} from "eslint/config";
import reactPlugin from 'eslint-plugin-react'; import stylisticTs from "@stylistic/eslint-plugin";
import reactHooksPlugin from 'eslint-plugin-react-hooks'; import tseslint from "typescript-eslint";
import reactRefreshPlugin from 'eslint-plugin-react-refresh'; import reactPlugin from "eslint-plugin-react";
import reactHooksPlugin from "eslint-plugin-react-hooks";
import reactRefreshPlugin from "eslint-plugin-react-refresh";
export default tseslint.config({ export default defineConfig({
extends: [ extends: [
eslint.configs.recommended, eslint.configs.recommended,
tseslint.configs.recommended, tseslint.configs.recommended,
], ],
files: ['**/*.{js,jsx,ts,tsx}'], files: ["**/*.{js,jsx,ts,tsx}"],
ignores: [ ignores: [
"dist/**/*", "dist/**/*",
], ],
languageOptions: { languageOptions: {
ecmaVersion: 2024, ecmaVersion: 2024,
sourceType: 'module', sourceType: "module",
globals: { globals: {
global: 'readonly' global: "readonly"
} }
}, },
settings: { settings: {
react: { version: '18.2' } react: { version: "18.2" }
}, },
plugins: { plugins: {
'react': reactPlugin, "react": reactPlugin,
'react-hooks': reactHooksPlugin, "react-hooks": reactHooksPlugin,
'react-refresh': reactRefreshPlugin "react-refresh": reactRefreshPlugin,
"@stylistic": stylisticTs
}, },
rules: { rules: {
'react-refresh/only-export-components': [ "react-refresh/only-export-components": [
'warn', "warn",
{ allowConstantExport: true } { allowConstantExport: true }
], ],
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
'@typescript-eslint/no-unused-vars': [ "@typescript-eslint/no-unused-vars": [
'warn', "warn",
{ {
varsIgnorePattern: '^_', varsIgnorePattern: "^_",
caughtErrors: 'all', caughtErrors: "all",
caughtErrorsIgnorePattern: '^_', caughtErrorsIgnorePattern: "^_",
argsIgnorePattern: '^_' argsIgnorePattern: "^_"
} }
], ],
'no-unused-vars': 'off', "no-unused-vars": "off",
'react/prop-types': 'off', "react/prop-types": "off",
'no-undef': 'off', "no-undef": "off",
'indent': ['error', 2], "indent": "off",
'no-var': 'error', "@stylistic/indent": ["error", 2],
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off', "semi": "off",
'@typescript-eslint/ban-ts-comment': 'off', "@stylistic/semi": ["error", "always"],
'@typescript-eslint/no-empty-object-type': 'off', "quotes": "off",
"@stylistic/quotes": ["error", "double", { avoidEscape: true }],
"no-var": "error",
"@typescript-eslint/no-non-null-asserted-optional-chain": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-empty-object-type": "off",
"@typescript-eslint/consistent-type-imports": ["error", { "fixStyle": "inline-type-imports" }],
}, },
linterOptions: { linterOptions: {
@@ -58,4 +67,4 @@ export default tseslint.config({
noInlineConfig: false noInlineConfig: false
} }
} }
) );
+4 -4
View File
@@ -1,6 +1,6 @@
export default { export default {
output: 'src/locales/$LOCALE/$NAMESPACE.json', output: "src/locales/$LOCALE/$NAMESPACE.json",
locales: [ 'de', 'fr', 'he', 'it','ja', 'zh' ], locales: [ "de", "fr", "he", "it","ja", "zh" ],
// Because some keys are dynamically generated, i18next-parser can't detect them. // Because some keys are dynamically generated, i18next-parser can't detect them.
// We add these keys manually, so we don't want to remove them. // We add these keys manually, so we don't want to remove them.
@@ -12,6 +12,6 @@ export default {
defaultValue: (_locale, _ns, _key) => { defaultValue: (_locale, _ns, _key) => {
// The default value is a string that indicates that the string is not translated. // The default value is a string that indicates that the string is not translated.
return '__STRING_NOT_TRANSLATED__'; return "__STRING_NOT_TRANSLATED__";
} }
} };
+1258 -175
View File
File diff suppressed because it is too large Load Diff
+10 -14
View File
@@ -31,14 +31,12 @@
"@maplibre/maplibre-gl-geocoder": "^1.9.0", "@maplibre/maplibre-gl-geocoder": "^1.9.0",
"@maplibre/maplibre-gl-inspect": "^1.7.1", "@maplibre/maplibre-gl-inspect": "^1.7.1",
"@maplibre/maplibre-gl-style-spec": "^23.3.0", "@maplibre/maplibre-gl-style-spec": "^23.3.0",
"@mdi/js": "^7.4.47",
"@mdi/react": "^1.6.1",
"@prantlf/jsonlint": "^16.0.0", "@prantlf/jsonlint": "^16.0.0",
"array-move": "^4.0.0", "array-move": "^4.0.0",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"codemirror": "^5.65.20", "codemirror": "^5.65.20",
"color": "^5.0.0", "color": "^5.0.2",
"detect-browser": "^5.3.0", "detect-browser": "^5.3.0",
"downshift": "^9.0.10", "downshift": "^9.0.10",
"events": "^3.3.0", "events": "^3.3.0",
@@ -61,17 +59,16 @@
"ol-mapbox-style": "^13.1.0", "ol-mapbox-style": "^13.1.0",
"pmtiles": "^4.3.0", "pmtiles": "^4.3.0",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react": "^18.2.0", "react": "^19.1.1",
"react-accessible-accordion": "^5.0.1", "react-accessible-accordion": "^5.0.1",
"react-aria-menubutton": "^7.0.3", "react-aria-menubutton": "^7.0.3",
"react-aria-modal": "^5.0.2", "react-aria-modal": "^5.0.2",
"react-collapse": "^5.1.1", "react-collapse": "^5.1.1",
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-dom": "^18.2.0", "react-dom": "^19.1.1",
"react-file-reader-input": "^2.0.0",
"react-i18next": "^15.7.3", "react-i18next": "^15.7.3",
"react-icon-base": "^2.1.2",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"reconnecting-websocket": "^4.4.0", "reconnecting-websocket": "^4.4.0",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"string-hash": "^1.1.3", "string-hash": "^1.1.3",
@@ -100,6 +97,7 @@
"@istanbuljs/nyc-config-typescript": "^1.0.2", "@istanbuljs/nyc-config-typescript": "^1.0.2",
"@rollup/plugin-replace": "^6.0.2", "@rollup/plugin-replace": "^6.0.2",
"@shellygo/cypress-test-utils": "^6.0.1", "@shellygo/cypress-test-utils": "^6.0.1",
"@stylistic/eslint-plugin": "^5.3.1",
"@types/codemirror": "^5.60.16", "@types/codemirror": "^5.60.16",
"@types/color": "^4.2.0", "@types/color": "^4.2.0",
"@types/cors": "^2.8.19", "@types/cors": "^2.8.19",
@@ -113,20 +111,18 @@
"@types/lodash.isequal": "^4.5.8", "@types/lodash.isequal": "^4.5.8",
"@types/lodash.throttle": "^4.1.9", "@types/lodash.throttle": "^4.1.9",
"@types/randomcolor": "^0.5.9", "@types/randomcolor": "^0.5.9",
"@types/react": "^18.2.67", "@types/react": "^19.1.12",
"@types/react-aria-menubutton": "^6.2.14", "@types/react-aria-menubutton": "^6.2.14",
"@types/react-aria-modal": "^5.0.0", "@types/react-aria-modal": "^5.0.0",
"@types/react-collapse": "^5.0.4", "@types/react-collapse": "^5.0.4",
"@types/react-color": "^3.0.13", "@types/react-color": "^3.0.13",
"@types/react-dom": "^18.2.22", "@types/react-dom": "^19.1.9",
"@types/react-file-reader-input": "^2.0.4",
"@types/react-icon-base": "^2.1.6",
"@types/string-hash": "^1.1.3", "@types/string-hash": "^1.1.3",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"@types/wicg-file-system-access": "^2023.10.6", "@types/wicg-file-system-access": "^2023.10.6",
"@vitejs/plugin-react": "^5.0.2", "@vitejs/plugin-react": "^5.0.2",
"cors": "^2.8.5", "cors": "^2.8.5",
"cypress": "^15.1.0", "cypress": "^15.2.0",
"cypress-plugin-tab": "^1.0.5", "cypress-plugin-tab": "^1.0.5",
"eslint": "^9.35.0", "eslint": "^9.35.0",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
@@ -139,9 +135,9 @@
"react-hot-loader": "^4.13.1", "react-hot-loader": "^4.13.1",
"sass": "^1.92.1", "sass": "^1.92.1",
"stylelint": "^16.24.0", "stylelint": "^16.24.0",
"stylelint-config-recommended-scss": "^16.0.0", "stylelint-config-recommended-scss": "^16.0.1",
"stylelint-scss": "^6.12.1", "stylelint-scss": "^6.12.1",
"typescript": "^5.8.3", "typescript": "^5.9.2",
"typescript-eslint": "^8.43.0", "typescript-eslint": "^8.43.0",
"uuid": "^13.0.0", "uuid": "^13.0.0",
"vite": "^7.1.5", "vite": "^7.1.5",
+172 -156
View File
@@ -1,40 +1,42 @@
import React from 'react' import React from "react";
import cloneDeep from 'lodash.clonedeep' import cloneDeep from "lodash.clonedeep";
import clamp from 'lodash.clamp' import clamp from "lodash.clamp";
import buffer from 'buffer' import buffer from "buffer";
import get from 'lodash.get' import get from "lodash.get";
import {unset} from 'lodash' import {unset} from "lodash";
import {arrayMoveMutable} from 'array-move' import {arrayMoveMutable} from "array-move";
import hash from "string-hash"; import hash from "string-hash";
import { PMTiles } from "pmtiles"; import { PMTiles } from "pmtiles";
import {Map, LayerSpecification, StyleSpecification, ValidationError, SourceSpecification} from 'maplibre-gl' import {type Map, type LayerSpecification, type StyleSpecification, type ValidationError, type SourceSpecification} from "maplibre-gl";
import {latest, validateStyleMin} from '@maplibre/maplibre-gl-style-spec' import {validateStyleMin} from "@maplibre/maplibre-gl-style-spec";
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import MapMaplibreGl from './MapMaplibreGl' import MapMaplibreGl from "./MapMaplibreGl";
import MapOpenLayers from './MapOpenLayers' import MapOpenLayers from "./MapOpenLayers";
import LayerList from './LayerList' import LayerList from "./LayerList";
import LayerEditor from './LayerEditor' import LayerEditor from "./LayerEditor";
import AppToolbar, { MapState } from './AppToolbar' import AppToolbar, { type MapState } from "./AppToolbar";
import AppLayout from './AppLayout' import AppLayout from "./AppLayout";
import MessagePanel from './AppMessagePanel' import MessagePanel from "./AppMessagePanel";
import ModalSettings from './ModalSettings' import ModalSettings from "./modals/ModalSettings";
import ModalExport from './ModalExport' import ModalExport from "./modals/ModalExport";
import ModalSources from './ModalSources' import ModalSources from "./modals/ModalSources";
import ModalOpen from './ModalOpen' import ModalOpen from "./modals/ModalOpen";
import ModalShortcuts from './ModalShortcuts' import ModalShortcuts from "./modals/ModalShortcuts";
import ModalDebug from './ModalDebug' import ModalDebug from "./modals/ModalDebug";
import ModalGlobalState from "./modals/ModalGlobalState";
import {downloadGlyphsMetadata, downloadSpriteMetadata} from '../libs/metadata' import {downloadGlyphsMetadata, downloadSpriteMetadata} from "../libs/metadata";
import style from '../libs/style' import style from "../libs/style";
import { undoMessages, redoMessages } from '../libs/diffmessage' import { undoMessages, redoMessages } from "../libs/diffmessage";
import { createStyleStore, type IStyleStore } from '../libs/store/style-store-factory' import { createStyleStore, type IStyleStore } from "../libs/store/style-store-factory";
import { RevisionStore } from '../libs/revisions' import { RevisionStore } from "../libs/revisions";
import LayerWatcher from '../libs/layerwatcher' import LayerWatcher from "../libs/layerwatcher";
import tokens from '../config/tokens.json' import tokens from "../config/tokens.json";
import isEqual from 'lodash.isequal' import isEqual from "lodash.isequal";
import { MapOptions } from 'maplibre-gl'; import { type MapOptions } from "maplibre-gl";
import { OnStyleChangedOpts, StyleSpecificationWithId } from '../libs/definitions' import { type OnStyleChangedOpts, type StyleSpecificationWithId } from "../libs/definitions";
// Buffer must be defined globally for @maplibre/maplibre-gl-style-spec validate() function to succeed. // Buffer must be defined globally for @maplibre/maplibre-gl-style-spec validate() function to succeed.
window.Buffer = buffer.Buffer; window.Buffer = buffer.Buffer;
@@ -45,21 +47,21 @@ function setFetchAccessToken(url: string, mapStyle: StyleSpecification) {
const matchesThunderforest = url.match(/\.thunderforest\.com/); const matchesThunderforest = url.match(/\.thunderforest\.com/);
const matchesLocationIQ = url.match(/\.locationiq\.com/); const matchesLocationIQ = url.match(/\.locationiq\.com/);
if (matchesTilehosting || matchesMaptiler) { if (matchesTilehosting || matchesMaptiler) {
const accessToken = style.getAccessToken("openmaptiles", mapStyle, {allowFallback: true}) const accessToken = style.getAccessToken("openmaptiles", mapStyle, {allowFallback: true});
if (accessToken) { if (accessToken) {
return url.replace('{key}', accessToken) return url.replace("{key}", accessToken);
} }
} }
else if (matchesThunderforest) { else if (matchesThunderforest) {
const accessToken = style.getAccessToken("thunderforest", mapStyle, {allowFallback: true}) const accessToken = style.getAccessToken("thunderforest", mapStyle, {allowFallback: true});
if (accessToken) { if (accessToken) {
return url.replace('{key}', accessToken) return url.replace("{key}", accessToken);
} }
} }
else if (matchesLocationIQ) { else if (matchesLocationIQ) {
const accessToken = style.getAccessToken("locationiq", mapStyle, {allowFallback: true}) const accessToken = style.getAccessToken("locationiq", mapStyle, {allowFallback: true});
if (accessToken) { if (accessToken) {
return url.replace('{key}', accessToken) return url.replace("{key}", accessToken);
} }
} }
else { else {
@@ -77,7 +79,7 @@ function updateRootSpec(spec: any, fieldName: string, newValues: any) {
values: newValues values: newValues
} }
} }
} };
} }
type MappedErrors = { type MappedErrors = {
@@ -90,7 +92,7 @@ type MappedErrors = {
message: string message: string
} }
} }
} };
type AppState = { type AppState = {
errors: MappedErrors[], errors: MappedErrors[],
@@ -125,9 +127,10 @@ type AppState = {
shortcuts: boolean shortcuts: boolean
export: boolean export: boolean
debug: boolean debug: boolean
globalState: boolean
} }
fileHandle: FileSystemFileHandle | null fileHandle: FileSystemFileHandle | null
} };
export default class App extends React.Component<any, AppState> { export default class App extends React.Component<any, AppState> {
revisionStore: RevisionStore; revisionStore: RevisionStore;
@@ -135,7 +138,7 @@ export default class App extends React.Component<any, AppState> {
layerWatcher: LayerWatcher; layerWatcher: LayerWatcher;
constructor(props: any) { constructor(props: any) {
super(props) super(props);
this.revisionStore = new RevisionStore(); this.revisionStore = new RevisionStore();
this.configureKeyboardShortcuts(); this.configureKeyboardShortcuts();
@@ -163,6 +166,7 @@ export default class App extends React.Component<any, AppState> {
shortcuts: false, shortcuts: false,
export: false, export: false,
debug: false, debug: false,
globalState: false,
}, },
maplibreGlDebugOptions: { maplibreGlDebugOptions: {
showTileBoundaries: false, showTileBoundaries: false,
@@ -173,11 +177,11 @@ export default class App extends React.Component<any, AppState> {
debugToolbox: false, debugToolbox: false,
}, },
fileHandle: null, fileHandle: null,
} };
this.layerWatcher = new LayerWatcher({ this.layerWatcher = new LayerWatcher({
onVectorLayersChange: v => this.setState({ vectorLayers: v }) onVectorLayersChange: v => this.setState({ vectorLayers: v })
}) });
} }
configureKeyboardShortcuts = () => { configureKeyboardShortcuts = () => {
@@ -212,6 +216,12 @@ export default class App extends React.Component<any, AppState> {
this.toggleModal("settings"); this.toggleModal("settings");
} }
}, },
{
key: "g",
handler: () => {
this.toggleModal("globalState");
}
},
{ {
key: "i", key: "i",
handler: () => { handler: () => {
@@ -232,7 +242,7 @@ export default class App extends React.Component<any, AppState> {
this.toggleModal("debug"); this.toggleModal("debug");
} }
}, },
] ];
document.body.addEventListener("keyup", (e) => { document.body.addEventListener("keyup", (e) => {
if(e.key === "Escape") { if(e.key === "Escape") {
@@ -241,19 +251,19 @@ export default class App extends React.Component<any, AppState> {
} }
else if(this.state.isOpen.shortcuts || document.activeElement === document.body) { else if(this.state.isOpen.shortcuts || document.activeElement === document.body) {
const shortcut = shortcuts.find((shortcut) => { const shortcut = shortcuts.find((shortcut) => {
return (shortcut.key === e.key) return (shortcut.key === e.key);
}) });
if(shortcut) { if(shortcut) {
this.setModal("shortcuts", false); this.setModal("shortcuts", false);
shortcut.handler(); shortcut.handler();
} }
} }
}) });
} };
handleKeyPress = (e: KeyboardEvent) => { handleKeyPress = (e: KeyboardEvent) => {
if(navigator.platform.toUpperCase().indexOf('MAC') >= 0) { if(navigator.platform.toUpperCase().indexOf("MAC") >= 0) {
if(e.metaKey && e.shiftKey && e.keyCode === 90) { if(e.metaKey && e.shiftKey && e.keyCode === 90) {
e.preventDefault(); e.preventDefault();
this.onRedo(); this.onRedo();
@@ -273,7 +283,7 @@ export default class App extends React.Component<any, AppState> {
this.onRedo(); this.onRedo();
} }
} }
} };
async componentDidMount() { async componentDidMount() {
this.styleStore = await createStyleStore((mapStyle, opts) => this.onStyleChanged(mapStyle, opts)); this.styleStore = await createStyleStore((mapStyle, opts) => this.onStyleChanged(mapStyle, opts));
@@ -285,33 +295,33 @@ export default class App extends React.Component<any, AppState> {
} }
saveStyle(snapshotStyle: StyleSpecificationWithId) { saveStyle(snapshotStyle: StyleSpecificationWithId) {
this.styleStore?.save(snapshotStyle) this.styleStore?.save(snapshotStyle);
} }
updateFonts(urlTemplate: string) { updateFonts(urlTemplate: string) {
const metadata: {[key: string]: string} = this.state.mapStyle.metadata || {} as any const metadata: {[key: string]: string} = this.state.mapStyle.metadata || {} as any;
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles const accessToken = metadata["maputnik:openmaptiles_access_token"] || tokens.openmaptiles;
const glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate; const glyphUrl = (typeof urlTemplate === "string")? urlTemplate.replace("{key}", accessToken): urlTemplate;
downloadGlyphsMetadata(glyphUrl, fonts => { downloadGlyphsMetadata(glyphUrl, fonts => {
this.setState({ spec: updateRootSpec(this.state.spec, 'glyphs', fonts)}) this.setState({ spec: updateRootSpec(this.state.spec, "glyphs", fonts)});
}) });
} }
updateIcons(baseUrl: string) { updateIcons(baseUrl: string) {
downloadSpriteMetadata(baseUrl, icons => { downloadSpriteMetadata(baseUrl, icons => {
this.setState({ spec: updateRootSpec(this.state.spec, 'sprite', icons)}) this.setState({ spec: updateRootSpec(this.state.spec, "sprite", icons)});
}) });
} }
onChangeMetadataProperty = (property: string, value: any) => { onChangeMetadataProperty = (property: string, value: any) => {
// If we're changing renderer reset the map state. // If we're changing renderer reset the map state.
if ( if (
property === 'maputnik:renderer' && property === "maputnik:renderer" &&
value !== get(this.state.mapStyle, ['metadata', 'maputnik:renderer'], 'mlgljs') value !== get(this.state.mapStyle, ["metadata", "maputnik:renderer"], "mlgljs")
) { ) {
this.setState({ this.setState({
mapState: 'map' mapState: "map"
}); });
} }
@@ -321,10 +331,10 @@ export default class App extends React.Component<any, AppState> {
...(this.state.mapStyle as any).metadata, ...(this.state.mapStyle as any).metadata,
[property]: value [property]: value
} }
} };
this.onStyleChanged(changedStyle) this.onStyleChanged(changedStyle);
} };
onStyleChanged = (newStyle: StyleSpecificationWithId, opts: OnStyleChangedOpts={}): void => { onStyleChanged = (newStyle: StyleSpecificationWithId, opts: OnStyleChangedOpts={}): void => {
opts = { opts = {
@@ -337,16 +347,16 @@ export default class App extends React.Component<any, AppState> {
// For the style object, find the urls that has "{key}" and insert the correct API keys // For the style object, find the urls that has "{key}" and insert the correct API keys
// Without this, going from e.g. MapTiler to OpenLayers and back will lose the maptlier key. // Without this, going from e.g. MapTiler to OpenLayers and back will lose the maptlier key.
if (newStyle.glyphs && typeof newStyle.glyphs === 'string') { if (newStyle.glyphs && typeof newStyle.glyphs === "string") {
newStyle.glyphs = setFetchAccessToken(newStyle.glyphs, newStyle); newStyle.glyphs = setFetchAccessToken(newStyle.glyphs, newStyle);
} }
if (newStyle.sprite && typeof newStyle.sprite === 'string') { if (newStyle.sprite && typeof newStyle.sprite === "string") {
newStyle.sprite = setFetchAccessToken(newStyle.sprite, newStyle); newStyle.sprite = setFetchAccessToken(newStyle.sprite, newStyle);
} }
for (const [_sourceId, source] of Object.entries(newStyle.sources)) { for (const [_sourceId, source] of Object.entries(newStyle.sources)) {
if (source && 'url' in source && typeof source.url === 'string') { if (source && "url" in source && typeof source.url === "string") {
source.url = setFetchAccessToken(source.url, newStyle); source.url = setFetchAccessToken(source.url, newStyle);
} }
} }
@@ -388,7 +398,7 @@ export default class App extends React.Component<any, AppState> {
message, message,
} }
} }
} };
} }
// Special case: Invalid source // Special case: Invalid source
@@ -405,7 +415,7 @@ export default class App extends React.Component<any, AppState> {
message, message,
} }
} }
} };
} }
const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/); const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/);
@@ -422,7 +432,7 @@ export default class App extends React.Component<any, AppState> {
message message
} }
} }
} };
} }
else { else {
return { return {
@@ -452,10 +462,10 @@ export default class App extends React.Component<any, AppState> {
} }
if(newStyle.glyphs !== this.state.mapStyle.glyphs) { if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
this.updateFonts(newStyle.glyphs as string) this.updateFonts(newStyle.glyphs as string);
} }
if(newStyle.sprite !== this.state.mapStyle.sprite) { if(newStyle.sprite !== this.state.mapStyle.sprite) {
this.updateIcons(newStyle.sprite as string) this.updateIcons(newStyle.sprite as string);
} }
if (opts.addRevision) { if (opts.addRevision) {
@@ -472,28 +482,28 @@ export default class App extends React.Component<any, AppState> {
}, () => { }, () => {
this.fetchSources(); this.fetchSources();
this.setStateInUrl(); this.setStateInUrl();
}) });
} };
onUndo = () => { onUndo = () => {
const activeStyle = this.revisionStore.undo() const activeStyle = this.revisionStore.undo();
const messages = undoMessages(this.state.mapStyle, activeStyle) const messages = undoMessages(this.state.mapStyle, activeStyle);
this.onStyleChanged(activeStyle, {addRevision: false}); this.onStyleChanged(activeStyle, {addRevision: false});
this.setState({ this.setState({
infos: messages, infos: messages,
}) });
} };
onRedo = () => { onRedo = () => {
const activeStyle = this.revisionStore.redo() const activeStyle = this.revisionStore.redo();
const messages = redoMessages(this.state.mapStyle, activeStyle) const messages = redoMessages(this.state.mapStyle, activeStyle);
this.onStyleChanged(activeStyle, {addRevision: false}); this.onStyleChanged(activeStyle, {addRevision: false});
this.setState({ this.setState({
infos: messages, infos: messages,
}) });
} };
onMoveLayer = (move: {oldIndex: number; newIndex: number}) => { onMoveLayer = (move: {oldIndex: number; newIndex: number}) => {
let { oldIndex, newIndex } = move; let { oldIndex, newIndex } = move;
@@ -511,97 +521,97 @@ export default class App extends React.Component<any, AppState> {
layers = layers.slice(0); layers = layers.slice(0);
arrayMoveMutable(layers, oldIndex, newIndex); arrayMoveMutable(layers, oldIndex, newIndex);
this.onLayersChange(layers); this.onLayersChange(layers);
} };
onLayersChange = (changedLayers: LayerSpecification[]) => { onLayersChange = (changedLayers: LayerSpecification[]) => {
const changedStyle = { const changedStyle = {
...this.state.mapStyle, ...this.state.mapStyle,
layers: changedLayers layers: changedLayers
} };
this.onStyleChanged(changedStyle) this.onStyleChanged(changedStyle);
} };
onLayerDestroy = (index: number) => { onLayerDestroy = (index: number) => {
const layers = this.state.mapStyle.layers; const layers = this.state.mapStyle.layers;
const remainingLayers = layers.slice(0); const remainingLayers = layers.slice(0);
remainingLayers.splice(index, 1); remainingLayers.splice(index, 1);
this.onLayersChange(remainingLayers); this.onLayersChange(remainingLayers);
} };
onLayerCopy = (index: number) => { onLayerCopy = (index: number) => {
const layers = this.state.mapStyle.layers; const layers = this.state.mapStyle.layers;
const changedLayers = layers.slice(0) const changedLayers = layers.slice(0);
const clonedLayer = cloneDeep(changedLayers[index]) const clonedLayer = cloneDeep(changedLayers[index]);
clonedLayer.id = clonedLayer.id + "-copy" clonedLayer.id = clonedLayer.id + "-copy";
changedLayers.splice(index, 0, clonedLayer) changedLayers.splice(index, 0, clonedLayer);
this.onLayersChange(changedLayers) this.onLayersChange(changedLayers);
} };
onLayerVisibilityToggle = (index: number) => { onLayerVisibilityToggle = (index: number) => {
const layers = this.state.mapStyle.layers; const layers = this.state.mapStyle.layers;
const changedLayers = layers.slice(0) const changedLayers = layers.slice(0);
const layer = { ...changedLayers[index] } const layer = { ...changedLayers[index] };
const changedLayout = 'layout' in layer ? {...layer.layout} : {} const changedLayout = "layout" in layer ? {...layer.layout} : {};
changedLayout.visibility = changedLayout.visibility === 'none' ? 'visible' : 'none' changedLayout.visibility = changedLayout.visibility === "none" ? "visible" : "none";
layer.layout = changedLayout layer.layout = changedLayout;
changedLayers[index] = layer changedLayers[index] = layer;
this.onLayersChange(changedLayers) this.onLayersChange(changedLayers);
} };
onLayerIdChange = (index: number, _oldId: string, newId: string) => { onLayerIdChange = (index: number, _oldId: string, newId: string) => {
const changedLayers = this.state.mapStyle.layers.slice(0) const changedLayers = this.state.mapStyle.layers.slice(0);
changedLayers[index] = { changedLayers[index] = {
...changedLayers[index], ...changedLayers[index],
id: newId id: newId
} };
this.onLayersChange(changedLayers) this.onLayersChange(changedLayers);
} };
onLayerChanged = (index: number, layer: LayerSpecification) => { onLayerChanged = (index: number, layer: LayerSpecification) => {
const changedLayers = this.state.mapStyle.layers.slice(0) const changedLayers = this.state.mapStyle.layers.slice(0);
changedLayers[index] = layer changedLayers[index] = layer;
this.onLayersChange(changedLayers) this.onLayersChange(changedLayers);
} };
setMapState = (newState: MapState) => { setMapState = (newState: MapState) => {
this.setState({ this.setState({
mapState: newState mapState: newState
}, this.setStateInUrl); }, this.setStateInUrl);
} };
setDefaultValues = (styleObj: StyleSpecificationWithId) => { setDefaultValues = (styleObj: StyleSpecificationWithId) => {
const metadata: {[key: string]: string} = styleObj.metadata || {} as any const metadata: {[key: string]: string} = styleObj.metadata || {} as any;
if(metadata['maputnik:renderer'] === undefined) { if(metadata["maputnik:renderer"] === undefined) {
const changedStyle = { const changedStyle = {
...styleObj, ...styleObj,
metadata: { metadata: {
...styleObj.metadata as any, ...styleObj.metadata as any,
'maputnik:renderer': 'mlgljs' "maputnik:renderer": "mlgljs"
} }
} };
return changedStyle return changedStyle;
} else { } else {
return styleObj return styleObj;
} }
} };
openStyle = (styleObj: StyleSpecificationWithId, fileHandle: FileSystemFileHandle | null) => { openStyle = (styleObj: StyleSpecificationWithId, fileHandle: FileSystemFileHandle | null) => {
this.setState({fileHandle: fileHandle}); this.setState({fileHandle: fileHandle});
styleObj = this.setDefaultValues(styleObj) styleObj = this.setDefaultValues(styleObj);
this.onStyleChanged(styleObj) this.onStyleChanged(styleObj);
} };
async fetchSources() { async fetchSources() {
const sourceList: {[key: string]: SourceSpecification & {layers: string[]}} = {}; const sourceList: {[key: string]: SourceSpecification & {layers: string[]}} = {};
for(const key of Object.keys(this.state.mapStyle.sources)) { for(const key of Object.keys(this.state.mapStyle.sources)) {
const source = this.state.mapStyle.sources[key]; const source = this.state.mapStyle.sources[key];
if(source.type !== "vector" || !('url' in source)) { if(source.type !== "vector" || !("url" in source)) {
sourceList[key] = this.state.sources[key] || {...this.state.mapStyle.sources[key]}; sourceList[key] = this.state.sources[key] || {...this.state.mapStyle.sources[key]};
if (sourceList[key].layers === undefined) { if (sourceList[key].layers === undefined) {
sourceList[key].layers = []; sourceList[key].layers = [];
@@ -615,7 +625,7 @@ export default class App extends React.Component<any, AppState> {
let url = source.url; let url = source.url;
try { try {
url = setFetchAccessToken(url!, this.state.mapStyle) url = setFetchAccessToken(url!, this.state.mapStyle);
} catch(err) { } catch(err) {
console.warn("Failed to setFetchAccessToken: ", err); console.warn("Failed to setFetchAccessToken: ", err);
} }
@@ -626,7 +636,7 @@ export default class App extends React.Component<any, AppState> {
} }
for(const layer of json.vector_layers) { for(const layer of json.vector_layers) {
sourceList[key].layers.push(layer.id) sourceList[key].layers.push(layer.id);
} }
}; };
@@ -635,7 +645,7 @@ export default class App extends React.Component<any, AppState> {
const json = await (new PMTiles(url!.substring(10))).getTileJson(""); const json = await (new PMTiles(url!.substring(10))).getTileJson("");
setVectorLayers(json); setVectorLayers(json);
} else { } else {
const response = await fetch(url!, { mode: 'cors' }); const response = await fetch(url!, { mode: "cors" });
const json = await response.json(); const json = await response.json();
setVectorLayers(json); setVectorLayers(json);
} }
@@ -649,13 +659,13 @@ export default class App extends React.Component<any, AppState> {
console.debug("Setting sources", sourceList); console.debug("Setting sources", sourceList);
this.setState({ this.setState({
sources: sourceList sources: sourceList
}) });
} }
} }
_getRenderer () { _getRenderer () {
const metadata: {[key:string]: string} = this.state.mapStyle.metadata || {} as any; const metadata: {[key:string]: string} = this.state.mapStyle.metadata || {} as any;
return metadata['maputnik:renderer'] || 'mlgljs'; return metadata["maputnik:renderer"] || "mlgljs";
} }
onMapChange = (mapView: { onMapChange = (mapView: {
@@ -668,7 +678,7 @@ export default class App extends React.Component<any, AppState> {
this.setState({ this.setState({
mapView, mapView,
}); });
} };
mapRenderer() { mapRenderer() {
const {mapStyle, dirtyMapStyle} = this.state; const {mapStyle, dirtyMapStyle} = this.state;
@@ -681,23 +691,23 @@ export default class App extends React.Component<any, AppState> {
}); });
}, },
onDataChange: (e: {map: Map}) => { onDataChange: (e: {map: Map}) => {
this.layerWatcher.analyzeMap(e.map) this.layerWatcher.analyzeMap(e.map);
this.fetchSources(); this.fetchSources();
}, },
} };
const renderer = this._getRenderer(); const renderer = this._getRenderer();
let mapElement; let mapElement;
// Check if OL code has been loaded? // Check if OL code has been loaded?
if(renderer === 'ol') { if(renderer === "ol") {
mapElement = <MapOpenLayers mapElement = <MapOpenLayers
{...mapProps} {...mapProps}
onChange={this.onMapChange} onChange={this.onMapChange}
debugToolbox={this.state.openlayersDebugOptions.debugToolbox} debugToolbox={this.state.openlayersDebugOptions.debugToolbox}
onLayerSelect={this.onLayerSelect} onLayerSelect={(layerId) => this.onLayerSelect(+layerId)}
/> />;
} else { } else {
mapElement = <MapMaplibreGl {...mapProps} mapElement = <MapMaplibreGl {...mapProps}
@@ -705,7 +715,7 @@ export default class App extends React.Component<any, AppState> {
options={this.state.maplibreGlDebugOptions} options={this.state.maplibreGlDebugOptions}
inspectModeEnabled={this.state.mapState === "inspect"} inspectModeEnabled={this.state.mapState === "inspect"}
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]} highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]}
onLayerSelect={this.onLayerSelect} /> onLayerSelect={this.onLayerSelect} />;
} }
let filterName; let filterName;
@@ -719,7 +729,7 @@ export default class App extends React.Component<any, AppState> {
return <div style={elementStyle} className="maputnik-map__container" data-wd-key="maplibre:container"> return <div style={elementStyle} className="maputnik-map__container" data-wd-key="maplibre:container">
{mapElement} {mapElement}
</div> </div>;
} }
setStateInUrl = () => { setStateInUrl = () => {
@@ -748,7 +758,7 @@ export default class App extends React.Component<any, AppState> {
} }
history.replaceState({selectedLayerIndex}, "Maputnik", url.href); history.replaceState({selectedLayerIndex}, "Maputnik", url.href);
} };
getInitialStateFromUrl = (mapStyle: StyleSpecification) => { getInitialStateFromUrl = (mapStyle: StyleSpecification) => {
const url = new URL(location.href); const url = new URL(location.href);
@@ -801,14 +811,14 @@ export default class App extends React.Component<any, AppState> {
console.warn(err); console.warn(err);
} }
} }
} };
onLayerSelect = (index: number) => { onLayerSelect = (index: number) => {
this.setState({ this.setState({
selectedLayerIndex: index, selectedLayerIndex: index,
selectedLayerOriginalId: this.state.mapStyle.layers[index].id, selectedLayerOriginalId: this.state.mapStyle.layers[index].id,
}, this.setStateInUrl); }, this.setStateInUrl);
} };
setModal(modalName: keyof AppState["isOpen"], value: boolean) { setModal(modalName: keyof AppState["isOpen"], value: boolean) {
this.setState({ this.setState({
@@ -816,7 +826,7 @@ export default class App extends React.Component<any, AppState> {
...this.state.isOpen, ...this.state.isOpen,
[modalName]: value [modalName]: value
} }
}, this.setStateInUrl) }, this.setStateInUrl);
} }
toggleModal(modalName: keyof AppState["isOpen"]) { toggleModal(modalName: keyof AppState["isOpen"]) {
@@ -825,7 +835,7 @@ export default class App extends React.Component<any, AppState> {
onSetFileHandle = (fileHandle: FileSystemFileHandle | null) => { onSetFileHandle = (fileHandle: FileSystemFileHandle | null) => {
this.setState({ fileHandle }); this.setState({ fileHandle });
} };
onChangeOpenlayersDebug = (key: keyof AppState["openlayersDebugOptions"], value: boolean) => { onChangeOpenlayersDebug = (key: keyof AppState["openlayersDebugOptions"], value: boolean) => {
this.setState({ this.setState({
@@ -834,7 +844,7 @@ export default class App extends React.Component<any, AppState> {
[key]: value, [key]: value,
} }
}); });
} };
onChangeMaplibreGlDebug = (key: keyof AppState["maplibreGlDebugOptions"], value: any) => { onChangeMaplibreGlDebug = (key: keyof AppState["maplibreGlDebugOptions"], value: any) => {
this.setState({ this.setState({
@@ -843,11 +853,11 @@ export default class App extends React.Component<any, AppState> {
[key]: value, [key]: value,
} }
}); });
} };
render() { render() {
const layers = this.state.mapStyle.layers || [] const layers = this.state.mapStyle.layers || [];
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : undefined const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : undefined;
const toolbar = <AppToolbar const toolbar = <AppToolbar
renderer={this._getRenderer()} renderer={this._getRenderer()}
@@ -859,7 +869,7 @@ export default class App extends React.Component<any, AppState> {
onStyleOpen={this.onStyleChanged} onStyleOpen={this.onStyleChanged}
onSetMapState={this.setMapState} onSetMapState={this.setMapState}
onToggleModal={this.toggleModal.bind(this)} onToggleModal={this.toggleModal.bind(this)}
/> />;
const layerList = <LayerList const layerList = <LayerList
onMoveLayer={this.onMoveLayer} onMoveLayer={this.onMoveLayer}
@@ -872,7 +882,7 @@ export default class App extends React.Component<any, AppState> {
layers={layers} layers={layers}
sources={this.state.sources} sources={this.state.sources}
errors={this.state.errors} errors={this.state.errors}
/> />;
const layerEditor = selectedLayer ? <LayerEditor const layerEditor = selectedLayer ? <LayerEditor
key={this.state.selectedLayerOriginalId} key={this.state.selectedLayerOriginalId}
@@ -890,7 +900,7 @@ export default class App extends React.Component<any, AppState> {
onLayerVisibilityToggle={this.onLayerVisibilityToggle} onLayerVisibilityToggle={this.onLayerVisibilityToggle}
onLayerIdChange={this.onLayerIdChange} onLayerIdChange={this.onLayerIdChange}
errors={this.state.errors} errors={this.state.errors}
/> : undefined /> : undefined;
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
currentLayer={selectedLayer} currentLayer={selectedLayer}
@@ -899,7 +909,7 @@ export default class App extends React.Component<any, AppState> {
mapStyle={this.state.mapStyle} mapStyle={this.state.mapStyle}
errors={this.state.errors} errors={this.state.errors}
infos={this.state.infos} infos={this.state.infos}
/> : undefined /> : undefined;
const modals = <div> const modals = <div>
@@ -910,41 +920,47 @@ export default class App extends React.Component<any, AppState> {
onChangeMaplibreGlDebug={this.onChangeMaplibreGlDebug} onChangeMaplibreGlDebug={this.onChangeMaplibreGlDebug}
onChangeOpenlayersDebug={this.onChangeOpenlayersDebug} onChangeOpenlayersDebug={this.onChangeOpenlayersDebug}
isOpen={this.state.isOpen.debug} isOpen={this.state.isOpen.debug}
onOpenToggle={this.toggleModal.bind(this, 'debug')} onOpenToggle={() => this.toggleModal("debug")}
mapView={this.state.mapView} mapView={this.state.mapView}
/> />
<ModalShortcuts <ModalShortcuts
isOpen={this.state.isOpen.shortcuts} isOpen={this.state.isOpen.shortcuts}
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')} onOpenToggle={() => this.toggleModal("shortcuts")}
/> />
<ModalSettings <ModalSettings
mapStyle={this.state.mapStyle} mapStyle={this.state.mapStyle}
onStyleChanged={this.onStyleChanged} onStyleChanged={this.onStyleChanged}
onChangeMetadataProperty={this.onChangeMetadataProperty} onChangeMetadataProperty={this.onChangeMetadataProperty}
isOpen={this.state.isOpen.settings} isOpen={this.state.isOpen.settings}
onOpenToggle={this.toggleModal.bind(this, 'settings')} onOpenToggle={() => this.toggleModal("settings")}
/> />
<ModalExport <ModalExport
mapStyle={this.state.mapStyle} mapStyle={this.state.mapStyle}
onStyleChanged={this.onStyleChanged} onStyleChanged={this.onStyleChanged}
isOpen={this.state.isOpen.export} isOpen={this.state.isOpen.export}
onOpenToggle={this.toggleModal.bind(this, 'export')} onOpenToggle={() => this.toggleModal("export")}
fileHandle={this.state.fileHandle} fileHandle={this.state.fileHandle}
onSetFileHandle={this.onSetFileHandle} onSetFileHandle={this.onSetFileHandle}
/> />
<ModalOpen <ModalOpen
isOpen={this.state.isOpen.open} isOpen={this.state.isOpen.open}
onStyleOpen={this.openStyle} onStyleOpen={this.openStyle}
onOpenToggle={this.toggleModal.bind(this, 'open')} onOpenToggle={() => this.toggleModal("open")}
fileHandle={this.state.fileHandle} fileHandle={this.state.fileHandle}
/> />
<ModalSources <ModalSources
mapStyle={this.state.mapStyle} mapStyle={this.state.mapStyle}
onStyleChanged={this.onStyleChanged} onStyleChanged={this.onStyleChanged}
isOpen={this.state.isOpen.sources} isOpen={this.state.isOpen.sources}
onOpenToggle={this.toggleModal.bind(this, 'sources')} onOpenToggle={() => this.toggleModal("sources")}
/> />
</div> <ModalGlobalState
mapStyle={this.state.mapStyle}
onStyleChanged={this.onStyleChanged}
isOpen={this.state.isOpen.globalState}
onOpenToggle={() => this.toggleModal("globalState")}
/>
</div>;
return <AppLayout return <AppLayout
toolbar={toolbar} toolbar={toolbar}
@@ -953,6 +969,6 @@ export default class App extends React.Component<any, AppState> {
map={this.mapRenderer()} map={this.mapRenderer()}
bottom={bottomPanel} bottom={bottomPanel}
modals={modals} modals={modals}
/> />;
} }
} }
+6 -6
View File
@@ -1,7 +1,7 @@
import React from 'react' import React from "react";
import ScrollContainer from './ScrollContainer' import ScrollContainer from "./ScrollContainer";
import { WithTranslation, withTranslation } from 'react-i18next'; import { type WithTranslation, withTranslation } from "react-i18next";
import { IconContext } from 'react-icons'; import { IconContext } from "react-icons";
type AppLayoutInternalProps = { type AppLayoutInternalProps = {
toolbar: React.ReactElement toolbar: React.ReactElement
@@ -17,7 +17,7 @@ class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
render() { render() {
document.body.dir = this.props.i18n.dir(); document.body.dir = this.props.i18n.dir();
return <IconContext.Provider value={{size: '14px'}}> return <IconContext.Provider value={{size: "14px"}}>
<div className="maputnik-layout"> <div className="maputnik-layout">
{this.props.toolbar} {this.props.toolbar}
<div className="maputnik-layout-main"> <div className="maputnik-layout-main">
@@ -37,7 +37,7 @@ class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
} }
{this.props.modals} {this.props.modals}
</div> </div>
</IconContext.Provider> </IconContext.Provider>;
} }
} }
+11 -11
View File
@@ -1,13 +1,13 @@
import React from 'react' import React from "react";
import {formatLayerId} from '../libs/format'; import {formatLayerId} from "../libs/format";
import {LayerSpecification, StyleSpecification} from 'maplibre-gl'; import {type LayerSpecification, type StyleSpecification} from "maplibre-gl";
import { Trans, WithTranslation, withTranslation } from 'react-i18next'; import { Trans, type WithTranslation, withTranslation } from "react-i18next";
type AppMessagePanelInternalProps = { type AppMessagePanelInternalProps = {
errors?: unknown[] errors?: unknown[]
infos?: string[] infos?: string[]
mapStyle?: StyleSpecification mapStyle?: StyleSpecification
onLayerSelect?(...args: unknown[]): unknown onLayerSelect?(index: number): void;
currentLayer?: LayerSpecification currentLayer?: LayerSpecification
selectedLayerIndex?: number selectedLayerIndex?: number
} & WithTranslation; } & WithTranslation;
@@ -15,7 +15,7 @@ type AppMessagePanelInternalProps = {
class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalProps> { class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalProps> {
static defaultProps = { static defaultProps = {
onLayerSelect: () => {}, onLayerSelect: () => {},
} };
render() { render() {
const {t, selectedLayerIndex} = this.props; const {t, selectedLayerIndex} = this.props;
@@ -48,17 +48,17 @@ class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalPro
} }
return <p key={"error-"+idx} className="maputnik-message-panel-error"> return <p key={"error-"+idx} className="maputnik-message-panel-error">
{content} {content}
</p> </p>;
}) });
const infos = this.props.infos?.map((m, i) => { const infos = this.props.infos?.map((m, i) => {
return <p key={"info-"+i}>{m}</p> return <p key={"info-"+i}>{m}</p>;
}) });
return <div className="maputnik-message-panel"> return <div className="maputnik-message-panel">
{errors} {errors}
{infos} {infos}
</div> </div>;
} }
} }
+29 -24
View File
@@ -1,6 +1,6 @@
import React from 'react' import React from "react";
import classnames from 'classnames' import classnames from "classnames";
import {detect} from 'detect-browser'; import {detect} from "detect-browser";
import { import {
MdOpenInBrowser, MdOpenInBrowser,
@@ -9,18 +9,19 @@ import {
MdHelpOutline, MdHelpOutline,
MdFindInPage, MdFindInPage,
MdLanguage, MdLanguage,
MdSave MdSave,
} from 'react-icons/md' MdPublic
import pkgJson from '../../package.json' } from "react-icons/md";
import pkgJson from "../../package.json";
//@ts-ignore //@ts-ignore
import maputnikLogo from 'maputnik-design/logos/logo-color.svg?inline' import maputnikLogo from "maputnik-design/logos/logo-color.svg?inline";
import { withTranslation, WithTranslation } from 'react-i18next'; import { withTranslation, type WithTranslation } from "react-i18next";
import { supportedLanguages } from '../i18n'; import { supportedLanguages } from "../i18n";
import type { OnStyleChangedCallback } from '../libs/definitions'; import type { OnStyleChangedCallback } from "../libs/definitions";
// 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;
type IconTextProps = { type IconTextProps = {
@@ -30,7 +31,7 @@ type IconTextProps = {
class IconText extends React.Component<IconTextProps> { class IconText extends React.Component<IconTextProps> {
render() { render() {
return <span className="maputnik-icon-text">{this.props.children}</span> return <span className="maputnik-icon-text">{this.props.children}</span>;
} }
} }
@@ -44,14 +45,14 @@ type ToolbarLinkProps = {
class ToolbarLink extends React.Component<ToolbarLinkProps> { class ToolbarLink extends React.Component<ToolbarLinkProps> {
render() { render() {
return <a return <a
className={classnames('maputnik-toolbar-link', this.props.className)} className={classnames("maputnik-toolbar-link", this.props.className)}
href={this.props.href} href={this.props.href}
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
data-wd-key="toolbar:link" data-wd-key="toolbar:link"
> >
{this.props.children} {this.props.children}
</a> </a>;
} }
} }
@@ -67,7 +68,7 @@ class ToolbarSelect extends React.Component<ToolbarSelectProps> {
data-wd-key={this.props.wdKey} data-wd-key={this.props.wdKey}
> >
{this.props.children} {this.props.children}
</div> </div>;
} }
} }
@@ -85,7 +86,7 @@ class ToolbarAction extends React.Component<ToolbarActionProps> {
onClick={this.props.onClick} onClick={this.props.onClick}
> >
{this.props.children} {this.props.children}
</button> </button>;
} }
} }
@@ -115,7 +116,7 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
add: false, add: false,
export: false, export: false,
} }
} };
handleSelection(val: MapState) { handleSelection(val: MapState) {
this.props.onSetMapState(val); this.props.onSetMapState(val);
@@ -133,7 +134,7 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
const el = document.querySelector("#skip-target-"+target) as HTMLButtonElement; const el = document.querySelector("#skip-target-"+target) as HTMLButtonElement;
el.focus(); el.focus();
} }
} };
render() { render() {
const t = this.props.t; const t = this.props.t;
@@ -147,7 +148,7 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
id: "inspect", id: "inspect",
group: "general", group: "general",
title: t("Inspect"), title: t("Inspect"),
disabled: this.props.renderer === 'ol', disabled: this.props.renderer === "ol",
}, },
{ {
id: "filter-deuteranopia", id: "filter-deuteranopia",
@@ -220,22 +221,26 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
</a> </a>
</div> </div>
<div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar"> <div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar">
<ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}> <ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, "open")}>
<MdOpenInBrowser /> <MdOpenInBrowser />
<IconText>{t("Open")}</IconText> <IconText>{t("Open")}</IconText>
</ToolbarAction> </ToolbarAction>
<ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, 'export')}> <ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, "export")}>
<MdSave /> <MdSave />
<IconText>{t("Save")}</IconText> <IconText>{t("Save")}</IconText>
</ToolbarAction> </ToolbarAction>
<ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, 'sources')}> <ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, "sources")}>
<MdLayers /> <MdLayers />
<IconText>{t("Data Sources")}</IconText> <IconText>{t("Data Sources")}</IconText>
</ToolbarAction> </ToolbarAction>
<ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}> <ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, "settings")}>
<MdSettings /> <MdSettings />
<IconText>{t("Style Settings")}</IconText> <IconText>{t("Style Settings")}</IconText>
</ToolbarAction> </ToolbarAction>
<ToolbarAction wdKey="nav:global-state" onClick={this.props.onToggleModal.bind(this, "globalState")}>
<MdPublic />
<IconText>{t("Global State")}</IconText>
</ToolbarAction>
<ToolbarSelect wdKey="nav:inspect"> <ToolbarSelect wdKey="nav:inspect">
<MdFindInPage /> <MdFindInPage />
@@ -292,7 +297,7 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
</ToolbarLink> </ToolbarLink>
</div> </div>
</div> </div>
</nav> </nav>;
} }
} }
+12 -13
View File
@@ -1,8 +1,7 @@
import React, {PropsWithChildren, SyntheticEvent} from 'react' import React, {type PropsWithChildren, type SyntheticEvent} from "react";
import classnames from 'classnames' import classnames from "classnames";
import FieldDocLabel from './FieldDocLabel' import FieldDocLabel from "./FieldDocLabel";
import Doc from './Doc' import Doc from "./Doc";
type BlockProps = PropsWithChildren & { type BlockProps = PropsWithChildren & {
"data-wd-key"?: string "data-wd-key"?: string
@@ -27,13 +26,13 @@ export default class Block extends React.Component<BlockProps, BlockState> {
super(props); super(props);
this.state = { this.state = {
showDoc: false, showDoc: false,
} };
} }
onChange(e: React.BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) { onChange(e: React.BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) {
const value = e.target.value const value = e.target.value;
if (this.props.onChange) { if (this.props.onChange) {
return this.props.onChange(value === "" ? undefined : value) return this.props.onChange(value === "" ? undefined : value);
} }
} }
@@ -41,7 +40,7 @@ export default class Block extends React.Component<BlockProps, BlockState> {
this.setState({ this.setState({
showDoc: val showDoc: val
}); });
} };
/** /**
* Some fields for example <InputColor/> bind click events inside the element * Some fields for example <InputColor/> bind click events inside the element
@@ -59,7 +58,7 @@ export default class Block extends React.Component<BlockProps, BlockState> {
if (event.nativeEvent.target.nodeName !== "A") { if (event.nativeEvent.target.nodeName !== "A") {
event.preventDefault(); event.preventDefault();
} }
} };
render() { render() {
return <label style={this.props.style} return <label style={this.props.style}
@@ -88,17 +87,17 @@ export default class Block extends React.Component<BlockProps, BlockState> {
<div className="maputnik-input-block-action"> <div className="maputnik-input-block-action">
{this.props.action} {this.props.action}
</div> </div>
<div className="maputnik-input-block-content" ref={el => this._blockEl = el}> <div className="maputnik-input-block-content" ref={el => {this._blockEl = el;}}>
{this.props.children} {this.props.children}
</div> </div>
{this.props.fieldSpec && {this.props.fieldSpec &&
<div <div
className="maputnik-doc-inline" className="maputnik-doc-inline"
style={{display: this.state.showDoc ? '' : 'none'}} style={{display: this.state.showDoc ? "" : "none"}}
> >
<Doc fieldSpec={this.props.fieldSpec} /> <Doc fieldSpec={this.props.fieldSpec} />
</div> </div>
} }
</label> </label>;
} }
} }
+6 -6
View File
@@ -1,6 +1,6 @@
import React from 'react' import React from "react";
import { Collapse as ReactCollapse } from 'react-collapse' import { Collapse as ReactCollapse } from "react-collapse";
import {reducedMotionEnabled} from '../libs/accessibility' import {reducedMotionEnabled} from "../libs/accessibility";
type CollapseProps = { type CollapseProps = {
@@ -12,7 +12,7 @@ type CollapseProps = {
export default class Collapse extends React.Component<CollapseProps> { export default class Collapse extends React.Component<CollapseProps> {
static defaultProps = { static defaultProps = {
isActive: true isActive: true
} };
render() { render() {
if (reducedMotionEnabled()) { if (reducedMotionEnabled()) {
@@ -20,14 +20,14 @@ export default class Collapse extends React.Component<CollapseProps> {
<div style={{display: this.props.isActive ? "block" : "none"}}> <div style={{display: this.props.isActive ? "block" : "none"}}>
{this.props.children} {this.props.children}
</div> </div>
) );
} }
else { else {
return ( return (
<ReactCollapse isOpened={this.props.isActive}> <ReactCollapse isOpened={this.props.isActive}>
{this.props.children} {this.props.children}
</ReactCollapse> </ReactCollapse>
) );
} }
} }
} }
+4 -4
View File
@@ -1,5 +1,5 @@
import React from 'react' import React from "react";
import {MdArrowDropDown, MdArrowDropUp} from 'react-icons/md' import {MdArrowDropDown, MdArrowDropUp} from "react-icons/md";
type CollapserProps = { type CollapserProps = {
isCollapsed: boolean isCollapsed: boolean
@@ -12,7 +12,7 @@ export default class Collapser extends React.Component<CollapserProps> {
width: 20, width: 20,
height: 20, height: 20,
...this.props.style, ...this.props.style,
} };
return this.props.isCollapsed ? <MdArrowDropUp style={iconStyle}/> : <MdArrowDropDown style={iconStyle} /> return this.props.isCollapsed ? <MdArrowDropUp style={iconStyle}/> : <MdArrowDropDown style={iconStyle} />;
} }
} }
+19 -7
View File
@@ -1,10 +1,10 @@
import React from 'react' import React from "react";
import Markdown from "react-markdown";
const headers = { const headers = {
js: "JS", js: "JS",
android: "Android", android: "Android",
ios: "iOS", ios: "iOS"
macos: "macOS",
}; };
type DocProps = { type DocProps = {
@@ -15,7 +15,7 @@ type DocProps = {
doc?: string doc?: string
} }
} }
'sdk-support'?: { "sdk-support"?: {
[key: string]: typeof headers [key: string]: typeof headers
} }
docUrl?: string, docUrl?: string,
@@ -28,7 +28,7 @@ export default class Doc extends React.Component<DocProps> {
const {fieldSpec} = this.props; const {fieldSpec} = this.props;
const {doc, values, docUrl, docUrlLinkText} = fieldSpec; const {doc, values, docUrl, docUrlLinkText} = fieldSpec;
const sdkSupport = fieldSpec['sdk-support']; const sdkSupport = fieldSpec["sdk-support"];
const renderValues = ( const renderValues = (
!!values && !!values &&
@@ -37,11 +37,23 @@ export default class Doc extends React.Component<DocProps> {
!Array.isArray(values) !Array.isArray(values)
); );
const sdkSupportToJsx = (value: string) => {
const supportValue = value.toLowerCase();
if (supportValue.startsWith("https://")) {
return <a href={supportValue} target="_blank" rel="noreferrer">{"#" + supportValue.split("/").pop()}</a>;
}
return value;
};
return ( return (
<> <>
{doc && {doc &&
<div className="SpecDoc"> <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 && {renderValues &&
<ul className="SpecDoc__values"> <ul className="SpecDoc__values">
{Object.entries(values).map(([key, value]) => { {Object.entries(values).map(([key, value]) => {
@@ -74,7 +86,7 @@ export default class Doc extends React.Component<DocProps> {
<td>{key}</td> <td>{key}</td>
{Object.keys(headers).map((k) => { {Object.keys(headers).map((k) => {
if (Object.prototype.hasOwnProperty.call(supportObj, k)) { if (Object.prototype.hasOwnProperty.call(supportObj, k)) {
return <td key={k}>{supportObj[k as keyof typeof headers]}</td>; return <td key={k}>{sdkSupportToJsx(supportObj[k as keyof typeof headers])}</td>;
} }
else { else {
return <td key={k}>no</td>; return <td key={k}>no</td>;
+2 -2
View File
@@ -1,5 +1,5 @@
import InputArray, { FieldArrayProps as InputArrayProps } from './InputArray' import InputArray, { type InputArrayProps } from "./InputArray";
import Fieldset from './Fieldset' import Fieldset from "./Fieldset";
type FieldArrayProps = InputArrayProps & { type FieldArrayProps = InputArrayProps & {
name?: string name?: string
+2 -2
View File
@@ -1,5 +1,5 @@
import Block from './Block' import Block from "./Block";
import InputAutocomplete, { InputAutocompleteProps } from './InputAutocomplete' import InputAutocomplete, { type InputAutocompleteProps } from "./InputAutocomplete";
type FieldAutocompleteProps = InputAutocompleteProps & { type FieldAutocompleteProps = InputAutocompleteProps & {
+2 -2
View File
@@ -1,5 +1,5 @@
import Block from './Block' import Block from "./Block";
import InputCheckbox, {InputCheckboxProps} from './InputCheckbox' import InputCheckbox, {type InputCheckboxProps} from "./InputCheckbox";
type FieldCheckboxProps = InputCheckboxProps & { type FieldCheckboxProps = InputCheckboxProps & {
+2 -2
View File
@@ -1,5 +1,5 @@
import Block from './Block' import Block from "./Block";
import InputColor, {InputColorProps} from './InputColor' import InputColor, {type InputColorProps} from "./InputColor";
type FieldColorProps = InputColorProps & { type FieldColorProps = InputColorProps & {
+4 -4
View File
@@ -1,8 +1,8 @@
import React from 'react' import React from "react";
import Block from './Block' import Block from "./Block";
import InputString from './InputString' import InputString from "./InputString";
import { WithTranslation, withTranslation } from 'react-i18next'; import { type WithTranslation, withTranslation } from "react-i18next";
type FieldCommentInternalProps = { type FieldCommentInternalProps = {
value?: string value?: string
+6 -6
View File
@@ -1,5 +1,5 @@
import React from 'react' import React, { type JSX } from "react";
import {MdInfoOutline, MdHighlightOff} from 'react-icons/md' import {MdInfoOutline, MdHighlightOff} from "react-icons/md";
type FieldDocLabelProps = { type FieldDocLabelProps = {
label: JSX.Element | string | undefined label: JSX.Element | string | undefined
@@ -28,12 +28,12 @@ const FieldDocLabel: React.FC<FieldDocLabelProps> = (props) => {
<label className="maputnik-doc-wrapper"> <label className="maputnik-doc-wrapper">
<div className="maputnik-doc-target"> <div className="maputnik-doc-target">
{label} {label}
{'\xa0'} {"\xa0"}
<button <button
aria-label={open ? 'close property documentation' : 'open property documentation'} aria-label={open ? "close property documentation" : "open property documentation"}
className={`maputnik-doc-button maputnik-doc-button--${open ? 'open' : 'closed'}`} className={`maputnik-doc-button maputnik-doc-button--${open ? "open" : "closed"}`}
onClick={() => onToggleDoc(!open)} onClick={() => onToggleDoc(!open)}
data-wd-key={'field-doc-button-' + label} data-wd-key={"field-doc-button-" + label}
> >
{open ? <MdHighlightOff /> : <MdInfoOutline />} {open ? <MdHighlightOff /> : <MdInfoOutline />}
</button> </button>
+2 -2
View File
@@ -1,5 +1,5 @@
import InputDynamicArray, {FieldDynamicArrayProps as InputDynamicArrayProps} from './InputDynamicArray' import InputDynamicArray, {type InputDynamicArrayProps} from "./InputDynamicArray";
import Fieldset from './Fieldset' import Fieldset from "./Fieldset";
type FieldDynamicArrayProps = InputDynamicArrayProps & { type FieldDynamicArrayProps = InputDynamicArrayProps & {
name?: string name?: string
+2 -2
View File
@@ -1,5 +1,5 @@
import InputEnum, {InputEnumProps} from './InputEnum' import InputEnum, {type InputEnumProps} from "./InputEnum";
import Fieldset from './Fieldset'; import Fieldset from "./Fieldset";
type FieldEnumProps = InputEnumProps & { type FieldEnumProps = InputEnumProps & {
+64 -37
View File
@@ -1,11 +1,11 @@
import React from 'react' import React from "react";
import SpecProperty from './_SpecProperty' import SpecProperty from "./_SpecProperty";
import DataProperty, { Stop } from './_DataProperty' import DataProperty, { type Stop } from "./_DataProperty";
import ZoomProperty from './_ZoomProperty' import ZoomProperty from "./_ZoomProperty";
import ExpressionProperty from './_ExpressionProperty' import ExpressionProperty from "./_ExpressionProperty";
import {function as styleFunction} from '@maplibre/maplibre-gl-style-spec'; import {function as styleFunction} from "@maplibre/maplibre-gl-style-spec";
import {findDefaultFromSpec} from '../libs/spec-helper'; import {findDefaultFromSpec} from "../libs/spec-helper";
function isLiteralExpression(value: any) { function isLiteralExpression(value: any) {
@@ -22,9 +22,9 @@ function isGetExpression(value: any) {
function isZoomField(value: any) { function isZoomField(value: any) {
return ( return (
typeof(value) === 'object' && typeof(value) === "object" &&
value.stops && value.stops &&
typeof(value.property) === 'undefined' && typeof(value.property) === "undefined" &&
Array.isArray(value.stops) && Array.isArray(value.stops) &&
value.stops.length > 1 && value.stops.length > 1 &&
value.stops.every((stop: Stop) => { value.stops.every((stop: Stop) => {
@@ -38,7 +38,7 @@ function isZoomField(value: any) {
function isIdentityProperty(value: any) { function isIdentityProperty(value: any) {
return ( return (
typeof(value) === 'object' && typeof(value) === "object" &&
value.type === "identity" && value.type === "identity" &&
Object.prototype.hasOwnProperty.call(value, "property") Object.prototype.hasOwnProperty.call(value, "property")
); );
@@ -46,16 +46,16 @@ function isIdentityProperty(value: any) {
function isDataStopProperty(value: any) { function isDataStopProperty(value: any) {
return ( return (
typeof(value) === 'object' && typeof(value) === "object" &&
value.stops && value.stops &&
typeof(value.property) !== 'undefined' && typeof(value.property) !== "undefined" &&
value.stops.length > 1 && value.stops.length > 1 &&
Array.isArray(value.stops) && Array.isArray(value.stops) &&
value.stops.every((stop: Stop) => { value.stops.every((stop: Stop) => {
return ( return (
Array.isArray(stop) && Array.isArray(stop) &&
stop.length === 2 && stop.length === 2 &&
typeof(stop[0]) === 'object' typeof(stop[0]) === "object"
); );
}) })
); );
@@ -90,6 +90,18 @@ function getDataType(value: any, fieldSpec={} as any) {
else if (fieldSpec.type === "array" && isArrayOfPrimatives(value)) { else if (fieldSpec.type === "array" && isArrayOfPrimatives(value)) {
return "value"; return "value";
} }
else if (fieldSpec.type === "numberArray" && isArrayOfPrimatives(value)) {
return "value";
}
else if (fieldSpec.type === "colorArray") {
return "value";
}
else if (fieldSpec.type === "padding") {
return "value";
}
else if (fieldSpec.type === "variableAnchorOffsetCollection") {
return "value";
}
else if (isZoomField(value)) { else if (isZoomField(value)) {
return "zoom_function"; return "zoom_function";
} }
@@ -129,18 +141,18 @@ const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
const getFieldFunctionType = (fieldSpec: any) => { const getFieldFunctionType = (fieldSpec: any) => {
if (fieldSpec.expression.interpolated) { if (fieldSpec.expression.interpolated) {
return 'exponential'; return "exponential";
} }
if (fieldSpec.type === 'number') { if (fieldSpec.type === "number") {
return 'interval'; return "interval";
} }
return 'categorical'; return "categorical";
}; };
const addStop = () => { const addStop = () => {
const stops = props.value.stops.slice(0); const stops = props.value.stops.slice(0);
const lastStop = stops[stops.length - 1]; const lastStop = stops[stops.length - 1];
if (typeof lastStop[0] === 'object') { if (typeof lastStop[0] === "object") {
stops.push([ stops.push([
{ zoom: lastStop[0].zoom + 1, value: lastStop[0].value }, { zoom: lastStop[0].zoom + 1, value: lastStop[0].value },
lastStop[1], lastStop[1],
@@ -160,7 +172,7 @@ const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
const deleteExpression = () => { const deleteExpression = () => {
const { fieldSpec, fieldName } = props; const { fieldSpec, fieldName } = props;
props.onChange(fieldName, fieldSpec.default); props.onChange(fieldName, fieldSpec.default);
setDataType('value'); setDataType("value");
}; };
const deleteStop = (stopIdx: number) => { const deleteStop = (stopIdx: number) => {
@@ -183,7 +195,7 @@ const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
const { value } = props; const { value } = props;
let zoomFunc: any; let zoomFunc: any;
if (typeof value === 'object') { if (typeof value === "object") {
if (value.stops) { if (value.stops) {
zoomFunc = { zoomFunc = {
base: value.base, base: value.base,
@@ -217,13 +229,13 @@ const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
if (isGetExpression(value)) { if (isGetExpression(value)) {
props.onChange(fieldName, { props.onChange(fieldName, {
type: 'identity', type: "identity",
property: value[1], property: value[1],
}); });
setDataType('value'); setDataType("value");
} else if (isLiteralExpression(value)) { } else if (isLiteralExpression(value)) {
props.onChange(fieldName, value[1]); props.onChange(fieldName, value[1]);
setDataType('value'); setDataType("value");
} }
}; };
@@ -233,7 +245,7 @@ const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
isGetExpression(value) || isGetExpression(value) ||
isLiteralExpression(value) || isLiteralExpression(value) ||
isPrimative(value) || isPrimative(value) ||
(Array.isArray(value) && fieldSpec.type === 'array') (Array.isArray(value) && fieldSpec.type === "array")
); );
}; };
@@ -241,26 +253,26 @@ const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
const { value, fieldSpec } = props; const { value, fieldSpec } = props;
let expression; let expression;
if (typeof value === 'object' && 'stops' in value) { if (typeof value === "object" && "stops" in value) {
expression = styleFunction.convertFunction(value, fieldSpec); expression = styleFunction.convertFunction(value, fieldSpec);
} else if (isIdentityProperty(value)) { } else if (isIdentityProperty(value)) {
expression = ['get', value.property]; expression = ["get", value.property];
} else { } else {
expression = ['literal', value || props.fieldSpec.default]; expression = ["literal", value || props.fieldSpec.default];
} }
props.onChange(props.fieldName, expression); props.onChange(props.fieldName, expression);
}; };
const makeDataFunction = () => { const makeDataFunction = () => {
const functionType = getFieldFunctionType(props.fieldSpec); const functionType = getFieldFunctionType(props.fieldSpec);
const stopValue = functionType === 'categorical' ? '' : 0; const stopValue = functionType === "categorical" ? "" : 0;
const { value } = props; const { value } = props;
let dataFunc; let dataFunc;
if (typeof value === 'object') { if (typeof value === "object") {
if (value.stops) { if (value.stops) {
dataFunc = { dataFunc = {
property: '', property: "",
type: functionType, type: functionType,
base: value.base, base: value.base,
stops: value.stops.map((stop: Stop) => { stops: value.stops.map((stop: Stop) => {
@@ -269,7 +281,7 @@ const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
}; };
} else { } else {
dataFunc = { dataFunc = {
property: '', property: "",
type: functionType, type: functionType,
base: value.base, base: value.base,
stops: [ stops: [
@@ -280,7 +292,7 @@ const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
} }
} else { } else {
dataFunc = { dataFunc = {
property: '', property: "",
type: functionType, type: functionType,
base: value.base, base: value.base,
stops: [ stops: [
@@ -293,6 +305,20 @@ const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
props.onChange(props.fieldName, dataFunc); props.onChange(props.fieldName, dataFunc);
}; };
const makeElevationFunction = () => {
const expression = [
"interpolate",
["linear"],
["elevation"],
0,
"black",
2000,
"white"
];
props.onChange(props.fieldName, expression);
};
const onMarkEditing = () => { const onMarkEditing = () => {
setIsEditing(true); setIsEditing(true);
}; };
@@ -302,11 +328,11 @@ const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
}; };
const propClass = const propClass =
props.fieldSpec.default === props.value ? 'maputnik-default-property' : 'maputnik-modified-property'; props.fieldSpec.default === props.value ? "maputnik-default-property" : "maputnik-modified-property";
let specField; let specField;
if (dataType === 'expression') { if (dataType === "expression") {
specField = ( specField = (
<ExpressionProperty <ExpressionProperty
errors={props.errors} errors={props.errors}
@@ -322,7 +348,7 @@ const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
onBlur={onUnmarkEditing} onBlur={onUnmarkEditing}
/> />
); );
} else if (dataType === 'zoom_function') { } else if (dataType === "zoom_function") {
specField = ( specField = (
<ZoomProperty <ZoomProperty
errors={props.errors} errors={props.errors}
@@ -337,7 +363,7 @@ const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
onExpressionClick={makeExpression} onExpressionClick={makeExpression}
/> />
); );
} else if (dataType === 'data_function') { } else if (dataType === "data_function") {
specField = ( specField = (
<DataProperty <DataProperty
errors={props.errors} errors={props.errors}
@@ -364,12 +390,13 @@ const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
onZoomClick={makeZoomFunction} onZoomClick={makeZoomFunction}
onDataClick={makeDataFunction} onDataClick={makeDataFunction}
onExpressionClick={makeExpression} onExpressionClick={makeExpression}
onElevationClick={makeElevationFunction}
/> />
); );
} }
return ( return (
<div className={propClass} data-wd-key={'spec-field-container:' + props.fieldName}> <div className={propClass} data-wd-key={"spec-field-container:" + props.fieldName}>
{specField} {specField}
</div> </div>
); );
+3 -3
View File
@@ -1,7 +1,7 @@
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json' import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import Block from './Block' import Block from "./Block";
import InputString from './InputString' import InputString from "./InputString";
type FieldIdProps = { type FieldIdProps = {
value: string value: string
+1 -1
View File
@@ -1,4 +1,4 @@
import InputJson, {InputJsonProps} from './InputJson' import InputJson, {type InputJsonProps} from "./InputJson";
type FieldJsonProps = InputJsonProps & {}; type FieldJsonProps = InputJsonProps & {};
+6 -6
View File
@@ -1,9 +1,9 @@
import React from 'react' import React from "react";
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json' import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import Block from './Block' import Block from "./Block";
import InputNumber from './InputNumber' import InputNumber from "./InputNumber";
import { WithTranslation, withTranslation } from 'react-i18next'; import { type WithTranslation, withTranslation } from "react-i18next";
type FieldMaxZoomInternalProps = { type FieldMaxZoomInternalProps = {
value?: number value?: number
@@ -14,7 +14,7 @@ type FieldMaxZoomInternalProps = {
const FieldMaxZoomInternal: React.FC<FieldMaxZoomInternalProps> = (props) => { const FieldMaxZoomInternal: React.FC<FieldMaxZoomInternalProps> = (props) => {
const t = props.t; const t = props.t;
return ( return (
<Block label={t('Max Zoom')} fieldSpec={latest.layer.maxzoom} <Block label={t("Max Zoom")} fieldSpec={latest.layer.maxzoom}
error={props.error} error={props.error}
data-wd-key="max-zoom" data-wd-key="max-zoom"
> >
+6 -6
View File
@@ -1,9 +1,9 @@
import React from 'react' import React from "react";
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json' import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import Block from './Block' import Block from "./Block";
import InputNumber from './InputNumber' import InputNumber from "./InputNumber";
import { WithTranslation, withTranslation } from 'react-i18next'; import { type WithTranslation, withTranslation } from "react-i18next";
type FieldMinZoomInternalProps = { type FieldMinZoomInternalProps = {
value?: number value?: number
@@ -14,7 +14,7 @@ type FieldMinZoomInternalProps = {
const FieldMinZoomInternal: React.FC<FieldMinZoomInternalProps> = (props) => { const FieldMinZoomInternal: React.FC<FieldMinZoomInternalProps> = (props) => {
const t = props.t; const t = props.t;
return ( return (
<Block label={t('Min Zoom')} fieldSpec={latest.layer.minzoom} <Block label={t("Min Zoom")} fieldSpec={latest.layer.minzoom}
error={props.error} error={props.error}
data-wd-key="min-zoom" data-wd-key="min-zoom"
> >
+2 -2
View File
@@ -1,5 +1,5 @@
import InputMultiInput, {InputMultiInputProps} from './InputMultiInput' import InputMultiInput, {type InputMultiInputProps} from "./InputMultiInput";
import Fieldset from './Fieldset' import Fieldset from "./Fieldset";
type FieldMultiInputProps = InputMultiInputProps & { type FieldMultiInputProps = InputMultiInputProps & {
+2 -2
View File
@@ -1,5 +1,5 @@
import InputNumber, {InputNumberProps} from './InputNumber' import InputNumber, {type InputNumberProps} from "./InputNumber";
import Block from './Block' import Block from "./Block";
type FieldNumberProps = InputNumberProps & { type FieldNumberProps = InputNumberProps & {
+2 -2
View File
@@ -1,5 +1,5 @@
import Block from './Block' import Block from "./Block";
import InputSelect, {InputSelectProps} from './InputSelect' import InputSelect, {type InputSelectProps} from "./InputSelect";
type FieldSelectProps = InputSelectProps & { type FieldSelectProps = InputSelectProps & {
+6 -6
View File
@@ -1,9 +1,9 @@
import React from 'react' import React from "react";
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json' import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import Block from './Block' import Block from "./Block";
import InputAutocomplete from './InputAutocomplete' import InputAutocomplete from "./InputAutocomplete";
import { WithTranslation, withTranslation } from 'react-i18next'; import { type WithTranslation, withTranslation } from "react-i18next";
type FieldSourceInternalProps = { type FieldSourceInternalProps = {
value?: string value?: string
@@ -23,7 +23,7 @@ const FieldSourceInternal: React.FC<FieldSourceInternalProps> = ({
}) => { }) => {
return ( return (
<Block <Block
label={t('Source')} label={t("Source")}
fieldSpec={latest.layer.source} fieldSpec={latest.layer.source}
error={error} error={error}
data-wd-key={wdKey} data-wd-key={wdKey}
+7 -7
View File
@@ -1,9 +1,9 @@
import React from 'react' import React from "react";
import {latest} from '@maplibre/maplibre-gl-style-spec' import {latest} from "@maplibre/maplibre-gl-style-spec";
import Block from './Block' import Block from "./Block";
import InputAutocomplete from './InputAutocomplete' import InputAutocomplete from "./InputAutocomplete";
import { WithTranslation, withTranslation } from 'react-i18next'; import { type WithTranslation, withTranslation } from "react-i18next";
type FieldSourceLayerInternalProps = { type FieldSourceLayerInternalProps = {
value?: string value?: string
@@ -21,8 +21,8 @@ const FieldSourceLayerInternal: React.FC<FieldSourceLayerInternalProps> = ({
}) => { }) => {
return ( return (
<Block <Block
label={t('Source Layer')} label={t("Source Layer")}
fieldSpec={latest.layer['source-layer']} fieldSpec={latest.layer["source-layer"]}
data-wd-key="layer-source-layer" data-wd-key="layer-source-layer"
error={error} error={error}
> >
+51
View File
@@ -0,0 +1,51 @@
import Block from "./Block";
import InputSpec, { type FieldSpecType, type InputSpecProps } from "./InputSpec";
import Fieldset from "./Fieldset";
function getElementFromType(fieldSpec: { type?: FieldSpecType, values?: unknown[] }): typeof Fieldset | typeof Block {
switch(fieldSpec.type) {
case "color":
return Block;
case "enum":
return (Object.keys(fieldSpec.values!).length <= 3 ? Fieldset : Block);
case "boolean":
return Block;
case "array":
return Fieldset;
case "resolvedImage":
return Block;
case "number":
return Block;
case "string":
return Block;
case "formatted":
return Block;
case "padding":
return Block;
case "numberArray":
return Fieldset;
case "colorArray":
return Fieldset;
case "variableAnchorOffsetCollection":
return Fieldset;
default:
console.warn("No such type for: " + fieldSpec.type);
return Block;
}
}
export type FieldSpecProps = InputSpecProps & {
name?: string
};
const FieldSpec: React.FC<FieldSpecProps> = (props) => {
const TypeBlock = getElementFromType(props.fieldSpec!);
return (
<TypeBlock label={props.label} action={props.action} fieldSpec={props.fieldSpec}>
<InputSpec {...props} />
</TypeBlock>
);
};
export default FieldSpec;
+2 -2
View File
@@ -1,5 +1,5 @@
import Block from './Block' import Block from "./Block";
import InputString, {InputStringProps} from './InputString' import InputString, {type InputStringProps} from "./InputString";
type FieldStringProps = InputStringProps & { type FieldStringProps = InputStringProps & {
name?: string name?: string
+10 -10
View File
@@ -1,10 +1,10 @@
import React from 'react' import React from "react";
import {v8} from '@maplibre/maplibre-gl-style-spec' import {v8} from "@maplibre/maplibre-gl-style-spec";
import Block from './Block' import Block from "./Block";
import InputSelect from './InputSelect' import InputSelect from "./InputSelect";
import InputString from './InputString' import InputString from "./InputString";
import { WithTranslation, withTranslation } from 'react-i18next'; import { type WithTranslation, withTranslation } from "react-i18next";
import { startCase } from 'lodash' import { startCase } from "lodash";
type FieldTypeInternalProps = { type FieldTypeInternalProps = {
value: string value: string
@@ -22,9 +22,9 @@ const FieldTypeInternal: React.FC<FieldTypeInternalProps> = ({
error, error,
disabled = false disabled = false
}) => { }) => {
const layerstypes: [string, string][] = Object.keys(v8.layer.type.values || {}).map(v => [v, startCase(v.replace(/-/g, ' '))]); const layerstypes: [string, string][] = Object.keys(v8.layer.type.values || {}).map(v => [v, startCase(v.replace(/-/g, " "))]);
return ( return (
<Block label={t('Type')} fieldSpec={v8.layer.type} <Block label={t("Type")} fieldSpec={v8.layer.type}
data-wd-key={wdKey} data-wd-key={wdKey}
error={error} error={error}
> >
@@ -36,7 +36,7 @@ const FieldTypeInternal: React.FC<FieldTypeInternalProps> = ({
options={layerstypes} options={layerstypes}
onChange={onChange} onChange={onChange}
value={value} value={value}
data-wd-key={wdKey + '.select'} data-wd-key={wdKey + ".select"}
/> />
)} )}
</Block> </Block>
+2 -2
View File
@@ -1,5 +1,5 @@
import InputUrl, {FieldUrlProps as InputUrlProps} from './InputUrl' import InputUrl, {type FieldUrlProps as InputUrlProps} from "./InputUrl";
import Block from './Block' import Block from "./Block";
type FieldUrlProps = InputUrlProps & { type FieldUrlProps = InputUrlProps & {
+6 -6
View File
@@ -1,7 +1,7 @@
import React, { PropsWithChildren, ReactElement } from 'react' import React, { type PropsWithChildren, type ReactElement } from "react";
import FieldDocLabel from './FieldDocLabel' import FieldDocLabel from "./FieldDocLabel";
import Doc from './Doc' import Doc from "./Doc";
import generateUniqueId from '../libs/document-uid'; import generateUniqueId from "../libs/document-uid";
type FieldsetProps = PropsWithChildren & { type FieldsetProps = PropsWithChildren & {
label?: string, label?: string,
@@ -12,7 +12,7 @@ type FieldsetProps = PropsWithChildren & {
const Fieldset: React.FC<FieldsetProps> = (props) => { const Fieldset: React.FC<FieldsetProps> = (props) => {
const [showDoc, setShowDoc] = React.useState(false); const [showDoc, setShowDoc] = React.useState(false);
const labelId = React.useRef(generateUniqueId('fieldset_label_')); const labelId = React.useRef(generateUniqueId("fieldset_label_"));
const onToggleDoc = (val: boolean) => { const onToggleDoc = (val: boolean) => {
setShowDoc(val); setShowDoc(val);
@@ -37,7 +37,7 @@ const Fieldset: React.FC<FieldsetProps> = (props) => {
<div className="maputnik-input-block-action">{props.action}</div> <div className="maputnik-input-block-action">{props.action}</div>
<div className="maputnik-input-block-content">{props.children}</div> <div className="maputnik-input-block-content">{props.children}</div>
{props.fieldSpec && ( {props.fieldSpec && (
<div className="maputnik-doc-inline" style={{ display: showDoc ? '' : 'none' }}> <div className="maputnik-doc-inline" style={{ display: showDoc ? "" : "none" }}>
<Doc fieldSpec={props.fieldSpec} /> <Doc fieldSpec={props.fieldSpec} />
</div> </div>
)} )}
+47 -52
View File
@@ -1,24 +1,24 @@
import React from 'react' import React from "react";
import {mdiTableRowPlusAfter} from '@mdi/js'; import { TbMathFunction } from "react-icons/tb";
import {isEqual} from 'lodash'; import { PiListPlusBold } from "react-icons/pi";
import {ExpressionSpecification, LegacyFilterSpecification} from 'maplibre-gl' import {isEqual} from "lodash";
import {latest, migrate, convertFilter} from '@maplibre/maplibre-gl-style-spec' import {type ExpressionSpecification, type LegacyFilterSpecification} from "maplibre-gl";
import {mdiFunctionVariant} from '@mdi/js'; import {latest, migrate, convertFilter} from "@maplibre/maplibre-gl-style-spec";
import {combiningFilterOps} from '../libs/filterops' import {combiningFilterOps} from "../libs/filterops";
import InputSelect from './InputSelect' import InputSelect from "./InputSelect";
import Block from './Block' import Block from "./Block";
import SingleFilterEditor from './SingleFilterEditor' import SingleFilterEditor from "./SingleFilterEditor";
import FilterEditorBlock from './FilterEditorBlock' import FilterEditorBlock from "./FilterEditorBlock";
import InputButton from './InputButton' import InputButton from "./InputButton";
import Doc from './Doc' import Doc from "./Doc";
import ExpressionProperty from './_ExpressionProperty'; import ExpressionProperty from "./_ExpressionProperty";
import { WithTranslation, withTranslation } from 'react-i18next'; import { type WithTranslation, withTranslation } from "react-i18next";
import type { StyleSpecificationWithId } from '../libs/definitions'; import type { StyleSpecificationWithId } from "../libs/definitions";
function combiningFilter(props: FilterEditorInternalProps): LegacyFilterSpecification | ExpressionSpecification { function combiningFilter(props: FilterEditorInternalProps): LegacyFilterSpecification | ExpressionSpecification {
const filter = props.filter || ['all']; const filter = props.filter || ["all"];
if (!Array.isArray(filter)) { if (!Array.isArray(filter)) {
return filter; return filter;
@@ -28,7 +28,7 @@ function combiningFilter(props: FilterEditorInternalProps): LegacyFilterSpecific
let filters = filter.slice(1); let filters = filter.slice(1);
if(combiningFilterOps.indexOf(combiningOp) < 0) { if(combiningFilterOps.indexOf(combiningOp) < 0) {
combiningOp = 'all'; combiningOp = "all";
filters = [filter.slice(0)]; filters = [filter.slice(0)];
} }
@@ -49,7 +49,7 @@ function createStyleFromFilter(filter: LegacyFilterSpecification | ExpressionSpe
"sources": { "sources": {
"tmp": { "tmp": {
"type": "geojson", "type": "geojson",
"data": '' "data": ""
} }
}, },
"sprite": "", "sprite": "",
@@ -81,14 +81,14 @@ function checkIfSimpleFilter (filter: LegacyFilterSpecification | ExpressionSpec
} }
function hasCombiningFilter(filter: LegacyFilterSpecification | ExpressionSpecification) { function hasCombiningFilter(filter: LegacyFilterSpecification | ExpressionSpecification) {
return combiningFilterOps.indexOf(filter[0]) >= 0 return combiningFilterOps.indexOf(filter[0]) >= 0;
} }
function hasNestedCombiningFilter(filter: LegacyFilterSpecification | ExpressionSpecification) { function hasNestedCombiningFilter(filter: LegacyFilterSpecification | ExpressionSpecification) {
if(hasCombiningFilter(filter)) { if(hasCombiningFilter(filter)) {
return filter.slice(1).map(f => hasCombiningFilter(f as any)).filter(f => f == true).length > 0 return filter.slice(1).map(f => hasCombiningFilter(f as any)).filter(f => f == true).length > 0;
} }
return false return false;
} }
type FilterEditorInternalProps = { type FilterEditorInternalProps = {
@@ -108,7 +108,7 @@ type FilterEditorState = {
class FilterEditorInternal extends React.Component<FilterEditorInternalProps, FilterEditorState> { class FilterEditorInternal extends React.Component<FilterEditorInternalProps, FilterEditorState> {
static defaultProps = { static defaultProps = {
filter: ["all"], filter: ["all"],
} };
constructor (props: FilterEditorInternalProps) { constructor (props: FilterEditorInternalProps) {
super(props); super(props);
@@ -120,42 +120,42 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
// Convert filter to combining filter // Convert filter to combining filter
onFilterPartChanged(filterIdx: number, newPart: any[]) { onFilterPartChanged(filterIdx: number, newPart: any[]) {
const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification;
newFilter[filterIdx] = newPart newFilter[filterIdx] = newPart;
this.props.onChange(newFilter) this.props.onChange(newFilter);
} }
deleteFilterItem(filterIdx: number) { deleteFilterItem(filterIdx: number) {
const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification;
newFilter.splice(filterIdx + 1, 1) newFilter.splice(filterIdx + 1, 1);
this.props.onChange(newFilter) this.props.onChange(newFilter);
} }
addFilterItem = () => { addFilterItem = () => {
const newFilterItem = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification const newFilterItem = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification;
(newFilterItem as any[]).push(['==', 'name', '']) (newFilterItem as any[]).push(["==", "name", ""]);
this.props.onChange(newFilterItem) this.props.onChange(newFilterItem);
} };
onToggleDoc = (val: boolean) => { onToggleDoc = (val: boolean) => {
this.setState({ this.setState({
showDoc: val showDoc: val
}); });
} };
makeFilter = () => { makeFilter = () => {
this.setState({ this.setState({
displaySimpleFilter: true, displaySimpleFilter: true,
}) });
} };
makeExpression = () => { makeExpression = () => {
const filter = combiningFilter(this.props); const filter = combiningFilter(this.props);
this.props.onChange(migrateFilter(filter)); this.props.onChange(migrateFilter(filter));
this.setState({ this.setState({
displaySimpleFilter: false, displaySimpleFilter: false,
}) });
} };
static getDerivedStateFromProps(props: Readonly<FilterEditorInternalProps>, state: FilterEditorState) { static getDerivedStateFromProps(props: Readonly<FilterEditorInternalProps>, state: FilterEditorState) {
const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props)); const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props));
@@ -170,7 +170,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
else if (displaySimpleFilter && state.displaySimpleFilter === false) { else if (displaySimpleFilter && state.displaySimpleFilter === false) {
return { return {
valueIsSimpleFilter: true, valueIsSimpleFilter: true,
} };
} }
else { else {
return { return {
@@ -198,12 +198,10 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
onClick={this.makeExpression} onClick={this.makeExpression}
title={t("Convert to expression")} title={t("Convert to expression")}
> >
<svg style={{marginRight: "0.2em", width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24"> <TbMathFunction />
<path fill="currentColor" d={mdiFunctionVariant} />
</svg>
{t("Upgrade to expression")} {t("Upgrade to expression")}
</InputButton> </InputButton>
</div> </div>;
} }
else if (displaySimpleFilter) { else if (displaySimpleFilter) {
const filter = combiningFilter(this.props); const filter = combiningFilter(this.props);
@@ -217,9 +215,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
title={t("Convert to expression")} title={t("Convert to expression")}
className="maputnik-make-zoom-function" className="maputnik-make-zoom-function"
> >
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24"> <TbMathFunction />
<path fill="currentColor" d={mdiFunctionVariant} />
</svg>
</InputButton> </InputButton>
</div> </div>
); );
@@ -241,7 +237,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
} }
</div> </div>
); );
}) });
return ( return (
@@ -272,15 +268,14 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
className="maputnik-add-filter" className="maputnik-add-filter"
onClick={this.addFilterItem} onClick={this.addFilterItem}
> >
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24"> <PiListPlusBold style={{ verticalAlign: "text-bottom" }} />
<path fill="currentColor" d={mdiTableRowPlusAfter} /> {t("Add filter")}
</svg> {t("Add filter")}
</InputButton> </InputButton>
</div> </div>
<div <div
key="doc" key="doc"
className="maputnik-doc-inline" className="maputnik-doc-inline"
style={{display: this.state.showDoc ? '' : 'none'}} style={{display: this.state.showDoc ? "" : "none"}}
> >
<Doc fieldSpec={fieldSpec} /> <Doc fieldSpec={fieldSpec} />
</div> </div>
@@ -306,7 +301,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
{this.state.valueIsSimpleFilter && {this.state.valueIsSimpleFilter &&
<div className="maputnik-expr-infobox"> <div className="maputnik-expr-infobox">
{t("You've entered an old style filter.")} {t("You've entered an old style filter.")}
{' '} {" "}
<button <button
onClick={this.makeFilter} onClick={this.makeFilter}
className="maputnik-expr-infobox__button" className="maputnik-expr-infobox__button"
+5 -5
View File
@@ -1,7 +1,7 @@
import React, { PropsWithChildren } from 'react' import React, { type PropsWithChildren } from "react";
import InputButton from './InputButton' import InputButton from "./InputButton";
import {MdDelete} from 'react-icons/md' import {MdDelete} from "react-icons/md";
import { WithTranslation, withTranslation } from 'react-i18next'; import { type WithTranslation, withTranslation } from "react-i18next";
type FilterEditorBlockInternalProps = PropsWithChildren & { type FilterEditorBlockInternalProps = PropsWithChildren & {
onDelete(...args: unknown[]): unknown onDelete(...args: unknown[]): unknown
@@ -23,7 +23,7 @@ class FilterEditorBlockInternal extends React.Component<FilterEditorBlockInterna
<MdDelete /> <MdDelete />
</InputButton> </InputButton>
</div> </div>
</div> </div>;
} }
} }
-13
View File
@@ -1,13 +0,0 @@
import React from 'react'
import IconBase from 'react-icon-base'
export default class IconBackground extends React.Component {
render() {
return (
<IconBase viewBox="0 0 20 20" {...this.props}>
<path d="m 1.821019,10.255581 7.414535,5.020197 c 0.372277,0.25206 0.958697,0.239771 1.30985,-0.02745 L 17.539255,9.926162 C 17.89041,9.658941 17.873288,9.238006 17.501015,8.985946 L 10.08648,3.9657402 C 9.714204,3.7136802 9.127782,3.7259703 8.776627,3.9931918 L 1.782775,9.315365 c -0.3511551,0.267221 -0.3340331,0.688156 0.03824,0.940216 z" />
</IconBase>
)
}
}
-13
View File
@@ -1,13 +0,0 @@
import React from 'react'
import IconBase from 'react-icon-base'
export default class IconCircle extends React.Component {
render() {
return (
<IconBase viewBox="0 0 20 20" {...this.props}>
<path transform="translate(2 2)" d="M7.5,0C11.6422,0,15,3.3578,15,7.5S11.6422,15,7.5,15 S0,11.6422,0,7.5S3.3578,0,7.5,0z M7.5,1.6666c-3.2217,0-5.8333,2.6117-5.8333,5.8334S4.2783,13.3334,7.5,13.3334 s5.8333-2.6117,5.8333-5.8334S10.7217,1.6666,7.5,1.6666z"></path>
</IconBase>
)
}
}
-13
View File
@@ -1,13 +0,0 @@
import React from 'react'
import IconBase from 'react-icon-base'
export default class IconFill extends React.Component {
render() {
return (
<IconBase viewBox="0 0 20 20" {...this.props}>
<path d="M 2.84978,9.763512 9.462149,4.7316391 16.47225,9.478015 9.859886,14.509879 2.84978,9.763512 m -1.028761,0.492069 7.414535,5.020197 c 0.372277,0.25206 0.958697,0.239771 1.30985,-0.02745 L 17.539255,9.926162 C 17.89041,9.658941 17.873288,9.238006 17.501015,8.985946 L 10.08648,3.9657402 C 9.714204,3.7136802 9.127782,3.7259703 8.776627,3.9931918 L 1.782775,9.315365 c -0.3511551,0.267221 -0.3340331,0.688156 0.03824,0.940216 l 0,0 z" />
</IconBase>
)
}
}
+25 -24
View File
@@ -1,32 +1,33 @@
import React from 'react' import React from "react";
import IconLine from './IconLine' import type {CSSProperties} from "react";
import IconFill from './IconFill' import { BsBack, BsDiamondFill, BsSunFill } from "react-icons/bs";
import IconSymbol from './IconSymbol' import { MdBubbleChart, MdCircle, MdLocationPin, MdPhoto, MdPriorityHigh } from "react-icons/md";
import IconBackground from './IconBackground' import { IoAnalyticsOutline } from "react-icons/io5";
import IconCircle from './IconCircle' import { IoMdCube } from "react-icons/io";
import IconMissing from './IconMissing' import { FaMountain } from "react-icons/fa";
type IconLayerProps = { type IconLayerProps = {
type: string type: string
style?: object style?: CSSProperties
className?: string className?: string
}; };
export default class IconLayer extends React.Component<IconLayerProps> { const IconLayer: React.FC<IconLayerProps> = (props) => {
render() { const iconProps = { style: props.style };
const iconProps = { style: this.props.style } switch(props.type) {
switch(this.props.type) { case "fill-extrusion": return <IoMdCube {...iconProps} />;
case 'fill-extrusion': return <IconBackground {...iconProps} /> case "raster": return <MdPhoto {...iconProps} />;
case 'raster': return <IconFill {...iconProps} /> case "hillshade": return <BsSunFill {...iconProps} />;
case 'hillshade': return <IconFill {...iconProps} /> case "color-relief": return <FaMountain {...iconProps} />;
case 'heatmap': return <IconFill {...iconProps} /> case "heatmap": return <MdBubbleChart {...iconProps} />;
case 'fill': return <IconFill {...iconProps} /> case "fill": return <BsDiamondFill {...iconProps} />;
case 'background': return <IconBackground {...iconProps} /> case "background": return <BsBack {...iconProps} />;
case 'line': return <IconLine {...iconProps} /> case "line": return <IoAnalyticsOutline {...iconProps} />;
case 'symbol': return <IconSymbol {...iconProps} /> case "symbol": return <MdLocationPin {...iconProps} />;
case 'circle': return <IconCircle {...iconProps} /> case "circle": return <MdCircle {...iconProps} />;
default: return <IconMissing {...iconProps} /> default: return <MdPriorityHigh {...iconProps} />;
}
} }
} };
export default IconLayer;
-13
View File
@@ -1,13 +0,0 @@
import React from 'react'
import IconBase from 'react-icon-base'
export default class IconLine extends React.Component {
render() {
return (
<IconBase viewBox="0 0 20 20" {...this.props}>
<path d="M 12.34,1.29 C 12.5114,1.1076 12.7497,1.0029 13,1 13.5523,1 14,1.4477 14,2 14.0047,2.2478 13.907,2.4866 13.73,2.66 9.785626,6.5516986 6.6148407,9.7551593 2.65,13.72 2.4793,13.8963 2.2453,13.9971 2,14 1.4477,14 1,13.5523 1,13 0.9953,12.7522 1.093,12.5134 1.27,12.34 4.9761967,8.7018093 9.0356422,4.5930579 12.34,1.29 Z" transform="translate(2,2)" />
</IconBase>
)
}
}
-11
View File
@@ -1,11 +0,0 @@
import React from 'react'
import {MdPriorityHigh} from 'react-icons/md'
export default class IconMissing extends React.Component {
render() {
return (
<MdPriorityHigh {...this.props} />
)
}
}
-15
View File
@@ -1,15 +0,0 @@
import React from 'react'
import IconBase from 'react-icon-base'
export default class IconSymbol extends React.Component {
render() {
return (
<IconBase viewBox="0 0 20 20" {...this.props}>
<g transform="matrix(1.2718518,0,0,1.2601269,16.559526,-7.4065264)">
<path d="m -9.7959773,11.060163 c -0.3734787,-0.724437 -0.3580577,-1.2147051 -0.00547,-1.8767873 l 8.6034029,-0.019416 c 0.39670292,0.6865644 0.38365934,1.4750693 -0.011097,1.8864953 l -3.1359613,-0.0033 -0.013695,7.1305 c -0.4055357,0.397083 -1.3146432,0.397083 -1.7769191,-0.02274 l 0.030226,-7.104422 z" />
</g>
</IconBase>
)
}
}
+21 -21
View File
@@ -1,29 +1,29 @@
import React from 'react' import React from "react";
import InputString from './InputString' import InputString from "./InputString";
import InputNumber from './InputNumber' import InputNumber from "./InputNumber";
export type FieldArrayProps = { export type InputArrayProps = {
value: (string | number | undefined)[] value: (string | number | undefined)[]
type?: string type?: string
length?: number length?: number
default?: (string | number | undefined)[] default?: (string | number | undefined)[]
onChange?(value: (string | number | undefined)[] | undefined): unknown onChange?(value: (string | number | undefined)[] | undefined): unknown
'aria-label'?: string "aria-label"?: string
label?: string label?: string
}; };
type FieldArrayState = { type InputArrayState = {
value: (string | number | undefined)[] value: (string | number | undefined)[]
initialPropsValue: unknown[] initialPropsValue: unknown[]
} };
export default class FieldArray extends React.Component<FieldArrayProps, FieldArrayState> { export default class InputArray extends React.Component<InputArrayProps, InputArrayState> {
static defaultProps = { static defaultProps = {
value: [], value: [],
default: [], default: [],
} };
constructor (props: FieldArrayProps) { constructor (props: InputArrayProps) {
super(props); super(props);
this.state = { this.state = {
value: this.props.value.slice(0), value: this.props.value.slice(0),
@@ -32,7 +32,7 @@ export default class FieldArray extends React.Component<FieldArrayProps, FieldAr
}; };
} }
static getDerivedStateFromProps(props: Readonly<FieldArrayProps>, state: FieldArrayState) { static getDerivedStateFromProps(props: Readonly<InputArrayProps>, state: InputArrayState) {
const value: any[] = []; const value: any[] = [];
const initialPropsValue = state.initialPropsValue.slice(0); const initialPropsValue = state.initialPropsValue.slice(0);
@@ -44,7 +44,7 @@ export default class FieldArray extends React.Component<FieldArrayProps, FieldAr
value[i] = state.value[i]; value[i] = state.value[i];
initialPropsValue[i] = state.value[i]; initialPropsValue[i] = state.value[i];
} }
}) });
return { return {
value, value,
@@ -54,7 +54,7 @@ export default class FieldArray extends React.Component<FieldArrayProps, FieldAr
isComplete(value: unknown[]) { isComplete(value: unknown[]) {
return Array(this.props.length).fill(null).every((_, i) => { return Array(this.props.length).fill(null).every((_, i) => {
const val = value[i] const val = value[i];
return !(val === undefined || val === ""); return !(val === undefined || val === "");
}); });
} }
@@ -82,20 +82,20 @@ export default class FieldArray extends React.Component<FieldArrayProps, FieldAr
const containsValues = ( const containsValues = (
value.length > 0 && value.length > 0 &&
!value.every(val => { !value.every(val => {
return (val === "" || val === undefined) return (val === "" || val === undefined);
}) })
); );
const inputs = Array(this.props.length).fill(null).map((_, i) => { const inputs = Array(this.props.length).fill(null).map((_, i) => {
if(this.props.type === 'number') { if(this.props.type === "number") {
return <InputNumber return <InputNumber
key={i} key={i}
default={containsValues || !this.props.default ? undefined : this.props.default[i] as number} default={containsValues || !this.props.default ? undefined : this.props.default[i] as number}
value={value[i] as number} value={value[i] as number}
required={containsValues ? true : false} required={containsValues ? true : false}
onChange={(v) => this.changeValue(i, v)} onChange={(v) => this.changeValue(i, v)}
aria-label={this.props['aria-label'] || this.props.label} aria-label={this.props["aria-label"] || this.props.label}
/> />;
} else { } else {
return <InputString return <InputString
key={i} key={i}
@@ -103,15 +103,15 @@ export default class FieldArray extends React.Component<FieldArrayProps, FieldAr
value={value[i] as string} value={value[i] as string}
required={containsValues ? true : false} required={containsValues ? true : false}
onChange={this.changeValue.bind(this, i)} onChange={this.changeValue.bind(this, i)}
aria-label={this.props['aria-label'] || this.props.label} aria-label={this.props["aria-label"] || this.props.label}
/> />;
} }
}) });
return ( return (
<div className="maputnik-array"> <div className="maputnik-array">
{inputs} {inputs}
</div> </div>
) );
} }
} }
+11 -11
View File
@@ -1,18 +1,18 @@
import InputAutocomplete from './InputAutocomplete' import InputAutocomplete from "./InputAutocomplete";
import { mount } from 'cypress/react' import { mount } from "cypress/react";
const fruits = ['apple', 'banana', 'cherry']; const fruits = ["apple", "banana", "cherry"];
describe('<InputAutocomplete />', () => { describe("<InputAutocomplete />", () => {
it('filters options when typing', () => { it("filters options when typing", () => {
mount( mount(
<InputAutocomplete aria-label="Fruit" options={fruits.map(f => [f, f])} /> <InputAutocomplete aria-label="Fruit" options={fruits.map(f => [f, f])} />
); );
cy.get('input').focus(); cy.get("input").focus();
cy.get('.maputnik-autocomplete-menu-item').should('have.length', 3); cy.get(".maputnik-autocomplete-menu-item").should("have.length", 3);
cy.get('input').type('ch'); cy.get("input").type("ch");
cy.get('.maputnik-autocomplete-menu-item').should('have.length', 1).and('contain', 'cherry'); cy.get(".maputnik-autocomplete-menu-item").should("have.length", 1).and("contain", "cherry");
cy.get('.maputnik-autocomplete-menu-item').click(); cy.get(".maputnik-autocomplete-menu-item").click();
cy.get('input').should('have.value', 'cherry'); cy.get("input").should("have.value", "cherry");
}); });
}); });
+39 -39
View File
@@ -1,37 +1,37 @@
import React from 'react' import React from "react";
import classnames from 'classnames' import classnames from "classnames";
import {useCombobox} from 'downshift' import {useCombobox} from "downshift";
const MAX_HEIGHT = 140 const MAX_HEIGHT = 140;
export type InputAutocompleteProps = { export type InputAutocompleteProps = {
value?: string value?: string
options?: any[] options?: any[]
onChange?(value: string | undefined): unknown onChange?(value: string | undefined): unknown
'aria-label'?: string "aria-label"?: string
}; };
export default function InputAutocomplete({ export default function InputAutocomplete({
value, value,
options = [], options = [],
onChange = () => {}, onChange = () => {},
'aria-label': ariaLabel, "aria-label": ariaLabel,
}: InputAutocompleteProps) { }: InputAutocompleteProps) {
const [input, setInput] = React.useState(value || '') const [input, setInput] = React.useState(value || "");
const menuRef = React.useRef<HTMLDivElement>(null) const menuRef = React.useRef<HTMLDivElement>(null);
const [maxHeight, setMaxHeight] = React.useState(MAX_HEIGHT) const [maxHeight, setMaxHeight] = React.useState(MAX_HEIGHT);
const filteredItems = React.useMemo(() => { const filteredItems = React.useMemo(() => {
const lv = input.toLowerCase() const lv = input.toLowerCase();
return options.filter((item) => item[0].toLowerCase().includes(lv)) return options.filter((item) => item[0].toLowerCase().includes(lv));
}, [options, input]) }, [options, input]);
const calcMaxHeight = React.useCallback(() => { const calcMaxHeight = React.useCallback(() => {
if (menuRef.current) { if (menuRef.current) {
const space = window.innerHeight - menuRef.current.getBoundingClientRect().top const space = window.innerHeight - menuRef.current.getBoundingClientRect().top;
setMaxHeight(Math.min(space, MAX_HEIGHT)) setMaxHeight(Math.min(space, MAX_HEIGHT));
} }
}, []) }, []);
const { const {
isOpen, isOpen,
@@ -43,48 +43,48 @@ export default function InputAutocomplete({
} = useCombobox({ } = useCombobox({
items: filteredItems, items: filteredItems,
inputValue: input, inputValue: input,
itemToString: (item) => (item ? item[0] : ''), itemToString: (item) => (item ? item[0] : ""),
stateReducer: (_state, action) => { stateReducer: (_state, action) => {
if (action.type === useCombobox.stateChangeTypes.InputClick) { if (action.type === useCombobox.stateChangeTypes.InputClick) {
return {...action.changes, isOpen: true} return {...action.changes, isOpen: true};
} }
return action.changes return action.changes;
}, },
onSelectedItemChange: ({selectedItem}) => { onSelectedItemChange: ({selectedItem}) => {
const v = selectedItem ? selectedItem[0] : '' const v = selectedItem ? selectedItem[0] : "";
setInput(v) setInput(v);
onChange(selectedItem ? selectedItem[0] : undefined) onChange(selectedItem ? selectedItem[0] : undefined);
}, },
onInputValueChange: ({inputValue: v}) => { onInputValueChange: ({inputValue: v}) => {
if (typeof v === 'string') { if (typeof v === "string") {
setInput(v) setInput(v);
onChange(v === '' ? undefined : v) onChange(v === "" ? undefined : v);
openMenu() openMenu();
} }
}, },
}) });
React.useEffect(() => { React.useEffect(() => {
if (isOpen) { if (isOpen) {
calcMaxHeight() calcMaxHeight();
} }
}, [isOpen, calcMaxHeight]) }, [isOpen, calcMaxHeight]);
React.useEffect(() => { React.useEffect(() => {
window.addEventListener('resize', calcMaxHeight) window.addEventListener("resize", calcMaxHeight);
return () => window.removeEventListener('resize', calcMaxHeight) return () => window.removeEventListener("resize", calcMaxHeight);
}, [calcMaxHeight]) }, [calcMaxHeight]);
React.useEffect(() => { React.useEffect(() => {
setInput(value || '') setInput(value || "");
}, [value]) }, [value]);
return ( return (
<div className="maputnik-autocomplete"> <div className="maputnik-autocomplete">
<input <input
{...getInputProps({ {...getInputProps({
'aria-label': ariaLabel, "aria-label": ariaLabel,
className: 'maputnik-string', className: "maputnik-string",
spellCheck: false, spellCheck: false,
onFocus: () => openMenu(), onFocus: () => openMenu(),
})} })}
@@ -92,7 +92,7 @@ export default function InputAutocomplete({
<div <div
{...getMenuProps({}, {suppressRefError: true})} {...getMenuProps({}, {suppressRefError: true})}
ref={menuRef} ref={menuRef}
style={{position: 'fixed', overflow: 'auto', maxHeight, zIndex: 998}} style={{position: "fixed", overflow: "auto", maxHeight, zIndex: 998}}
className="maputnik-autocomplete-menu" className="maputnik-autocomplete-menu"
> >
{isOpen && {isOpen &&
@@ -102,8 +102,8 @@ export default function InputAutocomplete({
{...getItemProps({ {...getItemProps({
item, item,
index, index,
className: classnames('maputnik-autocomplete-menu-item', { className: classnames("maputnik-autocomplete-menu-item", {
'maputnik-autocomplete-menu-item-selected': highlightedIndex === index, "maputnik-autocomplete-menu-item-selected": highlightedIndex === index,
}), }),
})} })}
> >
@@ -112,5 +112,5 @@ export default function InputAutocomplete({
))} ))}
</div> </div>
</div> </div>
) );
} }
+3 -3
View File
@@ -1,5 +1,5 @@
import React from 'react' import React from "react";
import classnames from 'classnames' import classnames from "classnames";
type InputButtonProps = { type InputButtonProps = {
"data-wd-key"?: string "data-wd-key"?: string
@@ -28,6 +28,6 @@ export default class InputButton extends React.Component<InputButtonProps> {
style={this.props.style} style={this.props.style}
> >
{this.props.children} {this.props.children}
</button> </button>;
} }
} }
+5 -5
View File
@@ -1,4 +1,4 @@
import React from 'react' import React from "react";
export type InputCheckboxProps = { export type InputCheckboxProps = {
value?: boolean value?: boolean
@@ -9,11 +9,11 @@ export type InputCheckboxProps = {
export default class InputCheckbox extends React.Component<InputCheckboxProps> { export default class InputCheckbox extends React.Component<InputCheckboxProps> {
static defaultProps = { static defaultProps = {
value: false, value: false,
} };
onChange = () => { onChange = () => {
this.props.onChange(!this.props.value); this.props.onChange(!this.props.value);
} };
render() { render() {
return <div className="maputnik-checkbox-wrapper"> return <div className="maputnik-checkbox-wrapper">
@@ -27,11 +27,11 @@ export default class InputCheckbox extends React.Component<InputCheckboxProps> {
/> />
<div className="maputnik-checkbox-box"> <div className="maputnik-checkbox-box">
<svg style={{ <svg style={{
display: this.props.value ? 'inline' : 'none' display: this.props.value ? "inline" : "none"
}} className="maputnik-checkbox-icon" viewBox='0 0 32 32'> }} className="maputnik-checkbox-icon" viewBox='0 0 32 32'>
<path d='M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z' /> <path d='M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z' />
</svg> </svg>
</div> </div>
</div> </div>;
} }
} }
+29 -29
View File
@@ -1,12 +1,12 @@
import React from 'react' import React from "react";
import Color from 'color' import Color from "color";
import ChromePicker from 'react-color/lib/components/chrome/Chrome' import ChromePicker from "react-color/lib/components/chrome/Chrome";
import {ColorResult} from 'react-color'; import {type ColorResult} from "react-color";
import lodash from 'lodash'; import lodash from "lodash";
function formatColor(color: ColorResult): string { function formatColor(color: ColorResult): string {
const rgb = color.rgb const rgb = color.rgb;
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})` return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})`;
} }
export type InputColorProps = { export type InputColorProps = {
@@ -16,14 +16,14 @@ export type InputColorProps = {
doc?: string doc?: string
style?: object style?: object
default?: string default?: string
'aria-label'?: string "aria-label"?: string
}; };
/*** Number fields with support for min, max and units and documentation*/ /*** Number fields with support for min, max and units and documentation*/
export default class InputColor extends React.Component<InputColorProps> { export default class InputColor extends React.Component<InputColorProps> {
state = { state = {
pickerOpened: false pickerOpened: false
} };
colorInput: HTMLInputElement | null = null; colorInput: HTMLInputElement | null = null;
constructor (props: InputColorProps) { constructor (props: InputColorProps) {
@@ -39,29 +39,29 @@ export default class InputColor extends React.Component<InputColorProps> {
//but I am too stupid to get it to work together with fixed position //but I am too stupid to get it to work together with fixed position
//and scrollbars so I have to fallback to JavaScript //and scrollbars so I have to fallback to JavaScript
calcPickerOffset = () => { calcPickerOffset = () => {
const elem = this.colorInput const elem = this.colorInput;
if(elem) { if(elem) {
const pos = elem.getBoundingClientRect() const pos = elem.getBoundingClientRect();
return { return {
top: pos.top, top: pos.top,
left: pos.left + 196, left: pos.left + 196,
} };
} else { } else {
return { return {
top: 160, top: 160,
left: 555, left: 555,
} };
} }
} };
togglePicker = () => { togglePicker = () => {
this.setState({ pickerOpened: !this.state.pickerOpened }) this.setState({ pickerOpened: !this.state.pickerOpened });
} };
get color() { get color() {
// Catch invalid color. // Catch invalid color.
try { try {
return Color(this.props.value).rgb() return Color(this.props.value).rgb();
} }
catch(err) { catch(err) {
console.warn("Error parsing color: ", err); console.warn("Error parsing color: ", err);
@@ -74,7 +74,7 @@ export default class InputColor extends React.Component<InputColorProps> {
} }
render() { render() {
const offset = this.calcPickerOffset() const offset = this.calcPickerOffset();
const currentColor = this.color.object(); const currentColor = this.color.object();
const currentChromeColor = { const currentChromeColor = {
r: currentColor.r, r: currentColor.r,
@@ -82,12 +82,12 @@ export default class InputColor extends React.Component<InputColorProps> {
b: currentColor.b, b: currentColor.b,
// Rename alpha -> a for ChromePicker // Rename alpha -> a for ChromePicker
a: currentColor.alpha! a: currentColor.alpha!
} };
const picker = <div const picker = <div
className="maputnik-color-picker-offset" className="maputnik-color-picker-offset"
style={{ style={{
position: 'fixed', position: "fixed",
zIndex: 1, zIndex: 1,
left: offset.left, left: offset.left,
top: offset.top, top: offset.top,
@@ -101,14 +101,14 @@ export default class InputColor extends React.Component<InputColorProps> {
onClick={this.togglePicker} onClick={this.togglePicker}
style={{ style={{
zIndex: -1, zIndex: -1,
position: 'fixed', position: "fixed",
top: '0px', top: "0px",
right: '0px', right: "0px",
bottom: '0px', bottom: "0px",
left: '0px', left: "0px",
}} }}
/> />
</div> </div>;
const swatchStyle = { const swatchStyle = {
backgroundColor: this.props.value backgroundColor: this.props.value
@@ -118,11 +118,11 @@ export default class InputColor extends React.Component<InputColorProps> {
{this.state.pickerOpened && picker} {this.state.pickerOpened && picker}
<div className="maputnik-color-swatch" style={swatchStyle}></div> <div className="maputnik-color-swatch" style={swatchStyle}></div>
<input <input
aria-label={this.props['aria-label']} aria-label={this.props["aria-label"]}
spellCheck="false" spellCheck="false"
autoComplete="off" autoComplete="off"
className="maputnik-color" className="maputnik-color"
ref={(input) => this.colorInput = input} ref={(input) => {this.colorInput = input;}}
onClick={this.togglePicker} onClick={this.togglePicker}
style={this.props.style} style={this.props.style}
name={this.props.name} name={this.props.name}
@@ -130,6 +130,6 @@ export default class InputColor extends React.Component<InputColorProps> {
value={this.props.value ? this.props.value : ""} value={this.props.value ? this.props.value : ""}
onChange={(e) => this.onChange(e.target.value)} onChange={(e) => this.onChange(e.target.value)}
/> />
</div> </div>;
} }
} }
+56 -46
View File
@@ -1,64 +1,67 @@
import React from 'react' import React from "react";
import capitalize from 'lodash.capitalize' import capitalize from "lodash.capitalize";
import {MdDelete} from 'react-icons/md' import {MdDelete} from "react-icons/md";
import { WithTranslation, withTranslation } from 'react-i18next'; import { type WithTranslation, withTranslation } from "react-i18next";
import InputString from './InputString' import InputString from "./InputString";
import InputNumber from './InputNumber' import InputNumber from "./InputNumber";
import InputButton from './InputButton' import InputButton from "./InputButton";
import FieldDocLabel from './FieldDocLabel' import FieldDocLabel from "./FieldDocLabel";
import InputEnum from './InputEnum' import InputEnum from "./InputEnum";
import InputUrl from './InputUrl' import InputUrl from "./InputUrl";
import InputColor from "./InputColor";
export type FieldDynamicArrayProps = { export type InputDynamicArrayProps = {
value?: (string | number | undefined)[] value?: (string | number | undefined)[]
type?: 'url' | 'number' | 'enum' | 'string' type?: "url" | "number" | "enum" | "string" | "color"
default?: (string | number | undefined)[] default?: (string | number | undefined)[]
onChange?(values: (string | number | undefined)[] | undefined): unknown onChange?(values: (string | number | undefined)[] | undefined): unknown
style?: object style?: object
fieldSpec?: { fieldSpec?: {
values?: any values?: any
} }
'aria-label'?: string "aria-label"?: string
label: string label: string
} };
type FieldDynamicArrayInternalProps = FieldDynamicArrayProps & WithTranslation; type InputDynamicArrayInternalProps = InputDynamicArrayProps & WithTranslation;
class FieldDynamicArrayInternal extends React.Component<FieldDynamicArrayInternalProps> { class InputDynamicArrayInternal extends React.Component<InputDynamicArrayInternalProps> {
changeValue(idx: number, newValue: string | number | undefined) { changeValue(idx: number, newValue: string | number | undefined) {
const values = this.values.slice(0) const values = this.values.slice(0);
values[idx] = newValue values[idx] = newValue;
if (this.props.onChange) this.props.onChange(values) if (this.props.onChange) this.props.onChange(values);
} }
get values() { get values() {
return this.props.value || this.props.default || [] return this.props.value || this.props.default || [];
} }
addValue = () => { addValue = () => {
const values = this.values.slice(0) const values = this.values.slice(0);
if (this.props.type === 'number') { if (this.props.type === "number") {
values.push(0) values.push(0);
} }
else if (this.props.type === 'url') { else if (this.props.type === "url") {
values.push(""); values.push("");
} }
else if (this.props.type === 'enum') { else if (this.props.type === "enum") {
const {fieldSpec} = this.props; const {fieldSpec} = this.props;
const defaultValue = Object.keys(fieldSpec!.values)[0]; const defaultValue = Object.keys(fieldSpec!.values)[0];
values.push(defaultValue); values.push(defaultValue);
} else if (this.props.type === "color") {
values.push("#000000");
} else { } else {
values.push("") values.push("");
} }
if (this.props.onChange) this.props.onChange(values) if (this.props.onChange) this.props.onChange(values);
} };
deleteValue(valueIdx: number) { deleteValue(valueIdx: number) {
const values = this.values.slice(0) const values = this.values.slice(0);
values.splice(valueIdx, 1) values.splice(valueIdx, 1);
if (this.props.onChange) this.props.onChange(values.length > 0 ? values : undefined); if (this.props.onChange) this.props.onChange(values.length > 0 ? values : undefined);
} }
@@ -72,35 +75,42 @@ class FieldDynamicArrayInternal extends React.Component<FieldDynamicArrayInterna
{...i18nProps} {...i18nProps}
/>; />;
let input; let input;
if(this.props.type === 'url') { if(this.props.type === "url") {
input = <InputUrl input = <InputUrl
value={v as string} value={v as string}
onChange={this.changeValue.bind(this, i)} onChange={this.changeValue.bind(this, i)}
aria-label={this.props['aria-label'] || this.props.label} aria-label={this.props["aria-label"] || this.props.label}
/> />;
} }
else if (this.props.type === 'number') { else if (this.props.type === "number") {
input = <InputNumber input = <InputNumber
value={v as number} value={v as number}
onChange={this.changeValue.bind(this, i)} onChange={this.changeValue.bind(this, i)}
aria-label={this.props['aria-label'] || this.props.label} aria-label={this.props["aria-label"] || this.props.label}
/> />;
} }
else if (this.props.type === 'enum') { else if (this.props.type === "enum") {
const options = Object.keys(this.props.fieldSpec?.values).map(v => [v, capitalize(v)]); const options = Object.keys(this.props.fieldSpec?.values).map(v => [v, capitalize(v)]);
input = <InputEnum input = <InputEnum
options={options} options={options}
value={v as string} value={v as string}
onChange={this.changeValue.bind(this, i)} onChange={this.changeValue.bind(this, i)}
aria-label={this.props['aria-label'] || this.props.label} aria-label={this.props["aria-label"] || this.props.label}
/> />;
}
else if (this.props.type === "color") {
input = <InputColor
value={v as string}
onChange={this.changeValue.bind(this, i)}
aria-label={this.props["aria-label"] || this.props.label}
/>;
} }
else { else {
input = <InputString input = <InputString
value={v as string} value={v as string}
onChange={this.changeValue.bind(this, i)} onChange={this.changeValue.bind(this, i)}
aria-label={this.props['aria-label'] || this.props.label} aria-label={this.props["aria-label"] || this.props.label}
/> />;
} }
return <div return <div
@@ -114,8 +124,8 @@ class FieldDynamicArrayInternal extends React.Component<FieldDynamicArrayInterna
<div className="maputnik-array-block-content"> <div className="maputnik-array-block-content">
{input} {input}
</div> </div>
</div> </div>;
}) });
return ( return (
<div className="maputnik-array"> <div className="maputnik-array">
@@ -131,8 +141,8 @@ class FieldDynamicArrayInternal extends React.Component<FieldDynamicArrayInterna
} }
} }
const FieldDynamicArray = withTranslation()(FieldDynamicArrayInternal); const InputDynamicArray = withTranslation()(InputDynamicArrayInternal);
export default FieldDynamicArray; export default InputDynamicArray;
type DeleteValueInputButtonProps = { type DeleteValueInputButtonProps = {
onClick?(...args: unknown[]): unknown onClick?(...args: unknown[]): unknown
@@ -149,6 +159,6 @@ class DeleteValueInputButton extends React.Component<DeleteValueInputButtonProps
<FieldDocLabel <FieldDocLabel
label={<MdDelete />} label={<MdDelete />}
/> />
</InputButton> </InputButton>;
} }
} }
+11 -11
View File
@@ -1,14 +1,14 @@
import React from 'react' import React from "react";
import InputSelect from './InputSelect' import InputSelect from "./InputSelect";
import InputMultiInput from './InputMultiInput' import InputMultiInput from "./InputMultiInput";
function optionsLabelLength(options: any[]) { function optionsLabelLength(options: any[]) {
let sum = 0; let sum = 0;
options.forEach(([_, label]) => { options.forEach(([_, label]) => {
sum += label.length sum += label.length;
}) });
return sum return sum;
} }
@@ -20,7 +20,7 @@ export type InputEnumProps = {
name?: string name?: string
onChange(...args: unknown[]): unknown onChange(...args: unknown[]): unknown
options: any[] options: any[]
'aria-label'?: string "aria-label"?: string
label?: string label?: string
}; };
@@ -35,15 +35,15 @@ export default class InputEnum extends React.Component<InputEnumProps> {
options={options} options={options}
value={(value || this.props.default)!} value={(value || this.props.default)!}
onChange={onChange} onChange={onChange}
aria-label={this.props['aria-label'] || label} aria-label={this.props["aria-label"] || label}
/> />;
} else { } else {
return <InputSelect return <InputSelect
options={options} options={options}
value={(value || this.props.default)!} value={(value || this.props.default)!}
onChange={onChange} onChange={onChange}
aria-label={this.props['aria-label'] || label} aria-label={this.props["aria-label"] || label}
/> />;
} }
} }
} }
+12 -12
View File
@@ -1,20 +1,20 @@
import React from 'react' import React from "react";
import InputAutocomplete from './InputAutocomplete' import InputAutocomplete from "./InputAutocomplete";
export type FieldFontProps = { export type InputFontProps = {
name: string name: string
value?: string[] value?: string[]
default?: string[] default?: string[]
fonts?: unknown[] fonts?: unknown[]
style?: object style?: object
onChange(...args: unknown[]): unknown onChange(...args: unknown[]): unknown
'aria-label'?: string "aria-label"?: string
}; };
export default class FieldFont extends React.Component<FieldFontProps> { export default class InputFont extends React.Component<InputFontProps> {
static defaultProps = { static defaultProps = {
fonts: [] fonts: []
} };
get values() { get values() {
const out = this.props.value || this.props.default || []; const out = this.props.value || this.props.default || [];
@@ -29,11 +29,11 @@ export default class FieldFont extends React.Component<FieldFontProps> {
} }
changeFont(idx: number, newValue: string) { changeFont(idx: number, newValue: string) {
const changedValues = this.values.slice(0) const changedValues = this.values.slice(0);
changedValues[idx] = newValue changedValues[idx] = newValue;
const filteredValues = changedValues const filteredValues = changedValues
.filter(v => v !== undefined) .filter(v => v !== undefined)
.filter(v => v !== "") .filter(v => v !== "");
this.props.onChange(filteredValues); this.props.onChange(filteredValues);
} }
@@ -44,13 +44,13 @@ export default class FieldFont extends React.Component<FieldFontProps> {
key={i} key={i}
> >
<InputAutocomplete <InputAutocomplete
aria-label={this.props['aria-label'] || this.props.name} aria-label={this.props["aria-label"] || this.props.name}
value={value} value={value}
options={this.props.fonts?.map(f => [f, f])} options={this.props.fonts?.map(f => [f, f])}
onChange={this.changeFont.bind(this, i)} onChange={this.changeFont.bind(this, i)}
/> />
</li> </li>;
}) });
return ( return (
<ul className="maputnik-font"> <ul className="maputnik-font">
+31 -30
View File
@@ -1,19 +1,20 @@
import React from 'react' import React from "react";
import classnames from 'classnames'; import classnames from "classnames";
import CodeMirror, { ModeSpec } from 'codemirror'; import CodeMirror, { type ModeSpec } from "codemirror";
import { Trans, WithTranslation, withTranslation } from 'react-i18next'; import { Trans, type WithTranslation, withTranslation } from "react-i18next";
import 'codemirror/mode/javascript/javascript' import "codemirror/mode/javascript/javascript";
import 'codemirror/addon/lint/lint' import "codemirror/addon/lint/lint";
import 'codemirror/addon/edit/matchbrackets' import "codemirror/addon/edit/matchbrackets";
import 'codemirror/lib/codemirror.css' import "codemirror/lib/codemirror.css";
import 'codemirror/addon/lint/lint.css' import "codemirror/addon/lint/lint.css";
import stringifyPretty from 'json-stringify-pretty-compact' import stringifyPretty from "json-stringify-pretty-compact";
import '../libs/codemirror-mgl'; import "../libs/codemirror-mgl";
import type { LayerSpecification } from "maplibre-gl";
export type InputJsonProps = { export type InputJsonProps = {
layer: any layer: LayerSpecification
maxHeight?: number maxHeight?: number
onChange?(...args: unknown[]): unknown onChange?(...args: unknown[]): unknown
lineNumbers?: boolean lineNumbers?: boolean
@@ -48,7 +49,7 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
onBlur: () => {}, onBlur: () => {},
onJSONInvalid: () => {}, onJSONInvalid: () => {},
onJSONValid: () => {}, onJSONValid: () => {},
} };
_keyEvent: string; _keyEvent: string;
_doc: CodeMirror.Editor | undefined; _doc: CodeMirror.Editor | undefined;
_el: HTMLDivElement | null = null; _el: HTMLDivElement | null = null;
@@ -72,7 +73,7 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
}, },
lineWrapping: this.props.lineWrapping, lineWrapping: this.props.lineWrapping,
tabSize: 2, tabSize: 2,
theme: 'maputnik', theme: "maputnik",
viewportMargin: Infinity, viewportMargin: Infinity,
lineNumbers: this.props.lineNumbers, lineNumbers: this.props.lineNumbers,
lint: this.props.lint || { lint: this.props.lint || {
@@ -83,14 +84,14 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
scrollbarStyle: "null", scrollbarStyle: "null",
}); });
this._doc.on('change', this.onChange); this._doc.on("change", this.onChange);
this._doc.on('focus', this.onFocus); this._doc.on("focus", this.onFocus);
this._doc.on('blur', this.onBlur); this._doc.on("blur", this.onBlur);
} }
onPointerDown = () => { onPointerDown = () => {
this._keyEvent = "pointer"; this._keyEvent = "pointer";
} };
onFocus = () => { onFocus = () => {
if (this.props.onFocus) this.props.onFocus(); if (this.props.onFocus) this.props.onFocus();
@@ -98,7 +99,7 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
isEditing: true, isEditing: true,
showMessage: (this._keyEvent === "keyboard"), showMessage: (this._keyEvent === "keyboard"),
}); });
} };
onBlur = () => { onBlur = () => {
this._keyEvent = "keyboard"; this._keyEvent = "keyboard";
@@ -107,12 +108,12 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
isEditing: false, isEditing: false,
showMessage: false, showMessage: false,
}); });
} };
componentWillUnMount () { componentWillUnMount () {
this._doc!.off('change', this.onChange); this._doc!.off("change", this.onChange);
this._doc!.off('focus', this.onFocus); this._doc!.off("focus", this.onFocus);
this._doc!.off('blur', this.onBlur); this._doc!.off("blur", this.onBlur);
} }
componentDidUpdate(prevProps: InputJsonProps) { componentDidUpdate(prevProps: InputJsonProps) {
@@ -120,7 +121,7 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
this._cancelNextChange = true; this._cancelNextChange = true;
this._doc!.setValue( this._doc!.setValue(
this.props.getValue!(this.props.layer), this.props.getValue!(this.props.layer),
) );
} }
} }
@@ -129,7 +130,7 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
this._cancelNextChange = false; this._cancelNextChange = false;
this.setState({ this.setState({
prevValue: this._doc!.getValue(), prevValue: this._doc!.getValue(),
}) });
return; return;
} }
const newCode = this._doc!.getValue(); const newCode = this._doc!.getValue();
@@ -140,14 +141,14 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
parsedLayer = JSON.parse(newCode); parsedLayer = JSON.parse(newCode);
} catch(_err) { } catch(_err) {
err = _err; err = _err;
console.warn(_err) console.warn(_err);
} }
if (err && this.props.onJSONInvalid) { if (err && this.props.onJSONInvalid) {
this.props.onJSONInvalid(); this.props.onJSONInvalid();
} }
else { else {
if (this.props.onChange) this.props.onChange(parsedLayer) if (this.props.onChange) this.props.onChange(parsedLayer);
if (this.props.onJSONValid) this.props.onJSONValid(); if (this.props.onJSONValid) this.props.onJSONValid();
} }
} }
@@ -155,7 +156,7 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
this.setState({ this.setState({
prevValue: newCode, prevValue: newCode,
}); });
} };
render() { render() {
const t = this.props.t; const t = this.props.t;
@@ -173,10 +174,10 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
</div> </div>
<div <div
className={classnames("codemirror-container", this.props.className)} className={classnames("codemirror-container", this.props.className)}
ref={(el) => this._el = el} ref={(el) => {this._el = el;}}
style={style} style={style}
/> />
</div> </div>;
} }
} }
+10 -10
View File
@@ -1,22 +1,22 @@
import React from 'react' import React from "react";
import classnames from 'classnames' import classnames from "classnames";
export type InputMultiInputProps = { export type InputMultiInputProps = {
name?: string name?: string
value: string value: string
options: any[] options: any[]
onChange(...args: unknown[]): unknown onChange(...args: unknown[]): unknown
'aria-label'?: string "aria-label"?: string
}; };
export default class InputMultiInput extends React.Component<InputMultiInputProps> { export default class InputMultiInput extends React.Component<InputMultiInputProps> {
render() { render() {
let options = this.props.options let options = this.props.options;
if(options.length > 0 && !Array.isArray(options[0])) { if(options.length > 0 && !Array.isArray(options[0])) {
options = options.map(v => [v, v]) options = options.map(v => [v, v]);
} }
const selectedValue = this.props.value || options[0][0] const selectedValue = this.props.value || options[0][0];
const radios = options.map(([val, label])=> { const radios = options.map(([val, label])=> {
return <label return <label
key={val} key={val}
@@ -29,11 +29,11 @@ export default class InputMultiInput extends React.Component<InputMultiInputProp
checked={val === selectedValue} checked={val === selectedValue}
/> />
{label} {label}
</label> </label>;
}) });
return <fieldset className="maputnik-multibutton" aria-label={this.props['aria-label']}> return <fieldset className="maputnik-multibutton" aria-label={this.props["aria-label"]}>
{radios} {radios}
</fieldset> </fieldset>;
} }
} }
+20 -20
View File
@@ -1,5 +1,5 @@
import React, { BaseSyntheticEvent } from 'react' import React, { type BaseSyntheticEvent } from "react";
import generateUniqueId from '../libs/document-uid'; import generateUniqueId from "../libs/document-uid";
export type InputNumberProps = { export type InputNumberProps = {
value?: number value?: number
@@ -23,22 +23,22 @@ type InputNumberState = {
* This is the value that is currently being edited. It can be an invalid value. * This is the value that is currently being edited. It can be an invalid value.
*/ */
dirtyValue?: number | string | undefined dirtyValue?: number | string | undefined
} };
export default class InputNumber extends React.Component<InputNumberProps, InputNumberState> { export default class InputNumber extends React.Component<InputNumberProps, InputNumberState> {
static defaultProps = { static defaultProps = {
rangeStep: 1 rangeStep: 1
} };
_keyboardEvent: boolean = false; _keyboardEvent: boolean = false;
constructor(props: InputNumberProps) { constructor(props: InputNumberProps) {
super(props) super(props);
this.state = { this.state = {
uuid: +generateUniqueId(), uuid: +generateUniqueId(),
editing: false, editing: false,
value: props.value, value: props.value,
dirtyValue: props.value, dirtyValue: props.value,
} };
} }
static getDerivedStateFromProps(props: Readonly<InputNumberProps>, state: InputNumberState) { static getDerivedStateFromProps(props: Readonly<InputNumberProps>, state: InputNumberState) {
@@ -57,7 +57,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
const hasChanged = this.props.value !== value; const hasChanged = this.props.value !== value;
if(this.isValid(value) && hasChanged) { if(this.isValid(value) && hasChanged) {
if (this.props.onChange) this.props.onChange(value) if (this.props.onChange) this.props.onChange(value);
this.setState({ this.setState({
value: value, value: value,
}); });
@@ -70,7 +70,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
this.setState({ this.setState({
dirtyValue: newValue === "" ? undefined : newValue, dirtyValue: newValue === "" ? undefined : newValue,
}) });
} }
isValid(v: number | string | undefined) { isValid(v: number | string | undefined) {
@@ -80,18 +80,18 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
const value = +v; const value = +v;
if(isNaN(value)) { if(isNaN(value)) {
return false return false;
} }
if(!isNaN(this.props.min!) && value < this.props.min!) { if(!isNaN(this.props.min!) && value < this.props.min!) {
return false return false;
} }
if(!isNaN(this.props.max!) && value > this.props.max!) { if(!isNaN(this.props.max!) && value > this.props.max!) {
return false return false;
} }
return true return true;
} }
resetValue = () => { resetValue = () => {
@@ -104,14 +104,14 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
// If set value is invalid fall back to the last valid value from props or at last resort the default value // If set value is invalid fall back to the last valid value from props or at last resort the default value
if (!this.isValid(this.state.value)) { if (!this.isValid(this.state.value)) {
if(this.isValid(this.props.value)) { if(this.isValid(this.props.value)) {
this.changeValue(this.props.value) this.changeValue(this.props.value);
this.setState({dirtyValue: this.props.value}); this.setState({dirtyValue: this.props.value});
} else { } else {
this.changeValue(undefined); this.changeValue(undefined);
this.setState({dirtyValue: undefined}); this.setState({dirtyValue: undefined});
} }
} }
} };
onChangeRange = (e: BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) => { onChangeRange = (e: BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) => {
let value = parseFloat(e.target.value); let value = parseFloat(e.target.value);
@@ -132,7 +132,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
value = this.state.value! - step; value = this.state.value! - step;
} }
else { else {
value = this.state.value! + step value = this.state.value! + step;
} }
dirtyValue = value; dirtyValue = value;
} }
@@ -153,7 +153,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
this.setState({value, dirtyValue}); this.setState({value, dirtyValue});
if (this.props.onChange) this.props.onChange(value); if (this.props.onChange) this.props.onChange(value);
} };
render() { render() {
if( if(
@@ -217,18 +217,18 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
}} }}
onBlur={_e => { onBlur={_e => {
this.setState({editing: false}); this.setState({editing: false});
this.resetValue() this.resetValue();
}} }}
data-wd-key={this.props["data-wd-key"] + "-text"} data-wd-key={this.props["data-wd-key"] + "-text"}
/> />
</div> </div>;
} }
else { else {
const value = this.state.editing ? this.state.dirtyValue : this.state.value; const value = this.state.editing ? this.state.dirtyValue : this.state.value;
return <input return <input
aria-label={this.props['aria-label']} aria-label={this.props["aria-label"]}
spellCheck="false" spellCheck="false"
className="maputnik-number" className="maputnik-number"
placeholder={this.props.default?.toString()} placeholder={this.props.default?.toString()}
@@ -240,7 +240,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
onBlur={this.resetValue} onBlur={this.resetValue}
required={this.props.required} required={this.props.required}
data-wd-key={this.props["data-wd-key"]} data-wd-key={this.props["data-wd-key"]}
/> />;
} }
} }
} }
+4 -4
View File
@@ -1,4 +1,4 @@
import React from 'react' import React from "react";
export type InputSelectProps = { export type InputSelectProps = {
value: string value: string
@@ -7,7 +7,7 @@ export type InputSelectProps = {
style?: object style?: object
onChange(value: string | [string, any]): unknown onChange(value: string | [string, any]): unknown
title?: string title?: string
'aria-label'?: string "aria-label"?: string
}; };
export default class InputSelect extends React.Component<InputSelectProps> { export default class InputSelect extends React.Component<InputSelectProps> {
@@ -24,9 +24,9 @@ export default class InputSelect extends React.Component<InputSelectProps> {
title={this.props.title} title={this.props.title}
value={this.props.value} value={this.props.value}
onChange={e => this.props.onChange(e.target.value)} onChange={e => this.props.onChange(e.target.value)}
aria-label={this.props['aria-label']} aria-label={this.props["aria-label"]}
> >
{ options.map(([val, label]) => <option key={val} value={val}>{label}</option>) } { options.map(([val, label]) => <option key={val} value={val}>{label}</option>) }
</select> </select>;
} }
} }
+103 -75
View File
@@ -1,24 +1,26 @@
import React, { ReactElement } from 'react' import React, { type ReactElement } from "react";
import InputColor, { InputColorProps } from './InputColor' import InputColor, { type InputColorProps } from "./InputColor";
import InputNumber, { InputNumberProps } from './InputNumber' import InputNumber, { type InputNumberProps } from "./InputNumber";
import InputCheckbox, { InputCheckboxProps } from './InputCheckbox' import InputCheckbox, { type InputCheckboxProps } from "./InputCheckbox";
import InputString, { InputStringProps } from './InputString' import InputString, { type InputStringProps } from "./InputString";
import InputArray, { FieldArrayProps } from './InputArray' import InputArray, { type InputArrayProps } from "./InputArray";
import InputDynamicArray, { FieldDynamicArrayProps } from './InputDynamicArray' import InputDynamicArray, { type InputDynamicArrayProps } from "./InputDynamicArray";
import InputFont, { FieldFontProps } from './InputFont' import InputFont, { type InputFontProps } from "./InputFont";
import InputAutocomplete, { InputAutocompleteProps } from './InputAutocomplete' import InputAutocomplete, { type InputAutocompleteProps } from "./InputAutocomplete";
import InputEnum, { InputEnumProps } from './InputEnum' import InputEnum, { type InputEnumProps } from "./InputEnum";
import capitalize from 'lodash.capitalize' import capitalize from "lodash.capitalize";
const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image'] const iconProperties = ["background-pattern", "fill-pattern", "line-pattern", "fill-extrusion-pattern", "icon-image"];
export type SpecFieldProps = { export type FieldSpecType = "number" | "enum" | "resolvedImage" | "formatted" | "string" | "color" | "boolean" | "array" | "numberArray" | "padding" | "colorArray" | "variableAnchorOffsetCollection";
export type InputSpecProps = {
onChange?(fieldName: string | undefined, value: number | undefined | (string | number | undefined)[]): unknown onChange?(fieldName: string | undefined, value: number | undefined | (string | number | undefined)[]): unknown
fieldName?: string fieldName?: string
fieldSpec?: { fieldSpec?: {
default?: unknown default?: unknown
type?: 'number' | 'enum' | 'resolvedImage' | 'formatted' | 'string' | 'color' | 'boolean' | 'array' type?: FieldSpecType
minimum?: number minimum?: number
maximum?: number maximum?: number
values?: unknown[] values?: unknown[]
@@ -28,7 +30,7 @@ export type SpecFieldProps = {
value?: string | number | unknown[] | boolean value?: string | number | unknown[] | boolean
/** Override the style of the field */ /** Override the style of the field */
style?: object style?: object
'aria-label'?: string "aria-label"?: string
error?: unknown[] error?: unknown[]
label?: string label?: string
action?: ReactElement action?: ReactElement
@@ -37,7 +39,7 @@ export type SpecFieldProps = {
/** Display any field from the Maplibre GL style spec and /** Display any field from the Maplibre GL style spec and
* choose the correct field component based on the @{fieldSpec} * choose the correct field component based on the @{fieldSpec}
* to display @{value}. */ * to display @{value}. */
export default class SpecField extends React.Component<SpecFieldProps> { export default class InputSpec extends React.Component<InputSpecProps> {
childNodes() { childNodes() {
const commonProps = { const commonProps = {
@@ -51,70 +53,96 @@ export default class SpecField extends React.Component<SpecFieldProps> {
name: this.props.fieldName, name: this.props.fieldName,
"data-wd-key": "spec-field-input:" + this.props.fieldName, "data-wd-key": "spec-field-input:" + this.props.fieldName,
onChange: (newValue: number | undefined | (string | number | undefined)[]) => this.props.onChange!(this.props.fieldName, newValue), onChange: (newValue: number | undefined | (string | number | undefined)[]) => this.props.onChange!(this.props.fieldName, newValue),
'aria-label': this.props['aria-label'], "aria-label": this.props["aria-label"],
} };
switch(this.props.fieldSpec?.type) { switch(this.props.fieldSpec?.type) {
case 'number': return ( case "number": return (
<InputNumber <InputNumber
{...commonProps as InputNumberProps} {...commonProps as InputNumberProps}
min={this.props.fieldSpec.minimum} min={this.props.fieldSpec.minimum}
max={this.props.fieldSpec.maximum} max={this.props.fieldSpec.maximum}
/> />
) );
case 'enum': { case "enum": {
const options = Object.keys(this.props.fieldSpec.values || []).map(v => [v, capitalize(v)]) const options = Object.keys(this.props.fieldSpec.values || []).map(v => [v, capitalize(v)]);
return <InputEnum return <InputEnum
{...commonProps as Omit<InputEnumProps, "options">} {...commonProps as Omit<InputEnumProps, "options">}
options={options} options={options}
/> />;
}
case 'resolvedImage':
case 'formatted':
case 'string':
if (iconProperties.indexOf(this.props.fieldName!) >= 0) {
const options = this.props.fieldSpec.values || [];
return <InputAutocomplete
{...commonProps as Omit<InputAutocompleteProps, "options">}
options={options.map(f => [f, f])}
/>
} else {
return <InputString
{...commonProps as InputStringProps}
/>
} }
case 'color': return ( case "resolvedImage":
<InputColor case "formatted":
{...commonProps as InputColorProps} case "string":
/> if (iconProperties.indexOf(this.props.fieldName!) >= 0) {
) const options = this.props.fieldSpec.values || [];
case 'boolean': return ( return <InputAutocomplete
<InputCheckbox {...commonProps as Omit<InputAutocompleteProps, "options">}
{...commonProps as InputCheckboxProps} options={options.map(f => [f, f])}
/> />;
)
case 'array':
if(this.props.fieldName === 'text-font') {
return <InputFont
{...commonProps as FieldFontProps}
fonts={this.props.fieldSpec.values}
/>
} else {
if (this.props.fieldSpec.length) {
return <InputArray
{...commonProps as FieldArrayProps}
type={this.props.fieldSpec.value}
length={this.props.fieldSpec.length}
/>
} else { } else {
return <InputDynamicArray return <InputString
{...commonProps as FieldDynamicArrayProps} {...commonProps as InputStringProps}
fieldSpec={this.props.fieldSpec} />;
type={this.props.fieldSpec.value as FieldDynamicArrayProps['type']}
/>
} }
} case "color": return (
default: return null <InputColor
{...commonProps as InputColorProps}
/>
);
case "boolean": return (
<InputCheckbox
{...commonProps as InputCheckboxProps}
/>
);
case "array":
if(this.props.fieldName === "text-font") {
return <InputFont
{...commonProps as InputFontProps}
fonts={this.props.fieldSpec.values}
/>;
} else {
if (this.props.fieldSpec.length) {
return <InputArray
{...commonProps as InputArrayProps}
type={this.props.fieldSpec.value}
length={this.props.fieldSpec.length}
/>;
} else {
return <InputDynamicArray
{...commonProps as InputDynamicArrayProps}
fieldSpec={this.props.fieldSpec}
type={this.props.fieldSpec.value as InputDynamicArrayProps["type"]}
/>;
}
}
case "numberArray": return (
<InputDynamicArray
{...commonProps as InputDynamicArrayProps}
fieldSpec={this.props.fieldSpec}
type="number"
value={(Array.isArray(this.props.value) ? this.props.value : [this.props.value]) as (string | number | undefined)[]}
/>
);
case "colorArray": return (
<InputDynamicArray
{...commonProps as InputDynamicArrayProps}
fieldSpec={this.props.fieldSpec}
type="color"
value={(Array.isArray(this.props.value) ? this.props.value : [this.props.value]) as (string | number | undefined)[]}
/>
);
case "padding": return (
<InputArray
{...commonProps as InputArrayProps}
type="number"
value={(Array.isArray(this.props.value) ? this.props.value : [this.props.value]) as (string | number | undefined)[]}
length={4}
/>
);
default:
console.warn(`No proper field input for ${this.props.fieldName} type: ${this.props.fieldSpec?.type}`);
return null;
} }
} }
+11 -11
View File
@@ -1,4 +1,4 @@
import React from 'react' import React from "react";
export type InputStringProps = { export type InputStringProps = {
"data-wd-key"?: string "data-wd-key"?: string
@@ -11,26 +11,26 @@ export type InputStringProps = {
required?: boolean required?: boolean
disabled?: boolean disabled?: boolean
spellCheck?: boolean spellCheck?: boolean
'aria-label'?: string "aria-label"?: string
title?: string title?: string
}; };
type InputStringState = { type InputStringState = {
editing: boolean editing: boolean
value?: string value?: string
} };
export default class InputString extends React.Component<InputStringProps, InputStringState> { export default class InputString extends React.Component<InputStringProps, InputStringState> {
static defaultProps = { static defaultProps = {
onInput: () => {}, onInput: () => {},
} };
constructor(props: InputStringProps) { constructor(props: InputStringProps) {
super(props) super(props);
this.state = { this.state = {
editing: false, editing: false,
value: props.value || '' value: props.value || ""
} };
} }
static getDerivedStateFromProps(props: Readonly<InputStringProps>, state: InputStringState) { static getDerivedStateFromProps(props: Readonly<InputStringProps>, state: InputStringState) {
@@ -47,17 +47,17 @@ export default class InputString extends React.Component<InputStringProps, Input
let classes; let classes;
if(this.props.multi) { if(this.props.multi) {
tag = "textarea" tag = "textarea";
classes = [ classes = [
"maputnik-string", "maputnik-string",
"maputnik-string--multi" "maputnik-string--multi"
] ];
} }
else { else {
tag = "input" tag = "input";
classes = [ classes = [
"maputnik-string" "maputnik-string"
] ];
} }
if(this.props.disabled) { if(this.props.disabled) {
+17 -17
View File
@@ -1,8 +1,8 @@
import React from 'react' import React, { type JSX } from "react";
import InputString from './InputString' import InputString from "./InputString";
import SmallError from './SmallError' import SmallError from "./SmallError";
import { Trans, WithTranslation, withTranslation } from 'react-i18next'; import { Trans, type WithTranslation, withTranslation } from "react-i18next";
import { TFunction } from 'i18next'; import { type TFunction } from "i18next";
function validate(url: string, t: TFunction): JSX.Element | undefined { function validate(url: string, t: TFunction): JSX.Element | undefined {
if (url === "") { if (url === "") {
@@ -63,23 +63,23 @@ export type FieldUrlProps = {
onInput?(...args: unknown[]): unknown onInput?(...args: unknown[]): unknown
multi?: boolean multi?: boolean
required?: boolean required?: boolean
'aria-label'?: string "aria-label"?: string
type?: string type?: string
className?: string className?: string
}; };
type FieldUrlInternalProps = FieldUrlProps & WithTranslation; type InputUrlInternalProps = FieldUrlProps & WithTranslation;
type FieldUrlState = { type InputUrlState = {
error?: React.ReactNode error?: React.ReactNode
} };
class FieldUrlInternal extends React.Component<FieldUrlInternalProps, FieldUrlState> { class InputUrlInternal extends React.Component<InputUrlInternalProps, InputUrlState> {
static defaultProps = { static defaultProps = {
onInput: () => {}, onInput: () => {},
} };
constructor (props: FieldUrlInternalProps) { constructor (props: InputUrlInternalProps) {
super(props); super(props);
this.state = { this.state = {
error: validate(props.value, props.t), error: validate(props.value, props.t),
@@ -91,14 +91,14 @@ class FieldUrlInternal extends React.Component<FieldUrlInternalProps, FieldUrlSt
error: validate(url, this.props.t), error: validate(url, this.props.t),
}); });
if (this.props.onInput) this.props.onInput(url); if (this.props.onInput) this.props.onInput(url);
} };
onChange = (url: string) => { onChange = (url: string) => {
this.setState({ this.setState({
error: validate(url, this.props.t), error: validate(url, this.props.t),
}); });
this.props.onChange(url); this.props.onChange(url);
} };
render () { render () {
return ( return (
@@ -107,7 +107,7 @@ class FieldUrlInternal extends React.Component<FieldUrlInternalProps, FieldUrlSt
{...this.props} {...this.props}
onInput={this.onInput} onInput={this.onInput}
onChange={this.onChange} onChange={this.onChange}
aria-label={this.props['aria-label']} aria-label={this.props["aria-label"]}
/> />
{this.state.error} {this.state.error}
</div> </div>
@@ -115,5 +115,5 @@ class FieldUrlInternal extends React.Component<FieldUrlInternalProps, FieldUrlSt
} }
} }
const FieldUrl = withTranslation()(FieldUrlInternal); const InputUrl = withTranslation()(InputUrlInternal);
export default FieldUrl; export default InputUrl;
+138 -137
View File
@@ -1,35 +1,35 @@
import React, {type JSX} from 'react' import React, {type JSX} from "react";
import { Wrapper, Button, Menu, MenuItem } from 'react-aria-menubutton' import { Wrapper, Button, Menu, MenuItem } from "react-aria-menubutton";
import {Accordion} from 'react-accessible-accordion'; import {Accordion} from "react-accessible-accordion";
import {MdMoreVert} from 'react-icons/md' import {MdMoreVert} from "react-icons/md";
import { IconContext } from 'react-icons' import { IconContext } from "react-icons";
import {BackgroundLayerSpecification, LayerSpecification, SourceSpecification} from 'maplibre-gl'; import {type BackgroundLayerSpecification, type LayerSpecification, type SourceSpecification} from "maplibre-gl";
import {v8} from '@maplibre/maplibre-gl-style-spec'; import {v8} from "@maplibre/maplibre-gl-style-spec";
import FieldJson from './FieldJson' import FieldJson from "./FieldJson";
import FilterEditor from './FilterEditor' import FilterEditor from "./FilterEditor";
import PropertyGroup from './PropertyGroup' import PropertyGroup from "./PropertyGroup";
import LayerEditorGroup from './LayerEditorGroup' import LayerEditorGroup from "./LayerEditorGroup";
import FieldType from './FieldType' import FieldType from "./FieldType";
import FieldId from './FieldId' import FieldId from "./FieldId";
import FieldMinZoom from './FieldMinZoom' import FieldMinZoom from "./FieldMinZoom";
import FieldMaxZoom from './FieldMaxZoom' import FieldMaxZoom from "./FieldMaxZoom";
import FieldComment from './FieldComment' import FieldComment from "./FieldComment";
import FieldSource from './FieldSource' import FieldSource from "./FieldSource";
import FieldSourceLayer from './FieldSourceLayer' import FieldSourceLayer from "./FieldSourceLayer";
import { changeType, changeProperty } from '../libs/layer' import { changeType, changeProperty } from "../libs/layer";
import {formatLayerId} from '../libs/format'; import {formatLayerId} from "../libs/format";
import { WithTranslation, withTranslation } from 'react-i18next'; import { type WithTranslation, withTranslation } from "react-i18next";
import { TFunction } from 'i18next'; import { type TFunction } from "i18next";
import { NON_SOURCE_LAYERS } from '../libs/non-source-layers'; import { NON_SOURCE_LAYERS } from "../libs/non-source-layers";
import { OnMoveLayerCallback } from '../libs/definitions'; import { type OnMoveLayerCallback } from "../libs/definitions";
type MaputnikLayoutGroup = { type MaputnikLayoutGroup = {
id: string; id: string;
title: string; title: string;
type: string; type: string;
fields: string[]; fields: string[];
} };
function getLayoutForSymbolType(t: TFunction): MaputnikLayoutGroup[] { function getLayoutForSymbolType(t: TFunction): MaputnikLayoutGroup[] {
const groups: MaputnikLayoutGroup[] = []; const groups: MaputnikLayoutGroup[] = [];
@@ -68,7 +68,7 @@ function getLayoutForSymbolType(t: TFunction): MaputnikLayoutGroup[] {
function getLayoutForType(type: LayerSpecification["type"], t: TFunction): MaputnikLayoutGroup[] { function getLayoutForType(type: LayerSpecification["type"], t: TFunction): MaputnikLayoutGroup[] {
if (Object.keys(v8.layer.type.values).indexOf(type) < 0) { if (Object.keys(v8.layer.type.values).indexOf(type) < 0) {
return [] return [];
} }
if (type === "symbol") { if (type === "symbol") {
return getLayoutForSymbolType(t); return getLayoutForSymbolType(t);
@@ -95,30 +95,30 @@ function getLayoutForType(type: LayerSpecification["type"], t: TFunction): Maput
function layoutGroups(layerType: LayerSpecification["type"], t: TFunction): {id: string, title: string, type: string, fields?: string[]}[] { function layoutGroups(layerType: LayerSpecification["type"], t: TFunction): {id: string, title: string, type: string, fields?: string[]}[] {
const layerGroup = { const layerGroup = {
id: 'layer', id: "layer",
title: t('Layer'), title: t("Layer"),
type: 'layer' type: "layer"
} };
const filterGroup = { const filterGroup = {
id: 'filter', id: "filter",
title: t('Filter'), title: t("Filter"),
type: 'filter' type: "filter"
} };
const editorGroup = { const editorGroup = {
id: 'jsoneditor', id: "jsoneditor",
title: t('JSON Editor'), title: t("JSON Editor"),
type: 'jsoneditor' type: "jsoneditor"
} };
return [layerGroup, filterGroup] return [layerGroup, filterGroup]
.concat(getLayoutForType(layerType, t)) .concat(getLayoutForType(layerType, t))
.concat([editorGroup]) .concat([editorGroup]);
} }
type LayerEditorInternalProps = { type LayerEditorInternalProps = {
layer: LayerSpecification layer: LayerSpecification
sources: {[key: string]: SourceSpecification & {layers: string[]}} sources: {[key: string]: SourceSpecification & {layers: string[]}}
vectorLayers: {[key: string]: any} vectorLayers: {[key: string]: any}
spec: object spec: any
onLayerChanged(...args: unknown[]): unknown onLayerChanged(...args: unknown[]): unknown
onLayerIdChange(...args: unknown[]): unknown onLayerIdChange(...args: unknown[]): unknown
onMoveLayer: OnMoveLayerCallback onMoveLayer: OnMoveLayerCallback
@@ -141,25 +141,25 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
onLayerChanged: () => {}, onLayerChanged: () => {},
onLayerIdChange: () => {}, onLayerIdChange: () => {},
onLayerDestroyed: () => {}, onLayerDestroyed: () => {},
} };
constructor(props: LayerEditorInternalProps) { constructor(props: LayerEditorInternalProps) {
super(props) super(props);
const editorGroups: {[keys:string]: boolean} = {} const editorGroups: {[keys:string]: boolean} = {};
for (const group of layoutGroups(this.props.layer.type, props.t)) { for (const group of layoutGroups(this.props.layer.type, props.t)) {
editorGroups[group.title] = true editorGroups[group.title] = true;
} }
this.state = { editorGroups } this.state = { editorGroups };
} }
static getDerivedStateFromProps(props: Readonly<LayerEditorInternalProps>, state: LayerEditorState) { static getDerivedStateFromProps(props: Readonly<LayerEditorInternalProps>, state: LayerEditorState) {
const additionalGroups = { ...state.editorGroups } const additionalGroups = { ...state.editorGroups };
for (const group of getLayoutForType(props.layer.type, props.t)) { for (const group of getLayoutForType(props.layer.type, props.t)) {
if(!(group.title in additionalGroups)) { if(!(group.title in additionalGroups)) {
additionalGroups[group.title] = true additionalGroups[group.title] = true;
} }
} }
@@ -173,23 +173,23 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
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: string, active: boolean) { onGroupToggle(groupTitle: string, active: boolean) {
const changedActiveGroups = { const changedActiveGroups = {
...this.state.editorGroups, ...this.state.editorGroups,
[groupTitle]: active, [groupTitle]: active,
} };
this.setState({ this.setState({
editorGroups: changedActiveGroups editorGroups: changedActiveGroups
}) });
} }
renderGroupType(type: string, fields?: string[]): JSX.Element { renderGroupType(type: string, fields?: string[]): JSX.Element {
let comment = "" let comment = "";
if(this.props.layer.metadata) { if(this.props.layer.metadata) {
comment = (this.props.layer.metadata as any)['maputnik:comment'] comment = (this.props.layer.metadata as any)["maputnik:comment"];
} }
const {errors, layerIndex} = this.props; const {errors, layerIndex} = this.props;
@@ -204,7 +204,7 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
message: error.parsed.data.message message: error.parsed.data.message
}; };
} }
}) });
let sourceLayerIds; let sourceLayerIds;
const layer = this.props.layer as Exclude<LayerSpecification, BackgroundLayerSpecification>; const layer = this.props.layer as Exclude<LayerSpecification, BackgroundLayerSpecification>;
@@ -213,82 +213,82 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
} }
switch(type) { switch(type) {
case 'layer': return <div> case "layer": return <div>
<FieldId <FieldId
value={this.props.layer.id} value={this.props.layer.id}
wdKey="layer-editor.layer-id" wdKey="layer-editor.layer-id"
error={errorData.id} error={errorData.id}
onChange={newId => this.props.onLayerIdChange(this.props.layerIndex, this.props.layer.id, newId)} onChange={newId => this.props.onLayerIdChange(this.props.layerIndex, this.props.layer.id, newId)}
/>
<FieldType
disabled={true}
error={errorData.type}
value={this.props.layer.type}
onChange={newType => this.props.onLayerChanged(
this.props.layerIndex,
changeType(this.props.layer, newType)
)}
/>
{this.props.layer.type !== 'background' && <FieldSource
error={errorData.source}
sourceIds={Object.keys(this.props.sources!)}
value={this.props.layer.source}
onChange={v => this.changeProperty(null, 'source', v)}
/>
}
{!NON_SOURCE_LAYERS.includes(this.props.layer.type) &&
<FieldSourceLayer
error={errorData['source-layer']}
sourceLayerIds={sourceLayerIds}
value={(this.props.layer as any)['source-layer']}
onChange={v => this.changeProperty(null, 'source-layer', v)}
/> />
} <FieldType
<FieldMinZoom disabled={true}
error={errorData.minzoom} error={errorData.type}
value={this.props.layer.minzoom} value={this.props.layer.type}
onChange={v => this.changeProperty(null, 'minzoom', v)} onChange={newType => this.props.onLayerChanged(
/>
<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 as any).filter}
properties={this.props.vectorLayers[(this.props.layer as any)['source-layer']]}
onChange={f => this.changeProperty(null, 'filter', f)}
/>
</div>
</div>
case 'properties':
return <PropertyGroup
errors={errorData}
layer={this.props.layer}
groupFields={fields!}
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, this.props.layerIndex,
layer changeType(this.props.layer, newType)
); )}
}} />
/> {this.props.layer.type !== "background" && <FieldSource
default: return <></> error={errorData.source}
sourceIds={Object.keys(this.props.sources!)}
value={this.props.layer.source}
onChange={v => this.changeProperty(null, "source", v)}
/>
}
{!NON_SOURCE_LAYERS.includes(this.props.layer.type) &&
<FieldSourceLayer
error={errorData["source-layer"]}
sourceLayerIds={sourceLayerIds}
value={(this.props.layer as any)["source-layer"]}
onChange={v => this.changeProperty(null, "source-layer", v)}
/>
}
<FieldMinZoom
error={errorData.minzoom}
value={this.props.layer.minzoom}
onChange={v => this.changeProperty(null, "minzoom", v)}
/>
<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 as any).filter}
properties={this.props.vectorLayers[(this.props.layer as any)["source-layer"]]}
onChange={f => this.changeProperty(null, "filter", f)}
/>
</div>
</div>;
case "properties":
return <PropertyGroup
errors={errorData}
layer={this.props.layer}
groupFields={fields!}
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
);
}}
/>;
default: return <></>;
} }
} }
@@ -296,16 +296,16 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
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 t = this.props.t; const t = this.props.t;
const groupIds: string[] = []; const groupIds: string[] = [];
const layerType = this.props.layer.type const layerType = this.props.layer.type;
const groups = layoutGroups(layerType, t).filter(group => { const groups = layoutGroups(layerType, t).filter(group => {
return !(layerType === 'background' && group.type === 'source') return !(layerType === "background" && group.type === "source");
}).map(group => { }).map(group => {
const groupId = group.id; const groupId = group.id;
groupIds.push(groupId); groupIds.push(groupId);
@@ -318,10 +318,10 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
onActiveToggle={this.onGroupToggle.bind(this, group.title)} onActiveToggle={this.onGroupToggle.bind(this, group.title)}
> >
{this.renderGroupType(group.type, group.fields)} {this.renderGroupType(group.type, group.fields)}
</LayerEditorGroup> </LayerEditorGroup>;
}) });
const layout = this.props.layer.layout || {} const layout = this.props.layer.layout || {};
const items: {[key: string]: { const items: {[key: string]: {
text: string, text: string,
@@ -356,17 +356,18 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
handler: () => this.moveLayer(+1), handler: () => this.moveLayer(+1),
wdKey: "menu-move-layer-down" wdKey: "menu-move-layer-down"
} }
} };
function handleSelection(id: string, event: React.SyntheticEvent) { function handleSelection(id: string, event: React.SyntheticEvent) {
event.stopPropagation(); event.stopPropagation();
items[id].handler(); items[id].handler();
} }
return <IconContext.Provider value={{size: '14px', color: '#8e8e8e'}}> return <IconContext.Provider value={{size: "14px", color: "#8e8e8e"}}>
<section className="maputnik-layer-editor" <section className="maputnik-layer-editor"
role="main" role="main"
aria-label={t("Layer editor")} aria-label={t("Layer editor")}
data-wd-key="layer-editor"
> >
<header> <header>
<div className="layer-header"> <div className="layer-header">
@@ -394,7 +395,7 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
<MenuItem value={id} className='more-menu__menu__item' data-wd-key={item.wdKey}> <MenuItem value={id} className='more-menu__menu__item' data-wd-key={item.wdKey}>
{item.text} {item.text}
</MenuItem> </MenuItem>
</li> </li>;
})} })}
</ul> </ul>
</Menu> </Menu>
@@ -411,7 +412,7 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
{groups} {groups}
</Accordion> </Accordion>
</section> </section>
</IconContext.Provider> </IconContext.Provider>;
} }
} }
+7 -19
View File
@@ -1,15 +1,11 @@
import React from 'react' import React from "react";
import Icon from '@mdi/react' import {MdArrowDropDown, MdArrowDropUp} from "react-icons/md";
import {
mdiMenuDown,
mdiMenuUp
} from '@mdi/js';
import { import {
AccordionItem, AccordionItem,
AccordionItemHeading, AccordionItemHeading,
AccordionItemButton, AccordionItemButton,
AccordionItemPanel, AccordionItemPanel,
} from 'react-accessible-accordion'; } from "react-accessible-accordion";
type LayerEditorGroupProps = { type LayerEditorGroupProps = {
@@ -30,22 +26,14 @@ export default class LayerEditorGroup extends React.Component<LayerEditorGroupPr
onClick={_e => this.props.onActiveToggle(!this.props.isActive)} onClick={_e => this.props.onActiveToggle(!this.props.isActive)}
> >
<AccordionItemButton className="maputnik-layer-editor-group__button"> <AccordionItemButton className="maputnik-layer-editor-group__button">
<span style={{flexGrow: 1}}>{this.props.title}</span> <span style={{flexGrow: 1, alignContent: "center"}}>{this.props.title}</span>
<Icon <MdArrowDropUp size={"2em"} className="maputnik-layer-editor-group__button__icon maputnik-layer-editor-group__button__icon--up"></MdArrowDropUp>
path={mdiMenuUp} <MdArrowDropDown size={"2em"} className="maputnik-layer-editor-group__button__icon maputnik-layer-editor-group__button__icon--down"></MdArrowDropDown>
size={1}
className="maputnik-layer-editor-group__button__icon maputnik-layer-editor-group__button__icon--up"
/>
<Icon
path={mdiMenuDown}
size={1}
className="maputnik-layer-editor-group__button__icon maputnik-layer-editor-group__button__icon--down"
/>
</AccordionItemButton> </AccordionItemButton>
</AccordionItemHeading> </AccordionItemHeading>
<AccordionItemPanel> <AccordionItemPanel>
{this.props.children} {this.props.children}
</AccordionItemPanel> </AccordionItemPanel>
</AccordionItem> </AccordionItem>;
} }
} }
+65 -64
View File
@@ -1,34 +1,34 @@
import React, {type JSX} from 'react' import React, {type JSX} from "react";
import classnames from 'classnames' import classnames from "classnames";
import lodash from 'lodash'; import lodash from "lodash";
import { import {
DndContext, DndContext,
PointerSensor, PointerSensor,
useSensor, useSensor,
useSensors, useSensors,
closestCenter, closestCenter,
DragEndEvent, type DragEndEvent,
} from '@dnd-kit/core'; } from "@dnd-kit/core";
import { import {
SortableContext, SortableContext,
verticalListSortingStrategy, verticalListSortingStrategy,
} from '@dnd-kit/sortable'; } from "@dnd-kit/sortable";
import LayerListGroup from './LayerListGroup' import LayerListGroup from "./LayerListGroup";
import LayerListItem from './LayerListItem' import LayerListItem from "./LayerListItem";
import ModalAdd from './ModalAdd' import ModalAdd from "./modals/ModalAdd";
import type {LayerSpecification, SourceSpecification} from 'maplibre-gl'; import type {LayerSpecification, SourceSpecification} from "maplibre-gl";
import generateUniqueId from '../libs/document-uid'; import generateUniqueId from "../libs/document-uid";
import { findClosestCommonPrefix, layerPrefix } from '../libs/layer'; import { findClosestCommonPrefix, layerPrefix } from "../libs/layer";
import { WithTranslation, withTranslation } from 'react-i18next'; import { type WithTranslation, withTranslation } from "react-i18next";
import { OnMoveLayerCallback } from '../libs/definitions'; import { type OnMoveLayerCallback } from "../libs/definitions";
type LayerListContainerProps = { type LayerListContainerProps = {
layers: LayerSpecification[] layers: LayerSpecification[]
selectedLayerIndex: number selectedLayerIndex: number
onLayersChange(layers: LayerSpecification[]): unknown onLayersChange(layers: LayerSpecification[]): unknown
onLayerSelect(...args: unknown[]): unknown onLayerSelect(index: number): void;
onLayerDestroy?(...args: unknown[]): unknown onLayerDestroy?(...args: unknown[]): unknown
onLayerCopy(...args: unknown[]): unknown onLayerCopy(...args: unknown[]): unknown
onLayerVisibilityToggle(...args: unknown[]): unknown onLayerVisibilityToggle(...args: unknown[]): unknown
@@ -48,9 +48,9 @@ type LayerListContainerState = {
class LayerListContainerInternal extends React.Component<LayerListContainerInternalProps, LayerListContainerState> { class LayerListContainerInternal extends React.Component<LayerListContainerInternalProps, LayerListContainerState> {
static defaultProps = { static defaultProps = {
onLayerSelect: () => {}, onLayerSelect: () => {},
} };
selectedItemRef: React.RefObject<any>; selectedItemRef: React.RefObject<any>;
scrollContainerRef: React.RefObject<HTMLElement>; scrollContainerRef: React.RefObject<HTMLElement | null>;
constructor(props: LayerListContainerInternalProps) { constructor(props: LayerListContainerInternalProps) {
super(props); super(props);
@@ -65,7 +65,7 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
isOpen: { isOpen: {
add: false, add: false,
} }
} };
} }
toggleModal(modalName: string) { toggleModal(modalName: string) {
@@ -78,74 +78,74 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
...this.state.isOpen, ...this.state.isOpen,
[modalName]: !this.state.isOpen[modalName] [modalName]: !this.state.isOpen[modalName]
} }
}) });
} }
toggleLayers = () => { toggleLayers = () => {
let idx = 0 let idx = 0;
const newGroups: {[key:string]: boolean} = {} const newGroups: {[key:string]: boolean} = {};
this.groupedLayers().forEach(layers => { this.groupedLayers().forEach(layers => {
const groupPrefix = layerPrefix(layers[0].id) const groupPrefix = layerPrefix(layers[0].id);
const lookupKey = [groupPrefix, idx].join('-') 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(): (LayerSpecification & {key: string})[][] { groupedLayers(): (LayerSpecification & {key: string})[][] {
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(previousLayer && layerPrefix(previousLayer.id) == layerPrefix(layer.id)) {
const lastGroup = groups[groups.length - 1] const lastGroup = groups[groups.length - 1];
lastGroup.push(layer) lastGroup.push(layer);
} else { } else {
groups.push([layer]) groups.push([layer]);
} }
} }
return groups return groups;
} }
toggleLayerGroup(groupPrefix: string, idx: number) { toggleLayerGroup(groupPrefix: string, idx: number) {
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: string, idx: number) { isCollapsed(groupPrefix: string, idx: number) {
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: LayerListContainerProps, nextState: LayerListContainerState) { shouldComponentUpdate (nextProps: LayerListContainerProps, nextState: LayerListContainerState) {
@@ -177,7 +177,7 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
const out = { const out = {
...props ...props
} as LayerListContainerProps & { layers?: any }; } as LayerListContainerProps & { layers?: any };
delete out['layers']; delete out["layers"];
return out; return out;
} }
@@ -200,7 +200,7 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
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) {
@@ -215,25 +215,25 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
render() { render() {
const listItems: JSX.Element[] = [] const listItems: JSX.Element[] = [];
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 = <LayerListGroup
data-wd-key={[groupPrefix, idx].join('-')} data-wd-key={[groupPrefix, idx].join("-")}
aria-controls={layers.map(l => l.key).join(" ")} aria-controls={layers.map(l => l.key).join(" ")}
key={`group-${groupPrefix}-${idx}`} key={`group-${groupPrefix}-${idx}`}
title={groupPrefix} title={groupPrefix}
isActive={!this.isCollapsed(groupPrefix, idx) || idx === this.props.selectedLayerIndex} isActive={!this.isCollapsed(groupPrefix, idx) || idx === this.props.selectedLayerIndex}
onActiveToggle={this.toggleLayerGroup.bind(this, groupPrefix, idx)} onActiveToggle={this.toggleLayerGroup.bind(this, groupPrefix, idx)}
/> />;
listItems.push(grp) 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 (
@@ -250,9 +250,9 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
const listItem = <LayerListItem const listItem = <LayerListItem
className={classnames({ className={classnames({
'maputnik-layer-list-item-collapsed': layers.length > 1 && this.isCollapsed(groupPrefix, groupIdx) && idx !== this.props.selectedLayerIndex, "maputnik-layer-list-item-collapsed": layers.length > 1 && this.isCollapsed(groupPrefix, groupIdx) && idx !== this.props.selectedLayerIndex,
'maputnik-layer-list-item-group-last': idxInGroup == layers.length - 1 && layers.length > 1, "maputnik-layer-list-item-group-last": idxInGroup == layers.length - 1 && layers.length > 1,
'maputnik-layer-list-item--error': !!layerError "maputnik-layer-list-item--error": !!layerError
})} })}
key={layer.key} key={layer.key}
id={layer.key} id={layer.key}
@@ -266,16 +266,17 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
onLayerCopy={this.props.onLayerCopy.bind(this)} onLayerCopy={this.props.onLayerCopy.bind(this)}
onLayerVisibilityToggle={this.props.onLayerVisibilityToggle.bind(this)} onLayerVisibilityToggle={this.props.onLayerVisibilityToggle.bind(this)}
{...additionalProps} {...additionalProps}
/> />;
listItems.push(listItem) listItems.push(listItem);
idx += 1 idx += 1;
}) });
}) });
const t = this.props.t; const t = this.props.t;
return <section return <section
className="maputnik-layer-list" className="maputnik-layer-list"
data-wd-key="layer-list"
role="complementary" role="complementary"
aria-label={t("Layers list")} aria-label={t("Layers list")}
ref={this.scrollContainerRef} ref={this.scrollContainerRef}
@@ -285,7 +286,7 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
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">
@@ -309,7 +310,7 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
<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">
{t("Add Layer")} {t("Add Layer")}
@@ -325,7 +326,7 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
{listItems} {listItems}
</ul> </ul>
</div> </div>
</section> </section>;
} }
} }
@@ -336,7 +337,7 @@ type LayerListProps = LayerListContainerProps & {
}; };
const LayerList: React.FC<LayerListProps> = (props) => { const LayerList: React.FC<LayerListProps> = (props) => {
const sensors = useSensors(useSensor(PointerSensor)); const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } }));
const handleDragEnd = (event: DragEndEvent) => { const handleDragEnd = (event: DragEndEvent) => {
const {active, over} = event; const {active, over} = event;
@@ -359,6 +360,6 @@ const LayerList: React.FC<LayerListProps> = (props) => {
</SortableContext> </SortableContext>
</DndContext> </DndContext>
); );
} };
export default LayerList; export default LayerList;
+5 -5
View File
@@ -1,12 +1,12 @@
import React from 'react' import React from "react";
import Collapser from './Collapser' import Collapser from "./Collapser";
type LayerListGroupProps = { type LayerListGroupProps = {
title: string title: string
"data-wd-key"?: string "data-wd-key"?: string
isActive: boolean isActive: boolean
onActiveToggle(...args: unknown[]): unknown onActiveToggle(...args: unknown[]): unknown
'aria-controls'?: string "aria-controls"?: string
}; };
export default class LayerListGroup extends React.Component<LayerListGroupProps> { export default class LayerListGroup extends React.Component<LayerListGroupProps> {
@@ -18,7 +18,7 @@ export default class LayerListGroup extends React.Component<LayerListGroupProps>
> >
<button <button
className="maputnik-layer-list-group-title" className="maputnik-layer-list-group-title"
aria-controls={this.props['aria-controls']} aria-controls={this.props["aria-controls"]}
aria-expanded={this.props.isActive} aria-expanded={this.props.isActive}
> >
{this.props.title} {this.props.title}
@@ -29,6 +29,6 @@ export default class LayerListGroup extends React.Component<LayerListGroupProps>
isCollapsed={this.props.isActive} isCollapsed={this.props.isActive}
/> />
</div> </div>
</li> </li>;
} }
} }
+22 -21
View File
@@ -1,11 +1,11 @@
import React from 'react' import React from "react";
import classnames from 'classnames' import classnames from "classnames";
import {MdContentCopy, MdVisibility, MdVisibilityOff, MdDelete} from 'react-icons/md' import {MdContentCopy, MdVisibility, MdVisibilityOff, MdDelete} from "react-icons/md";
import { IconContext } from 'react-icons' import { IconContext } from "react-icons";
import {useSortable} from '@dnd-kit/sortable' import {useSortable} from "@dnd-kit/sortable";
import {CSS} from '@dnd-kit/utilities' import {CSS} from "@dnd-kit/utilities";
import IconLayer from './IconLayer' import IconLayer from "./IconLayer";
type DraggableLabelProps = { type DraggableLabelProps = {
@@ -21,11 +21,12 @@ const DraggableLabel: React.FC<DraggableLabelProps> = (props) => {
<IconLayer <IconLayer
className="layer-handle__icon" className="layer-handle__icon"
type={props.layerType} type={props.layerType}
style={{ width: "1em", height: "1em", verticalAlign: "middle" }}
/> />
<button className="maputnik-layer-list-item-id"> <button className="maputnik-layer-list-item-id">
{props.layerId} {props.layerId}
</button> </button>
</div> </div>;
}; };
type IconActionProps = { type IconActionProps = {
@@ -39,17 +40,17 @@ type IconActionProps = {
class IconAction extends React.Component<IconActionProps> { class IconAction extends React.Component<IconActionProps> {
renderIcon() { renderIcon() {
switch(this.props.action) { switch(this.props.action) {
case 'duplicate': return <MdContentCopy /> case "duplicate": return <MdContentCopy />;
case 'show': return <MdVisibility /> case "show": return <MdVisibility />;
case 'hide': return <MdVisibilityOff /> case "hide": return <MdVisibilityOff />;
case 'delete': return <MdDelete /> case "delete": return <MdDelete />;
} }
} }
render() { render() {
const {classBlockName, classBlockModifier} = this.props; const {classBlockName, classBlockModifier} = this.props;
let classAdditions = ''; let classAdditions = "";
if (classBlockName) { if (classBlockName) {
classAdditions = `maputnik-layer-list-icon-action__${classBlockName}`; classAdditions = `maputnik-layer-list-icon-action__${classBlockName}`;
@@ -67,7 +68,7 @@ class IconAction extends React.Component<IconActionProps> {
aria-hidden="true" aria-hidden="true"
> >
{this.renderIcon()} {this.renderIcon()}
</button> </button>;
} }
} }
@@ -79,7 +80,7 @@ type LayerListItemProps = {
isSelected?: boolean isSelected?: boolean
visibility?: string visibility?: string
className?: string className?: string
onLayerSelect(...args: unknown[]): unknown onLayerSelect(index: number): void;
onLayerCopy?(...args: unknown[]): unknown onLayerCopy?(...args: unknown[]): unknown
onLayerDestroy?(...args: unknown[]): unknown onLayerDestroy?(...args: unknown[]): unknown
onLayerVisibilityToggle?(...args: unknown[]): unknown onLayerVisibilityToggle?(...args: unknown[]): unknown
@@ -88,7 +89,7 @@ type LayerListItemProps = {
const LayerListItem = React.forwardRef<HTMLLIElement, LayerListItemProps>((props, ref) => { const LayerListItem = React.forwardRef<HTMLLIElement, LayerListItemProps>((props, ref) => {
const { const {
isSelected = false, isSelected = false,
visibility = 'visible', visibility = "visible",
onLayerCopy = () => {}, onLayerCopy = () => {},
onLayerDestroy = () => {}, onLayerDestroy = () => {},
onLayerVisibilityToggle = () => {}, onLayerVisibilityToggle = () => {},
@@ -109,12 +110,12 @@ const LayerListItem = React.forwardRef<HTMLLIElement, LayerListItemProps>((props
opacity: isDragging ? 0.5 : 1, opacity: isDragging ? 0.5 : 1,
}; };
const visibilityAction = visibility === 'visible' ? 'show' : 'hide'; const visibilityAction = visibility === "visible" ? "show" : "hide";
// Cast ref to MutableRefObject since we know from the codebase that's what's always passed // Cast ref to MutableRefObject since we know from the codebase that's what's always passed
const refObject = ref as React.MutableRefObject<HTMLLIElement | null> | null; const refObject = ref as React.MutableRefObject<HTMLLIElement | null> | null;
return <IconContext.Provider value={{size: '14px'}}> return <IconContext.Provider value={{size: "14px"}}>
<li <li
ref={(node) => { ref={(node) => {
setNodeRef(node); setNodeRef(node);
@@ -140,13 +141,13 @@ const LayerListItem = React.forwardRef<HTMLLIElement, LayerListItemProps>((props
<span style={{flexGrow: 1}} /> <span style={{flexGrow: 1}} />
<IconAction <IconAction
wdKey={"layer-list-item:" + props.layerId+":delete"} wdKey={"layer-list-item:" + props.layerId+":delete"}
action={'delete'} action={"delete"}
classBlockName="delete" classBlockName="delete"
onClick={_e => onLayerDestroy!(props.layerIndex)} onClick={_e => onLayerDestroy!(props.layerIndex)}
/> />
<IconAction <IconAction
wdKey={"layer-list-item:" + props.layerId+":copy"} wdKey={"layer-list-item:" + props.layerId+":copy"}
action={'duplicate'} action={"duplicate"}
classBlockName="duplicate" classBlockName="duplicate"
onClick={_e => onLayerCopy!(props.layerIndex)} onClick={_e => onLayerCopy!(props.layerIndex)}
/> />
@@ -158,7 +159,7 @@ const LayerListItem = React.forwardRef<HTMLLIElement, LayerListItemProps>((props
onClick={_e => onLayerVisibilityToggle!(props.layerIndex)} onClick={_e => onLayerVisibilityToggle!(props.layerIndex)}
/> />
</li> </li>
</IconContext.Provider> </IconContext.Provider>;
}); });
export default LayerListItem; export default LayerListItem;
+65 -77
View File
@@ -1,67 +1,56 @@
import React, {type JSX} from 'react' import React from "react";
import {createRoot} from 'react-dom/client' import {createRoot} from "react-dom/client";
import MapLibreGl, {LayerSpecification, LngLat, Map, MapOptions, SourceSpecification, StyleSpecification} from 'maplibre-gl' import MapLibreGl, {type LayerSpecification, type LngLat, type Map, type MapOptions, type SourceSpecification, type StyleSpecification} from "maplibre-gl";
import MaplibreInspect from '@maplibre/maplibre-gl-inspect' import MaplibreInspect from "@maplibre/maplibre-gl-inspect";
import colors from '@maplibre/maplibre-gl-inspect/lib/colors' import colors from "@maplibre/maplibre-gl-inspect/lib/colors";
import MapMaplibreGlLayerPopup from './MapMaplibreGlLayerPopup' import MapMaplibreGlLayerPopup from "./MapMaplibreGlLayerPopup";
import MapMaplibreGlFeaturePropertyPopup, { InspectFeature } from './MapMaplibreGlFeaturePropertyPopup' import MapMaplibreGlFeaturePropertyPopup, { type InspectFeature } from "./MapMaplibreGlFeaturePropertyPopup";
import Color from 'color' import Color from "color";
import ZoomControl from '../libs/zoomcontrol' import ZoomControl from "../libs/zoomcontrol";
import { HighlightedLayer, colorHighlightedLayer } from '../libs/highlight' import { type HighlightedLayer, colorHighlightedLayer } from "../libs/highlight";
import 'maplibre-gl/dist/maplibre-gl.css' import "maplibre-gl/dist/maplibre-gl.css";
import '../maplibregl.css' import "../maplibregl.css";
import '../libs/maplibre-rtl' import "../libs/maplibre-rtl";
import MaplibreGeocoder, { MaplibreGeocoderApi, MaplibreGeocoderApiConfig } from '@maplibre/maplibre-gl-geocoder'; import MaplibreGeocoder, { type MaplibreGeocoderApi, type MaplibreGeocoderApiConfig } from "@maplibre/maplibre-gl-geocoder";
import '@maplibre/maplibre-gl-geocoder/dist/maplibre-gl-geocoder.css'; import "@maplibre/maplibre-gl-geocoder/dist/maplibre-gl-geocoder.css";
import { withTranslation, WithTranslation } from 'react-i18next' import { withTranslation, type WithTranslation } from "react-i18next";
import i18next from 'i18next' import i18next from "i18next";
import { Protocol } from "pmtiles"; import { Protocol } from "pmtiles";
function renderPopup(
popupElement: JSX.Element,
mountNode: HTMLElement,
popup: MapLibreGl.Popup
): HTMLElement {
const root = createRoot(mountNode);
popup.once('close', () => root.unmount());
root.render(popupElement);
return mountNode as HTMLElement;
}
function buildInspectStyle(originalMapStyle: StyleSpecification, coloredLayers: HighlightedLayer[], highlightedLayer?: HighlightedLayer) { function buildInspectStyle(originalMapStyle: StyleSpecification, coloredLayers: HighlightedLayer[], highlightedLayer?: HighlightedLayer) {
const backgroundLayer = { const backgroundLayer = {
"id": "background", "id": "background",
"type": "background", "type": "background",
"paint": { "paint": {
"background-color": '#1c1f24', "background-color": "#1c1f24",
} }
} as LayerSpecification } as LayerSpecification;
const layer = colorHighlightedLayer(highlightedLayer) const layer = colorHighlightedLayer(highlightedLayer);
if(layer) { if(layer) {
coloredLayers.push(layer) coloredLayers.push(layer);
} }
const sources: {[key:string]: SourceSpecification} = {} const sources: {[key:string]: SourceSpecification} = {};
Object.keys(originalMapStyle.sources).forEach(sourceId => { Object.keys(originalMapStyle.sources).forEach(sourceId => {
const source = originalMapStyle.sources[sourceId] const source = originalMapStyle.sources[sourceId];
if(source.type !== 'raster' && source.type !== 'raster-dem') { if(source.type !== "raster" && source.type !== "raster-dem") {
sources[sourceId] = source sources[sourceId] = source;
} }
}) });
const inspectStyle = { const inspectStyle = {
...originalMapStyle, ...originalMapStyle,
sources: sources, sources: sources,
layers: [backgroundLayer].concat(coloredLayers as LayerSpecification[]) layers: [backgroundLayer].concat(coloredLayers as LayerSpecification[])
} };
return inspectStyle return inspectStyle;
} }
type MapMaplibreGlInternalProps = { type MapMaplibreGlInternalProps = {
onDataChange?(event: {map: Map | null}): unknown onDataChange?(event: {map: Map | null}): unknown
onLayerSelect(...args: unknown[]): unknown onLayerSelect(index: number): void
mapStyle: StyleSpecification mapStyle: StyleSpecification
inspectModeEnabled: boolean inspectModeEnabled: boolean
highlightedLayer?: HighlightedLayer highlightedLayer?: HighlightedLayer
@@ -89,20 +78,20 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
onLayerSelect: () => {}, onLayerSelect: () => {},
onChange: () => {}, onChange: () => {},
options: {} as MapOptions, options: {} as MapOptions,
} };
container: HTMLDivElement | null = null container: HTMLDivElement | null = null;
constructor(props: MapMaplibreGlInternalProps) { constructor(props: MapMaplibreGlInternalProps) {
super(props) super(props);
this.state = { this.state = {
map: null, map: null,
inspect: null, inspect: null,
geocoder: null, geocoder: null,
zoomControl: null, zoomControl: null,
} };
i18next.on('languageChanged', () => { i18next.on("languageChanged", () => {
this.forceUpdate(); this.forceUpdate();
}) });
} }
@@ -131,7 +120,7 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
} }
if(this.state.inspect && this.props.inspectModeEnabled !== this.state.inspect._showInspectMap) { if(this.state.inspect && this.props.inspectModeEnabled !== this.state.inspect._showInspectMap) {
this.state.inspect.toggleInspector() this.state.inspect.toggleInspector();
} }
if (this.state.inspect && this.props.inspectModeEnabled) { if (this.state.inspect && this.props.inspectModeEnabled) {
this.state.inspect.setOriginalStyle(styleWithTokens); this.state.inspect.setOriginalStyle(styleWithTokens);
@@ -163,7 +152,7 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
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!;
@@ -173,12 +162,13 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
const geocoder = this.initGeocoder(map); const geocoder = this.initGeocoder(map);
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 root = createRoot(tmpNode);
const inspectPopup = new MapLibreGl.Popup({ const inspectPopup = new MapLibreGl.Popup({
closeOnClick: false closeOnClick: false
@@ -192,30 +182,28 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
showInspectButton: false, showInspectButton: false,
blockHoverPopupOnClick: true, blockHoverPopupOnClick: true,
assignLayerColor: (layerId: string, alpha: number) => { assignLayerColor: (layerId: string, alpha: number) => {
return Color(colors.brightColor(layerId, alpha)).desaturate(0.5).string() return Color(colors.brightColor(layerId, alpha)).desaturate(0.5).string();
}, },
buildInspectStyle: (originalMapStyle: StyleSpecification, coloredLayers: HighlightedLayer[]) => buildInspectStyle(originalMapStyle, coloredLayers, this.props.highlightedLayer), buildInspectStyle: (originalMapStyle: StyleSpecification, coloredLayers: HighlightedLayer[]) => buildInspectStyle(originalMapStyle, coloredLayers, this.props.highlightedLayer),
renderPopup: (features: InspectFeature[]) => { renderPopup: (features: InspectFeature[]) => {
if(this.props.inspectModeEnabled) { if(this.props.inspectModeEnabled) {
return renderPopup( inspectPopup.once("open", () => {
<MapMaplibreGlFeaturePropertyPopup features={features} />, root.render(<MapMaplibreGlFeaturePropertyPopup features={features} />);
tmpNode, });
inspectPopup return tmpNode;
);
} else { } else {
return renderPopup( inspectPopup.once("open", () => {
<MapMaplibreGlLayerPopup root.render(<MapMaplibreGlLayerPopup
features={features} features={features}
onLayerSelect={this.onLayerSelectById} onLayerSelect={this.onLayerSelectById}
zoom={this.state.zoom} zoom={this.state.zoom}
/>, />,);
tmpNode, });
inspectPopup return tmpNode;
);
} }
} }
}) });
map.addControl(inspect) map.addControl(inspect);
map.on("style.load", () => { map.on("style.load", () => {
this.setState({ this.setState({
@@ -225,18 +213,18 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
zoomControl, zoomControl,
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({
@@ -251,7 +239,7 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
onLayerSelectById = (id: string) => { onLayerSelectById = (id: string) => {
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);
} };
initGeocoder(map: Map) { initGeocoder(map: Map) {
const geocoderConfig = { const geocoderConfig = {
@@ -269,15 +257,15 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
(feature.bbox[3] - feature.bbox[1]) / 2 (feature.bbox[3] - feature.bbox[1]) / 2
]; ];
const point = { const point = {
type: 'Feature', type: "Feature",
geometry: { geometry: {
type: 'Point', type: "Point",
coordinates: center coordinates: center
}, },
place_name: feature.properties.display_name, place_name: feature.properties.display_name,
properties: feature.properties, properties: feature.properties,
text: feature.properties.display_name, text: feature.properties.display_name,
place_type: ['place'], place_type: ["place"],
center center
}; };
features.push(point); features.push(point);
@@ -294,7 +282,7 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
placeholder: this.props.t("Search"), placeholder: this.props.t("Search"),
maplibregl: MapLibreGl, maplibregl: MapLibreGl,
}); });
map.addControl(geocoder, 'top-left'); map.addControl(geocoder, "top-left");
return geocoder; return geocoder;
} }
@@ -306,9 +294,9 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
className="maputnik-map__map" className="maputnik-map__map"
role="region" role="region"
aria-label={t("Map view")} aria-label={t("Map view")}
ref={x => this.container = x} ref={x => {this.container = x;}}
data-wd-key="maplibre:map" data-wd-key="maplibre:map"
></div> ></div>;
} }
} }
@@ -1,17 +1,17 @@
import React from 'react' import React from "react";
import type { GeoJSONFeatureWithSourceLayer } from '@maplibre/maplibre-gl-inspect' import type { GeoJSONFeatureWithSourceLayer } from "@maplibre/maplibre-gl-inspect";
export type InspectFeature = GeoJSONFeatureWithSourceLayer & { export type InspectFeature = GeoJSONFeatureWithSourceLayer & {
inspectModeCounter?: number inspectModeCounter?: number
counter?: number counter?: number
} };
function displayValue(value: string | number | Date | object | undefined) { function displayValue(value: string | number | Date | object | undefined) {
if (typeof value === 'undefined' || value === null) return value; if (typeof value === "undefined" || value === null) return value;
if (value instanceof Date) return value.toLocaleString(); if (value instanceof Date) return value.toLocaleString();
if (typeof value === 'object' || if (typeof value === "object" ||
typeof value === 'number' || typeof value === "number" ||
typeof value === 'string') return value.toString(); typeof value === "string") return value.toString();
return value; return value;
} }
@@ -19,21 +19,21 @@ function renderKeyValueTableRow(key: string, value: string | undefined) {
return <tr key={key}> return <tr key={key}>
<td className="maputnik-popup-table-cell">{key}</td> <td className="maputnik-popup-table-cell">{key}</td>
<td className="maputnik-popup-table-cell">{value}</td> <td className="maputnik-popup-table-cell">{value}</td>
</tr> </tr>;
} }
function renderFeature(feature: InspectFeature, idx: number) { function renderFeature(feature: InspectFeature, idx: number) {
return <React.Fragment key={idx}> return <React.Fragment key={idx}>
<tr> <tr>
<td colSpan={2} className="maputnik-popup-layer-id">{feature.layer['source']}: {feature.layer['source-layer']}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</td> <td colSpan={2} className="maputnik-popup-layer-id">{feature.layer["source"]}: {feature.layer["source-layer"]}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</td>
</tr> </tr>
{renderKeyValueTableRow("$type", feature.geometry.type)} {renderKeyValueTableRow("$type", feature.geometry.type)}
{renderKeyValueTableRow("$id", displayValue(feature.id))} {renderKeyValueTableRow("$id", displayValue(feature.id))}
{Object.keys(feature.properties).map(propertyName => { {Object.keys(feature.properties).map(propertyName => {
const property = feature.properties[propertyName]; const property = feature.properties[propertyName];
return renderKeyValueTableRow(propertyName, displayValue(property)) return renderKeyValueTableRow(propertyName, displayValue(property));
})} })}
</React.Fragment> </React.Fragment>;
} }
function removeDuplicatedFeatures(features: InspectFeature[]) { function removeDuplicatedFeatures(features: InspectFeature[]) {
@@ -41,22 +41,22 @@ function removeDuplicatedFeatures(features: InspectFeature[]) {
features.forEach(feature => { features.forEach(feature => {
const featureIndex = uniqueFeatures.findIndex(feature2 => { const featureIndex = uniqueFeatures.findIndex(feature2 => {
return feature.layer['source-layer'] === feature2.layer['source-layer'] return feature.layer["source-layer"] === feature2.layer["source-layer"]
&& JSON.stringify(feature.properties) === JSON.stringify(feature2.properties) && JSON.stringify(feature.properties) === JSON.stringify(feature2.properties);
}) });
if(featureIndex === -1) { if(featureIndex === -1) {
uniqueFeatures.push(feature) uniqueFeatures.push(feature);
} else { } else {
if('inspectModeCounter' in uniqueFeatures[featureIndex]) { if("inspectModeCounter" in uniqueFeatures[featureIndex]) {
uniqueFeatures[featureIndex].inspectModeCounter!++ uniqueFeatures[featureIndex].inspectModeCounter!++;
} else { } else {
uniqueFeatures[featureIndex].inspectModeCounter = 2 uniqueFeatures[featureIndex].inspectModeCounter = 2;
} }
} }
}) });
return uniqueFeatures return uniqueFeatures;
} }
type FeaturePropertyPopupProps = { type FeaturePropertyPopupProps = {
@@ -65,16 +65,16 @@ type FeaturePropertyPopupProps = {
class FeaturePropertyPopup extends React.Component<FeaturePropertyPopupProps> { class FeaturePropertyPopup extends React.Component<FeaturePropertyPopupProps> {
render() { render() {
const features = removeDuplicatedFeatures(this.props.features) const features = removeDuplicatedFeatures(this.props.features);
return <div className="maputnik-feature-property-popup"> return <div className="maputnik-feature-property-popup" dir="ltr" data-wd-key="feature-property-popup">
<table className="maputnik-popup-table"> <table className="maputnik-popup-table">
<tbody> <tbody>
{features.map(renderFeature)} {features.map(renderFeature)}
</tbody> </tbody>
</table> </table>
</div> </div>;
} }
} }
export default FeaturePropertyPopup export default FeaturePropertyPopup;
+23 -23
View File
@@ -1,29 +1,29 @@
import React from 'react' import React from "react";
import IconLayer from './IconLayer' import IconLayer from "./IconLayer";
import type {InspectFeature} from './MapMaplibreGlFeaturePropertyPopup'; import type {InspectFeature} from "./MapMaplibreGlFeaturePropertyPopup";
function groupFeaturesBySourceLayer(features: InspectFeature[]) { function groupFeaturesBySourceLayer(features: InspectFeature[]) {
const sources: {[key: string]: InspectFeature[]} = {} const sources: {[key: string]: InspectFeature[]} = {};
const returnedFeatures: {[key: string]: number} = {} const returnedFeatures: {[key: string]: number} = {};
features.forEach(feature => { features.forEach(feature => {
const sourceKey = feature.layer['source-layer'] as string; const sourceKey = feature.layer["source-layer"] as string;
if(Object.prototype.hasOwnProperty.call(returnedFeatures, feature.layer.id)) { if(Object.prototype.hasOwnProperty.call(returnedFeatures, feature.layer.id)) {
returnedFeatures[feature.layer.id]++ returnedFeatures[feature.layer.id]++;
const featureObject = sources[sourceKey].find((f: InspectFeature) => f.layer.id === feature.layer.id) const featureObject = sources[sourceKey].find((f: InspectFeature) => f.layer.id === feature.layer.id);
featureObject!.counter = returnedFeatures[feature.layer.id] featureObject!.counter = returnedFeatures[feature.layer.id];
} else { } else {
sources[sourceKey] = sources[sourceKey] || [] sources[sourceKey] = sources[sourceKey] || [];
sources[sourceKey].push(feature) sources[sourceKey].push(feature);
returnedFeatures[feature.layer.id] = 1 returnedFeatures[feature.layer.id] = 1;
} }
}) });
return sources return sources;
} }
type FeatureLayerPopupProps = { type FeatureLayerPopupProps = {
@@ -66,7 +66,7 @@ class FeatureLayerPopup extends React.Component<FeatureLayerPopupProps> {
} }
render() { render() {
const sources = groupFeaturesBySourceLayer(this.props.features) const sources = groupFeaturesBySourceLayer(this.props.features);
const items = Object.keys(sources).map(vectorLayerId => { const items = Object.keys(sources).map(vectorLayerId => {
const layers = sources[vectorLayerId].map((feature: InspectFeature, idx: number) => { const layers = sources[vectorLayerId].map((feature: InspectFeature, idx: number) => {
@@ -83,7 +83,7 @@ class FeatureLayerPopup extends React.Component<FeatureLayerPopupProps> {
<label <label
className="maputnik-popup-layer__label" className="maputnik-popup-layer__label"
onClick={() => { onClick={() => {
this.props.onLayerSelect(feature.layer.id) this.props.onLayerSelect(feature.layer.id);
}} }}
> >
{feature.layer.type && {feature.layer.type &&
@@ -96,19 +96,19 @@ class FeatureLayerPopup extends React.Component<FeatureLayerPopupProps> {
{feature.layer.id} {feature.layer.id}
{feature.counter && <span> × {feature.counter}</span>} {feature.counter && <span> × {feature.counter}</span>}
</label> </label>
</div> </div>;
}) });
return <div key={vectorLayerId}> return <div key={vectorLayerId}>
<div className="maputnik-popup-layer-id">{vectorLayerId}</div> <div className="maputnik-popup-layer-id">{vectorLayerId}</div>
{layers} {layers}
</div> </div>;
}) });
return <div className="maputnik-feature-layer-popup"> return <div className="maputnik-feature-layer-popup" data-wd-key="feature-layer-popup" dir="ltr">
{items} {items}
</div> </div>;
} }
} }
export default FeatureLayerPopup export default FeatureLayerPopup;
+23 -23
View File
@@ -1,16 +1,16 @@
import React from 'react' import React from "react";
import {throttle} from 'lodash'; import {throttle} from "lodash";
import { WithTranslation, withTranslation } from 'react-i18next'; import { type WithTranslation, withTranslation } from "react-i18next";
import MapMaplibreGlLayerPopup from './MapMaplibreGlLayerPopup'; import MapMaplibreGlLayerPopup from "./MapMaplibreGlLayerPopup";
import 'ol/ol.css' import "ol/ol.css";
//@ts-ignore //@ts-ignore
import {apply} from 'ol-mapbox-style'; import {apply} from "ol-mapbox-style";
import {Map, View, Overlay} from 'ol'; import {Map, View, Overlay} from "ol";
import {toLonLat} from 'ol/proj'; import {toLonLat} from "ol/proj";
import type {StyleSpecification} from 'maplibre-gl'; import type {StyleSpecification} from "maplibre-gl";
function renderCoords (coords: string[]) { function renderCoords (coords: string[]) {
@@ -19,8 +19,8 @@ function renderCoords (coords: string[]) {
} }
else { else {
return <span className="maputnik-coords"> return <span className="maputnik-coords">
{coords.map((coord) => String(coord).padStart(7, "\u00A0")).join(', ')} {coords.map((coord) => String(coord).padStart(7, "\u00A0")).join(", ")}
</span> </span>;
} }
} }
@@ -29,7 +29,7 @@ type MapOpenLayersInternalProps = {
mapStyle: object mapStyle: object
accessToken?: string accessToken?: string
style?: object style?: object
onLayerSelect(...args: unknown[]): unknown onLayerSelect(layerId: string): void
debugToolbox: boolean debugToolbox: boolean
replaceAccessTokens(...args: unknown[]): unknown replaceAccessTokens(...args: unknown[]): unknown
onChange(...args: unknown[]): unknown onChange(...args: unknown[]): unknown
@@ -48,7 +48,7 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
onMapLoaded: () => {}, onMapLoaded: () => {},
onDataChange: () => {}, onDataChange: () => {},
onLayerSelect: () => {}, onLayerSelect: () => {},
} };
updateStyle: any; updateStyle: any;
map: any; map: any;
container: HTMLDivElement | null = null; container: HTMLDivElement | null = null;
@@ -101,15 +101,15 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
}) })
}); });
map.on('pointermove', (evt) => { map.on("pointermove", (evt) => {
const coords = toLonLat(evt.coordinate); const coords = toLonLat(evt.coordinate);
this.setState({ this.setState({
cursor: [ cursor: [
coords[0].toFixed(2), coords[0].toFixed(2),
coords[1].toFixed(2) coords[1].toFixed(2)
] ]
}) });
}) });
const onMoveEnd = () => { const onMoveEnd = () => {
const zoom = map.getView().getZoom(); const zoom = map.getView().getZoom();
@@ -122,12 +122,12 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
lat: center[1], lat: center[1],
}, },
}); });
} };
onMoveEnd(); onMoveEnd();
map.on('moveend', onMoveEnd); map.on("moveend", onMoveEnd);
map.on('postrender', (_e) => { map.on("postrender", (_e) => {
const center = toLonLat(map.getView().getCenter()!); const center = toLonLat(map.getView().getCenter()!);
this.setState({ this.setState({
center: [ center: [
@@ -150,13 +150,13 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
closeOverlay = (e: any) => { closeOverlay = (e: any) => {
e.target.blur(); e.target.blur();
this.overlay!.setPosition(undefined); this.overlay!.setPosition(undefined);
} };
render() { render() {
const t = this.props.t; const t = this.props.t;
return <div className="maputnik-ol-container"> return <div className="maputnik-ol-container">
<div <div
ref={x => this.popupContainer = x} ref={x => {this.popupContainer = x;}}
style={{background: "black"}} style={{background: "black"}}
className="maputnik-popup" className="maputnik-popup"
> >
@@ -193,14 +193,14 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
} }
<div <div
className="maputnik-ol" className="maputnik-ol"
ref={x => this.container = x} ref={x => {this.container = x;}}
role="region" role="region"
aria-label={t("Map view")} aria-label={t("Map view")}
style={{ style={{
...this.props.style, ...this.props.style,
}}> }}>
</div> </div>
</div> </div>;
} }
} }
+25 -29
View File
@@ -1,38 +1,34 @@
import React from 'react' import React from "react";
import FieldFunction from './FieldFunction' import FieldFunction from "./FieldFunction";
import type {LayerSpecification} from 'maplibre-gl' import type {LayerSpecification} from "maplibre-gl";
const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image'] const iconProperties = ["background-pattern", "fill-pattern", "line-pattern", "fill-extrusion-pattern", "icon-image"];
/** Extract field spec by {@fieldName} from the {@layerType} in the /** Extract field spec by {@fieldName} from the {@layerType} in the
* style specification from either the paint or layout group */ * style specification from either the paint or layout group */
function getFieldSpec(spec: any, layerType: LayerSpecification["type"], fieldName: string) { function getFieldSpec(spec: any, layerType: LayerSpecification["type"], fieldName: string) {
const groupName = getGroupName(spec, layerType, fieldName) const groupName = getGroupName(spec, layerType, fieldName);
const group = spec[groupName + '_' + layerType] const group = spec[groupName + "_" + layerType];
const fieldSpec = group[fieldName] const fieldSpec = group[fieldName];
if(iconProperties.indexOf(fieldName) >= 0) { if(iconProperties.indexOf(fieldName) >= 0) {
return { return {
...fieldSpec, ...fieldSpec,
values: spec.$root.sprite.values values: spec.$root.sprite.values
} };
} }
if(fieldName === 'text-font') { if(fieldName === "text-font") {
return { return {
...fieldSpec, ...fieldSpec,
values: spec.$root.glyphs.values values: spec.$root.glyphs.values
} };
} }
return fieldSpec return fieldSpec;
} }
function getGroupName(spec: any, layerType: LayerSpecification["type"], fieldName: string) { function getGroupName(spec: any, layerType: LayerSpecification["type"], fieldName: string): "paint" | "layout" {
const paint = spec['paint_' + layerType] || {} const paint = spec["paint_" + layerType] || {};
if (fieldName in paint) { return (fieldName in paint) ? "paint" : "layout";
return 'paint'
} else {
return 'layout'
}
} }
type PropertyGroupProps = { type PropertyGroupProps = {
@@ -45,21 +41,21 @@ type PropertyGroupProps = {
export default class PropertyGroup extends React.Component<PropertyGroupProps> { export default class PropertyGroup extends React.Component<PropertyGroupProps> {
onPropertyChange = (property: string, newValue: any) => { onPropertyChange = (property: string, newValue: any) => {
const group = getGroupName(this.props.spec, this.props.layer.type, property) const group = getGroupName(this.props.spec, this.props.layer.type, property);
this.props.onChange(group ,property, newValue) this.props.onChange(group ,property, newValue);
} };
render() { render() {
const {errors} = this.props; const {errors} = this.props;
const fields = this.props.groupFields.map(fieldName => { const fields = this.props.groupFields.map(fieldName => {
const fieldSpec = getFieldSpec(this.props.spec, this.props.layer.type, fieldName) const fieldSpec = getFieldSpec(this.props.spec, this.props.layer.type, fieldName);
const paint = this.props.layer.paint || {} const paint = this.props.layer.paint || {};
const layout = this.props.layer.layout || {} const layout = this.props.layer.layout || {};
const fieldValue = fieldName in paint const fieldValue = fieldName in paint
? paint[fieldName as keyof typeof paint] ? paint[fieldName as keyof typeof paint]
: layout[fieldName as keyof typeof layout] : layout[fieldName as keyof typeof layout];
const fieldType = fieldName in paint ? 'paint' : 'layout'; const fieldType = fieldName in paint ? "paint" : "layout";
return <FieldFunction return <FieldFunction
errors={errors} errors={errors}
@@ -69,11 +65,11 @@ export default class PropertyGroup extends React.Component<PropertyGroupProps> {
value={fieldValue} value={fieldValue}
fieldType={fieldType} fieldType={fieldType}
fieldSpec={fieldSpec} fieldSpec={fieldSpec}
/> />;
}) });
return <div className="maputnik-property-group"> return <div className="maputnik-property-group">
{fields} {fields}
</div> </div>;
} }
} }
+2 -2
View File
@@ -1,4 +1,4 @@
import React from 'react' import React from "react";
type ScrollContainerProps = { type ScrollContainerProps = {
children?: React.ReactNode children?: React.ReactNode
@@ -8,6 +8,6 @@ export default class ScrollContainer extends React.Component<ScrollContainerProp
render() { render() {
return <div className="maputnik-scroll-container"> return <div className="maputnik-scroll-container">
{this.props.children} {this.props.children}
</div> </div>;
} }
} }
+21 -21
View File
@@ -1,14 +1,14 @@
import React from 'react' import React from "react";
import {otherFilterOps} from '../libs/filterops' import {otherFilterOps} from "../libs/filterops";
import InputString from './InputString' import InputString from "./InputString";
import InputAutocomplete from './InputAutocomplete' import InputAutocomplete from "./InputAutocomplete";
import InputSelect from './InputSelect' import InputSelect from "./InputSelect";
function tryParseInt(v: string | number) { function tryParseInt(v: string | number) {
if (v === '') return v if (v === "") return v;
if (isNaN(v as number)) return v if (isNaN(v as number)) return v;
return parseFloat(v as string) return parseFloat(v as string);
} }
function tryParseBool(v: string | boolean) { function tryParseBool(v: string | boolean) {
@@ -43,23 +43,23 @@ type SingleFilterEditorProps = {
export default class SingleFilterEditor extends React.Component<SingleFilterEditorProps> { export default class SingleFilterEditor extends React.Component<SingleFilterEditorProps> {
static defaultProps = { static defaultProps = {
properties: {}, properties: {},
} };
onFilterPartChanged(filterOp: string, propertyName: string, filterArgs: string[]) { onFilterPartChanged(filterOp: string, propertyName: string, filterArgs: string[]) {
let newFilter = [filterOp, propertyName, ...filterArgs.map(parseFilter)] let newFilter = [filterOp, propertyName, ...filterArgs.map(parseFilter)];
if(filterOp === 'has' || filterOp === '!has') { if(filterOp === "has" || filterOp === "!has") {
newFilter = [filterOp, propertyName] newFilter = [filterOp, propertyName];
} else if(filterArgs.length === 0) { } else if(filterArgs.length === 0) {
newFilter = [filterOp, propertyName, ''] newFilter = [filterOp, propertyName, ""];
} }
this.props.onChange(newFilter) this.props.onChange(newFilter);
} }
render() { render() {
const f = this.props.filter const f = this.props.filter;
const filterOp = f[0] const filterOp = f[0];
const propertyName = f[1] const propertyName = f[1];
const filterArgs = f.slice(2) const filterArgs = f.slice(2);
return <div className="maputnik-filter-editor-single"> return <div className="maputnik-filter-editor-single">
<div className="maputnik-filter-editor-property"> <div className="maputnik-filter-editor-property">
@@ -82,11 +82,11 @@ export default class SingleFilterEditor extends React.Component<SingleFilterEdit
<div className="maputnik-filter-editor-args"> <div className="maputnik-filter-editor-args">
<InputString <InputString
aria-label="value" aria-label="value"
value={filterArgs.join(',')} value={filterArgs.join(",")}
onChange={(v: string) => this.onFilterPartChanged(filterOp, propertyName, v.split(','))} onChange={(v: string) => this.onFilterPartChanged(filterOp, propertyName, v.split(","))}
/> />
</div> </div>
} }
</div> </div>;
} }
} }
+3 -3
View File
@@ -1,6 +1,6 @@
import React from 'react' import React from "react";
import { WithTranslation, withTranslation } from 'react-i18next'; import { type WithTranslation, withTranslation } from "react-i18next";
import './SmallError.scss'; import "./SmallError.scss";
type SmallErrorInternalProps = { type SmallErrorInternalProps = {
-43
View File
@@ -1,43 +0,0 @@
import Block from './Block'
import InputSpec, { SpecFieldProps as InputFieldSpecProps } from './InputSpec'
import Fieldset from './Fieldset'
const typeMap = {
color: () => Block,
enum: ({fieldSpec}: any) => (Object.keys(fieldSpec.values).length <= 3 ? Fieldset : Block),
boolean: () => Block,
array: () => Fieldset,
resolvedImage: () => Block,
number: () => Block,
string: () => Block,
formatted: () => Block,
padding: () => Block,
};
export type SpecFieldProps = InputFieldSpecProps & {
name?: string
};
const SpecField: React.FC<SpecFieldProps> = (props) => {
const fieldType = props.fieldSpec?.type;
const typeBlockFn = typeMap[fieldType!];
let TypeBlock;
if (typeBlockFn) {
TypeBlock = typeBlockFn(props);
}
else {
console.warn("No such type for '%s'", fieldType);
TypeBlock = Block;
}
return (
<TypeBlock label={props.label} action={props.action} fieldSpec={props.fieldSpec}>
<InputSpec {...props} />
</TypeBlock>
);
};
export default SpecField;
+56 -57
View File
@@ -1,20 +1,21 @@
import React from 'react' import React from "react";
import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js'; import {PiListPlusBold} from "react-icons/pi";
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json' import {TbMathFunction} from "react-icons/tb";
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import InputButton from './InputButton' import InputButton from "./InputButton";
import InputSpec from './InputSpec' import InputSpec from "./InputSpec";
import InputNumber from './InputNumber' import InputNumber from "./InputNumber";
import InputString from './InputString' import InputString from "./InputString";
import InputSelect from './InputSelect' import InputSelect from "./InputSelect";
import Block from './Block' import Block from "./Block";
import docUid from '../libs/document-uid' import docUid from "../libs/document-uid";
import sortNumerically from '../libs/sort-numerically' import sortNumerically from "../libs/sort-numerically";
import {findDefaultFromSpec} from '../libs/spec-helper'; import {findDefaultFromSpec} from "../libs/spec-helper";
import { WithTranslation, withTranslation } from 'react-i18next'; import { type WithTranslation, withTranslation } from "react-i18next";
import labelFromFieldName from '../libs/label-from-field-name' import labelFromFieldName from "../libs/label-from-field-name";
import DeleteStopButton from './_DeleteStopButton' import DeleteStopButton from "./_DeleteStopButton";
@@ -30,7 +31,7 @@ function setStopRefs(props: DataPropertyInternalProps, state: DataPropertyState)
} }
newRefs[idx] = docUid("stop-"); newRefs[idx] = docUid("stop-");
} }
}) });
} }
return newRefs; return newRefs;
@@ -59,17 +60,17 @@ type DataPropertyValue = {
base?: number base?: number
type?: string type?: string
stops: Stop[] stops: Stop[]
} };
export type Stop = [{ export type Stop = [{
zoom: number zoom: number
value: number value: number
}, number] }, number];
class DataPropertyInternal extends React.Component<DataPropertyInternalProps, DataPropertyState> { class DataPropertyInternal extends React.Component<DataPropertyInternalProps, DataPropertyState> {
state = { state = {
refs: {} as {[key: number]: string} refs: {} as {[key: number]: string}
} };
componentDidMount() { componentDidMount() {
const newRefs = setStopRefs(this.props, this.state); const newRefs = setStopRefs(this.props, this.state);
@@ -77,7 +78,7 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
if(newRefs) { if(newRefs) {
this.setState({ this.setState({
refs: newRefs refs: newRefs
}) });
} }
} }
@@ -93,20 +94,20 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
getFieldFunctionType(fieldSpec: any) { getFieldFunctionType(fieldSpec: any) {
if (fieldSpec.expression.interpolated) { if (fieldSpec.expression.interpolated) {
return "exponential" return "exponential";
} }
if (fieldSpec.type === "number") { if (fieldSpec.type === "number") {
return "interval" return "interval";
} }
return "categorical" return "categorical";
} }
getDataFunctionTypes(fieldSpec: any) { getDataFunctionTypes(fieldSpec: any) {
if (fieldSpec.expression.interpolated) { if (fieldSpec.expression.interpolated) {
return ["interpolate", "categorical", "interval", "exponential", "identity"] return ["interpolate", "categorical", "interval", "exponential", "identity"];
} }
else { else {
return ["categorical", "interval", "identity"] return ["categorical", "interval", "identity"];
} }
} }
@@ -117,7 +118,7 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
return { return {
ref: this.state.refs[idx], ref: this.state.refs[idx],
data: stop data: stop
} };
}) })
// Sort by zoom // Sort by zoom
.sort((a, b) => sortNumerically(a.data[0].zoom, b.data[0].zoom)); .sort((a, b) => sortNumerically(a.data[0].zoom, b.data[0].zoom));
@@ -127,7 +128,7 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
mappedWithRef mappedWithRef
.forEach((stop, idx) =>{ .forEach((stop, idx) =>{
newRefs[idx] = stop.ref; newRefs[idx] = stop.ref;
}) });
this.setState({ this.setState({
refs: newRefs refs: newRefs
@@ -144,7 +145,7 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
}; };
} }
else { else {
const stopValue = value.type === 'categorical' ? '' : 0; const stopValue = value.type === "categorical" ? "" : 0;
value = { value = {
property: "", property: "",
type: value.type, type: value.type,
@@ -154,13 +155,13 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
[{zoom: 10, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec as any)] [{zoom: 10, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec as any)]
], ],
...value, ...value,
} };
} }
this.props.onChange!(fieldName, value); this.props.onChange!(fieldName, value);
} };
changeStop(changeIdx: number, stopData: { zoom: number | undefined, value: number }, value: number) { changeStop(changeIdx: number, stopData: { zoom: number | undefined, value: number }, value: number) {
const stops = this.props.value?.stops.slice(0) || [] const stops = this.props.value?.stops.slice(0) || [];
// const changedStop = stopData.zoom === undefined ? stopData.value : stopData // const changedStop = stopData.zoom === undefined ? stopData.value : stopData
stops[changeIdx] = [ stops[changeIdx] = [
{ {
@@ -175,20 +176,20 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
const changedValue = { const changedValue = {
...this.props.value, ...this.props.value,
stops: orderedStops, stops: orderedStops,
} };
this.onChange(this.props.fieldName, changedValue) this.onChange(this.props.fieldName, changedValue);
} }
changeBase(newValue: number | undefined) { changeBase(newValue: number | undefined) {
const changedValue = { const changedValue = {
...this.props.value, ...this.props.value,
base: newValue base: newValue
} };
if (changedValue.base === undefined) { if (changedValue.base === undefined) {
delete changedValue["base"]; delete changedValue["base"];
} }
this.props.onChange!(this.props.fieldName, changedValue) this.props.onChange!(this.props.fieldName, changedValue);
} }
changeDataType(propVal: string) { changeDataType(propVal: string) {
@@ -205,43 +206,43 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
changeDataProperty(propName: "property" | "default", propVal: any) { changeDataProperty(propName: "property" | "default", propVal: any) {
if (propVal) { if (propVal) {
this.props.value![propName] = propVal this.props.value![propName] = propVal;
} }
else { else {
delete this.props.value![propName] delete this.props.value![propName];
} }
this.onChange(this.props.fieldName, this.props.value) this.onChange(this.props.fieldName, this.props.value);
} }
render() { render() {
const t = this.props.t; const t = this.props.t;
if (typeof this.props.value?.type === "undefined") { if (typeof this.props.value?.type === "undefined") {
this.props.value!.type = this.getFieldFunctionType(this.props.fieldSpec) this.props.value!.type = this.getFieldFunctionType(this.props.fieldSpec);
} }
let dataFields; let dataFields;
if (this.props.value?.stops) { if (this.props.value?.stops) {
dataFields = this.props.value.stops.map((stop, idx) => { dataFields = this.props.value.stops.map((stop, idx) => {
const zoomLevel = typeof stop[0] === 'object' ? stop[0].zoom : undefined; const zoomLevel = typeof stop[0] === "object" ? stop[0].zoom : undefined;
const key = this.state.refs[idx]; const key = this.state.refs[idx];
const dataLevel = typeof stop[0] === 'object' ? stop[0].value : stop[0]; const dataLevel = typeof stop[0] === "object" ? stop[0].value : stop[0];
const value = stop[1] const value = stop[1];
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop?.bind(this, idx)} /> const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop?.bind(this, idx)} />;
const dataProps = { const dataProps = {
'aria-label': t("Input value"), "aria-label": t("Input value"),
label: t("Data value"), label: t("Data value"),
value: dataLevel as any, value: dataLevel as any,
onChange: (newData: string | number | undefined) => this.changeStop(idx, { zoom: zoomLevel, value: newData as number }, value) onChange: (newData: string | number | undefined) => this.changeStop(idx, { zoom: zoomLevel, value: newData as number }, value)
} };
let dataInput; let dataInput;
if(this.props.value?.type === "categorical") { if(this.props.value?.type === "categorical") {
dataInput = <InputString {...dataProps} /> dataInput = <InputString {...dataProps} />;
} }
else { else {
dataInput = <InputNumber {...dataProps} /> dataInput = <InputNumber {...dataProps} />;
} }
let zoomInput = null; let zoomInput = null;
@@ -254,7 +255,7 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
min={0} min={0}
max={22} max={22}
/> />
</div> </div>;
} }
return <tr key={key}> return <tr key={key}>
@@ -276,8 +277,8 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
<td> <td>
{deleteStopBtn} {deleteStopBtn}
</td> </td>
</tr> </tr>;
}) });
} }
return <div className="maputnik-data-spec-block"> return <div className="maputnik-data-spec-block">
@@ -360,23 +361,21 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
className="maputnik-add-stop" className="maputnik-add-stop"
onClick={this.props.onAddStop?.bind(this)} onClick={this.props.onAddStop?.bind(this)}
> >
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24"> <PiListPlusBold style={{ verticalAlign: "text-bottom" }} />
<path fill="currentColor" d={mdiTableRowPlusAfter} /> {t("Add stop")}
</svg> {t("Add stop")}
</InputButton> </InputButton>
} }
<InputButton <InputButton
className="maputnik-add-stop" className="maputnik-add-stop"
onClick={this.props.onExpressionClick?.bind(this)} onClick={this.props.onExpressionClick?.bind(this)}
> >
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24"> <TbMathFunction style={{ verticalAlign: "text-bottom" }} />
<path fill="currentColor" d={mdiFunctionVariant} /> {t("Convert to expression")}
</svg> {t("Convert to expression")}
</InputButton> </InputButton>
</div> </div>
</div> </div>
</fieldset> </fieldset>
</div> </div>;
} }
} }
+5 -5
View File
@@ -1,8 +1,8 @@
import React from 'react' import React from "react";
import InputButton from './InputButton' import InputButton from "./InputButton";
import {MdDelete} from 'react-icons/md' import {MdDelete} from "react-icons/md";
import { WithTranslation, withTranslation } from 'react-i18next'; import { type WithTranslation, withTranslation } from "react-i18next";
type DeleteStopButtonInternalProps = { type DeleteStopButtonInternalProps = {
@@ -19,7 +19,7 @@ class DeleteStopButtonInternal extends React.Component<DeleteStopButtonInternalP
title={t("Remove zoom level from stop")} title={t("Remove zoom level from stop")}
> >
<MdDelete /> <MdDelete />
</InputButton> </InputButton>;
} }
} }
+16 -16
View File
@@ -1,12 +1,12 @@
import React from 'react' import React from "react";
import {MdDelete, MdUndo} from 'react-icons/md' import {MdDelete, MdUndo} from "react-icons/md";
import stringifyPretty from 'json-stringify-pretty-compact' import stringifyPretty from "json-stringify-pretty-compact";
import { WithTranslation, withTranslation } from 'react-i18next'; import { type WithTranslation, withTranslation } from "react-i18next";
import Block from './Block' import Block from "./Block";
import InputButton from './InputButton' import InputButton from "./InputButton";
import labelFromFieldName from '../libs/label-from-field-name' import labelFromFieldName from "../libs/label-from-field-name";
import FieldJson from './FieldJson' import FieldJson from "./FieldJson";
type ExpressionPropertyInternalProps = { type ExpressionPropertyInternalProps = {
@@ -32,7 +32,7 @@ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInter
errors: {}, errors: {},
onFocus: () => {}, onFocus: () => {},
onBlur: () => {}, onBlur: () => {},
} };
constructor(props: ExpressionPropertyInternalProps) { constructor(props: ExpressionPropertyInternalProps) {
super(props); super(props);
@@ -44,14 +44,14 @@ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInter
onJSONInvalid = (_err: Error) => { onJSONInvalid = (_err: Error) => {
this.setState({ this.setState({
jsonError: true, jsonError: true,
}) });
} };
onJSONValid = () => { onJSONValid = () => {
this.setState({ this.setState({
jsonError: false, jsonError: false,
}) });
} };
render() { render() {
const {t, errors, fieldName, fieldType, value, canUndo} = this.props; const {t, errors, fieldName, fieldType, value, canUndo} = this.props;
@@ -89,7 +89,7 @@ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInter
const foundErrors = []; const foundErrors = [];
function getValue(data: any) { function getValue(data: any) {
return stringifyPretty(data, {indent: 2, maxLength: 38}) return stringifyPretty(data, {indent: 2, maxLength: 38});
} }
if (jsonError) { if (jsonError) {
@@ -102,7 +102,7 @@ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInter
}) })
.forEach(([_key, error]) => { .forEach(([_key, error]) => {
return foundErrors.push(error); return foundErrors.push(error);
}) });
if (fieldError) { if (fieldError) {
foundErrors.push(fieldError); foundErrors.push(fieldError);
@@ -135,7 +135,7 @@ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInter
getValue={getValue} getValue={getValue}
onChange={this.props.onChange} onChange={this.props.onChange}
/> />
</Block> </Block>;
} }
} }
+30 -22
View File
@@ -1,60 +1,68 @@
import React from 'react' import React from "react";
import InputButton from './InputButton' import InputButton from "./InputButton";
import {MdFunctions, MdInsertChart} from 'react-icons/md' import {MdFunctions, MdInsertChart} from "react-icons/md";
import {mdiFunctionVariant} from '@mdi/js'; import { TbMathFunction } from "react-icons/tb";
import { WithTranslation, withTranslation } from 'react-i18next'; import { type WithTranslation, withTranslation } from "react-i18next";
type FunctionInputButtonsInternalProps = { type FunctionInputButtonsInternalProps = {
fieldSpec?: any fieldSpec?: any
onZoomClick?(...args: unknown[]): unknown onZoomClick?(): void
onDataClick?(...args: unknown[]): unknown onDataClick?(): void
onExpressionClick?(...args: unknown[]): unknown onExpressionClick?(): void
onElevationClick?(): void
} & WithTranslation; } & WithTranslation;
class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsInternalProps> { class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsInternalProps> {
render() { render() {
const t = this.props.t; const t = this.props.t;
let makeZoomInputButton, makeDataInputButton, expressionInputButton;
if (this.props.fieldSpec.expression?.parameters.includes('zoom')) { if (this.props.fieldSpec.expression?.parameters.includes("zoom")) {
expressionInputButton = ( const expressionInputButton = (
<InputButton <InputButton
className="maputnik-make-zoom-function" className="maputnik-make-zoom-function"
onClick={this.props.onExpressionClick} onClick={this.props.onExpressionClick}
title={t("Convert to expression")} title={t("Convert to expression")}
> >
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24"> <TbMathFunction />
<path fill="currentColor" d={mdiFunctionVariant} />
</svg>
</InputButton> </InputButton>
); );
makeZoomInputButton = <InputButton const makeZoomInputButton = <InputButton
className="maputnik-make-zoom-function" className="maputnik-make-zoom-function"
onClick={this.props.onZoomClick} onClick={this.props.onZoomClick}
title={t("Convert property into a zoom function")} title={t("Convert property into a zoom function")}
> >
<MdFunctions /> <MdFunctions />
</InputButton> </InputButton>;
if (this.props.fieldSpec['property-type'] === 'data-driven') { let makeDataInputButton;
if (this.props.fieldSpec["property-type"] === "data-driven") {
makeDataInputButton = <InputButton makeDataInputButton = <InputButton
className="maputnik-make-data-function" className="maputnik-make-data-function"
onClick={this.props.onDataClick} onClick={this.props.onDataClick}
title={t("Convert property to data function")} title={t("Convert property to data function")}
> >
<MdInsertChart /> <MdInsertChart />
</InputButton> </InputButton>;
} }
return <div> return <div>
{expressionInputButton} {expressionInputButton}
{makeDataInputButton} {makeDataInputButton}
{makeZoomInputButton} {makeZoomInputButton}
</div> </div>;
} } else if (this.props.fieldSpec.expression?.parameters.includes("elevation")) {
else { const inputElevationButton = <InputButton
return <div>{expressionInputButton}</div> className="maputnik-make-elevation-function"
onClick={this.props.onElevationClick}
title={t("Convert property into a elevation function")}
data-wd-key='make-elevation-function'
>
<MdFunctions />
</InputButton>;
return <div>{inputElevationButton}</div>;
} else {
return <div></div>;
} }
} }
} }
+15 -13
View File
@@ -1,27 +1,28 @@
import React from 'react' import React from "react";
import SpecField, {SpecFieldProps} from './SpecField' import FieldSpec, {type FieldSpecProps} from "./FieldSpec";
import FunctionButtons from './_FunctionButtons' import FunctionButtons from "./_FunctionButtons";
import labelFromFieldName from '../libs/label-from-field-name' import labelFromFieldName from "../libs/label-from-field-name";
type SpecPropertyProps = SpecFieldProps & { type SpecPropertyProps = FieldSpecProps & {
onZoomClick(...args: unknown[]): unknown
onDataClick(...args: unknown[]): unknown
fieldName?: string fieldName?: string
fieldType?: string fieldType?: string
fieldSpec?: any fieldSpec?: any
value?: any value?: any
errors?: {[key: string]: {message: string}} errors?: {[key: string]: {message: string}}
onExpressionClick?(...args: unknown[]): unknown onZoomClick(): void
onDataClick(): void
onExpressionClick?(): void
onElevationClick?(): void
}; };
export default class SpecProperty extends React.Component<SpecPropertyProps> { export default class SpecProperty extends React.Component<SpecPropertyProps> {
static defaultProps = { static defaultProps = {
errors: {}, errors: {},
} };
render() { render() {
const {errors, fieldName, fieldType} = this.props; const {errors, fieldName, fieldType} = this.props;
@@ -31,16 +32,17 @@ export default class SpecProperty extends React.Component<SpecPropertyProps> {
onZoomClick={this.props.onZoomClick} onZoomClick={this.props.onZoomClick}
onDataClick={this.props.onDataClick} onDataClick={this.props.onDataClick}
onExpressionClick={this.props.onExpressionClick} onExpressionClick={this.props.onExpressionClick}
/> onElevationClick={this.props.onElevationClick}
/>;
const error = errors![fieldType+"."+fieldName as any] as any; const error = errors![fieldType+"."+fieldName as any] as any;
return <SpecField return <FieldSpec
{...this.props} {...this.props}
error={error} error={error}
fieldSpec={this.props.fieldSpec} fieldSpec={this.props.fieldSpec}
label={labelFromFieldName(this.props.fieldName || '')} label={labelFromFieldName(this.props.fieldName || "")}
action={functionBtn} action={functionBtn}
/> />;
} }
} }
+37 -38
View File
@@ -1,19 +1,20 @@
import React from 'react' import React from "react";
import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js'; import { PiListPlusBold } from "react-icons/pi";
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json' import { TbMathFunction } from "react-icons/tb";
import { WithTranslation, withTranslation } from 'react-i18next'; import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import { type WithTranslation, withTranslation } from "react-i18next";
import InputButton from './InputButton' import InputButton from "./InputButton";
import InputSpec from './InputSpec' import InputSpec from "./InputSpec";
import InputNumber from './InputNumber' import InputNumber from "./InputNumber";
import InputSelect from './InputSelect' import InputSelect from "./InputSelect";
import Block from './Block' import Block from "./Block";
import DeleteStopButton from './_DeleteStopButton' import DeleteStopButton from "./_DeleteStopButton";
import labelFromFieldName from '../libs/label-from-field-name' import labelFromFieldName from "../libs/label-from-field-name";
import docUid from '../libs/document-uid' import docUid from "../libs/document-uid";
import sortNumerically from '../libs/sort-numerically' import sortNumerically from "../libs/sort-numerically";
/** /**
@@ -35,7 +36,7 @@ function setStopRefs(props: ZoomPropertyInternalProps, state: ZoomPropertyState)
} else { } else {
newRefs[idx] = state.refs[idx]; newRefs[idx] = state.refs[idx];
} }
}) });
} }
return newRefs; return newRefs;
} }
@@ -43,7 +44,7 @@ function setStopRefs(props: ZoomPropertyInternalProps, state: ZoomPropertyState)
type ZoomWithStops = { type ZoomWithStops = {
stops: [number | undefined, number][] stops: [number | undefined, number][]
base?: number base?: number
} };
type ZoomPropertyInternalProps = { type ZoomPropertyInternalProps = {
@@ -64,16 +65,16 @@ type ZoomPropertyInternalProps = {
type ZoomPropertyState = { type ZoomPropertyState = {
refs: {[key: number]: string} refs: {[key: number]: string}
} };
class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, ZoomPropertyState> { class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, ZoomPropertyState> {
static defaultProps = { static defaultProps = {
errors: {}, errors: {},
} };
state = { state = {
refs: {} as {[key: number]: string} refs: {} as {[key: number]: string}
} };
componentDidMount() { componentDidMount() {
const newRefs = setStopRefs(this.props, this.state); const newRefs = setStopRefs(this.props, this.state);
@@ -81,7 +82,7 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
if(newRefs) { if(newRefs) {
this.setState({ this.setState({
refs: newRefs refs: newRefs
}) });
} }
} }
@@ -102,7 +103,7 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
return { return {
ref: this.state.refs[idx], ref: this.state.refs[idx],
data: stop data: stop
} };
}) })
// Sort by zoom // Sort by zoom
.sort((a, b) => sortNumerically(a.data[0]!, b.data[0]!)); .sort((a, b) => sortNumerically(a.data[0]!, b.data[0]!));
@@ -112,7 +113,7 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
mappedWithRef mappedWithRef
.forEach((stop, idx) =>{ .forEach((stop, idx) =>{
newRefs[idx] = stop.ref; newRefs[idx] = stop.ref;
}) });
this.setState({ this.setState({
refs: newRefs refs: newRefs
@@ -130,34 +131,34 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
const changedValue = { const changedValue = {
...this.props.value as ZoomWithStops, ...this.props.value as ZoomWithStops,
stops: orderedStops stops: orderedStops
} };
this.props.onChange!(this.props.fieldName, changedValue) this.props.onChange!(this.props.fieldName, changedValue);
} }
changeBase(newValue: number | undefined) { changeBase(newValue: number | undefined) {
const changedValue = { const changedValue = {
...this.props.value, ...this.props.value,
base: newValue base: newValue
} };
if (changedValue.base === undefined) { if (changedValue.base === undefined) {
delete changedValue["base"]; delete changedValue["base"];
} }
this.props.onChange!(this.props.fieldName, changedValue) this.props.onChange!(this.props.fieldName, changedValue);
} }
changeDataType = (type: string) => { changeDataType = (type: string) => {
if (type !== "interpolate" && this.props.onChangeToDataFunction) { if (type !== "interpolate" && this.props.onChangeToDataFunction) {
this.props.onChangeToDataFunction(type); this.props.onChangeToDataFunction(type);
} }
} };
render() { render() {
const t = this.props.t; const t = this.props.t;
const zoomFields = this.props.value?.stops.map((stop, idx) => { const zoomFields = this.props.value?.stops.map((stop, idx) => {
const zoomLevel = stop[0] const zoomLevel = stop[0];
const value = stop[1] const value = stop[1];
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop?.bind(this, idx)} /> const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop?.bind(this, idx)} />;
return <tr return <tr
key={`${stop[0]}-${stop[1]}`} key={`${stop[0]}-${stop[1]}`}
> >
@@ -182,7 +183,7 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
<td> <td>
{deleteStopBtn} {deleteStopBtn}
</td> </td>
</tr> </tr>;
}); });
// return <div className="maputnik-zoom-spec-property"> // return <div className="maputnik-zoom-spec-property">
@@ -233,29 +234,27 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
className="maputnik-add-stop" className="maputnik-add-stop"
onClick={this.props.onAddStop?.bind(this)} onClick={this.props.onAddStop?.bind(this)}
> >
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24"> <PiListPlusBold style={{ verticalAlign: "text-bottom" }} />
<path fill="currentColor" d={mdiTableRowPlusAfter} /> {t("Add stop")}
</svg> {t("Add stop")}
</InputButton> </InputButton>
<InputButton <InputButton
className="maputnik-add-stop" className="maputnik-add-stop"
onClick={this.props.onExpressionClick?.bind(this)} onClick={this.props.onExpressionClick?.bind(this)}
> >
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24"> <TbMathFunction style={{ verticalAlign: "text-bottom" }} />
<path fill="currentColor" d={mdiFunctionVariant} /> {t("Convert to expression")}
</svg> {t("Convert to expression")}
</InputButton> </InputButton>
</div> </div>
</div> </div>
</fieldset> </fieldset>
</div> </div>;
} }
getDataFunctionTypes(fieldSpec: { getDataFunctionTypes(fieldSpec: {
"property-type"?: string "property-type"?: string
"function-type"?: string "function-type"?: string
}) { }) {
if (fieldSpec['property-type'] === 'data-driven') { if (fieldSpec["property-type"] === "data-driven") {
return ["interpolate", "categorical", "interval", "exponential", "identity"]; return ["interpolate", "categorical", "interval", "exponential", "identity"];
} }
else { else {
@@ -1,16 +1,15 @@
import React, { PropsWithChildren } from 'react' import React, { type PropsWithChildren } from "react";
import {MdClose} from 'react-icons/md' import {MdClose} from "react-icons/md";
import AriaModal from 'react-aria-modal' import AriaModal from "react-aria-modal";
import classnames from 'classnames'; import classnames from "classnames";
import { WithTranslation, withTranslation } from 'react-i18next'; import { type WithTranslation, withTranslation } from "react-i18next";
type ModalInternalProps = PropsWithChildren & { type ModalInternalProps = PropsWithChildren & {
"data-wd-key"?: string "data-wd-key"?: string
isOpen: boolean isOpen: boolean
title: string title: string
onOpenToggle(value: boolean): unknown onOpenToggle(): void
underlayClickExits?: boolean underlayClickExits?: boolean
underlayProps?: any
className?: string className?: string
} & WithTranslation; } & WithTranslation;
@@ -18,7 +17,7 @@ type ModalInternalProps = PropsWithChildren & {
class ModalInternal extends React.Component<ModalInternalProps> { class ModalInternal extends React.Component<ModalInternalProps> {
static defaultProps = { static defaultProps = {
underlayClickExits: true underlayClickExits: true
} };
// See <https://github.com/maplibre/maputnik/issues/416> // See <https://github.com/maplibre/maputnik/issues/416>
onClose = () => { onClose = () => {
@@ -27,9 +26,9 @@ class ModalInternal extends React.Component<ModalInternalProps> {
} }
setTimeout(() => { setTimeout(() => {
this.props.onOpenToggle(false); this.props.onOpenToggle();
}, 0); }, 0);
} };
render() { render() {
const t = this.props.t; const t = this.props.t;
@@ -37,18 +36,17 @@ class ModalInternal extends React.Component<ModalInternalProps> {
return <AriaModal return <AriaModal
titleText={this.props.title} titleText={this.props.title}
underlayClickExits={this.props.underlayClickExits} underlayClickExits={this.props.underlayClickExits}
// @ts-ignore
underlayProps={this.props.underlayProps}
data-wd-key={this.props["data-wd-key"]} data-wd-key={this.props["data-wd-key"]}
verticallyCenter={true} verticallyCenter={true}
onExit={this.onClose} onExit={this.onClose}
dialogClass='maputnik-modal-container'
> >
<div className={classnames("maputnik-modal", this.props.className)} <div className={classnames("maputnik-modal", this.props.className)}
data-wd-key={this.props["data-wd-key"]} data-wd-key={this.props["data-wd-key"]}
> >
<header className="maputnik-modal-header"> <header className="maputnik-modal-header">
<h1 className="maputnik-modal-header-title">{this.props.title}</h1> <h1 className="maputnik-modal-header-title">{this.props.title}</h1>
<span className="maputnik-modal-header-space"></span> <span className="maputnik-space"></span>
<button className="maputnik-modal-header-toggle" <button className="maputnik-modal-header-toggle"
title={t("Close modal")} title={t("Close modal")}
onClick={this.onClose} onClick={this.onClose}
@@ -61,7 +59,7 @@ class ModalInternal extends React.Component<ModalInternalProps> {
<div className="maputnik-modal-content">{this.props.children}</div> <div className="maputnik-modal-content">{this.props.children}</div>
</div> </div>
</div> </div>
</AriaModal> </AriaModal>;
} }
else { else {
return false; return false;
@@ -1,20 +1,20 @@
import React from 'react' import React from "react";
import { type WithTranslation, withTranslation } from "react-i18next";
import type {LayerSpecification, SourceSpecification} from "maplibre-gl";
import InputButton from './InputButton' import InputButton from "../InputButton";
import Modal from './Modal' import Modal from "./Modal";
import FieldType from './FieldType' import FieldType from "../FieldType";
import FieldId from './FieldId' import FieldId from "../FieldId";
import FieldSource from './FieldSource' import FieldSource from "../FieldSource";
import FieldSourceLayer from './FieldSourceLayer' import FieldSourceLayer from "../FieldSourceLayer";
import type {LayerSpecification, SourceSpecification} from 'maplibre-gl' import { NON_SOURCE_LAYERS } from "../../libs/non-source-layers";
import { WithTranslation, withTranslation } from 'react-i18next';
import { NON_SOURCE_LAYERS } from '../libs/non-source-layers'
type ModalAddInternalProps = { type ModalAddInternalProps = {
layers: LayerSpecification[] layers: LayerSpecification[]
onLayersChange(layers: LayerSpecification[]): unknown onLayersChange(layers: LayerSpecification[]): unknown
isOpen: boolean isOpen: boolean
onOpenToggle(open: boolean): unknown onOpenToggle(): void
// A dict of source id's and the available source layers // A dict of source id's and the available source layers
sources: Record<string, SourceSpecification & {layers: string[]}>; sources: Record<string, SourceSpecification & {layers: string[]}>;
} & WithTranslation; } & WithTranslation;
@@ -23,50 +23,50 @@ type ModalAddState = {
type: LayerSpecification["type"] type: LayerSpecification["type"]
id: string id: string
source?: string source?: string
'source-layer'?: string "source-layer"?: string
error?: string | null error?: string | null
}; };
class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddState> { class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddState> {
addLayer = () => { addLayer = () => {
if (this.props.layers.some(l => l.id === this.state.id)) { if (this.props.layers.some(l => l.id === this.state.id)) {
this.setState({ error: this.props.t('Layer ID already exists') }) this.setState({ error: this.props.t("Layer ID already exists") });
return return;
} }
const changedLayers = this.props.layers.slice(0) const changedLayers = this.props.layers.slice(0);
const layer: ModalAddState = { const layer: ModalAddState = {
id: this.state.id, id: this.state.id,
type: this.state.type, type: this.state.type,
} };
if(this.state.type !== 'background') { if(this.state.type !== "background") {
layer.source = this.state.source layer.source = this.state.source;
if(!NON_SOURCE_LAYERS.includes(this.state.type) && this.state['source-layer']) { if(!NON_SOURCE_LAYERS.includes(this.state.type) && this.state["source-layer"]) {
layer['source-layer'] = this.state['source-layer'] layer["source-layer"] = this.state["source-layer"];
} }
} }
changedLayers.push(layer as LayerSpecification) changedLayers.push(layer as LayerSpecification);
this.setState({ error: null }, () => { this.setState({ error: null }, () => {
this.props.onLayersChange(changedLayers) this.props.onLayersChange(changedLayers);
this.props.onOpenToggle(false) this.props.onOpenToggle();
}) });
} };
constructor(props: ModalAddInternalProps) { constructor(props: ModalAddInternalProps) {
super(props) super(props);
const state: ModalAddState = { const state: ModalAddState = {
type: 'fill', type: "fill",
id: '', id: "",
error: null, error: null,
} };
if(Object.keys(props.sources).length > 0) { if(Object.keys(props.sources).length > 0) {
state.source = Object.keys(this.props.sources)[0]; state.source = Object.keys(this.props.sources)[0];
const sourceLayers = this.props.sources[state.source].layers || [] const sourceLayers = this.props.sources[state.source].layers || [];
if (sourceLayers.length > 0) { if (sourceLayers.length > 0) {
state['source-layer'] = sourceLayers[0]; state["source-layer"] = sourceLayers[0];
} }
} }
this.state = state; this.state = state;
@@ -104,22 +104,22 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
getSources(type: LayerSpecification["type"]) { getSources(type: LayerSpecification["type"]) {
switch(type) { switch(type) {
case 'background': case "background":
return []; return [];
case 'hillshade': case "hillshade":
case 'color-relief': case "color-relief":
return Object.entries(this.props.sources).filter(([_, v]) => v.type === 'raster-dem').map(([k, _]) => k); return Object.entries(this.props.sources).filter(([_, v]) => v.type === "raster-dem").map(([k, _]) => k);
case 'raster': case "raster":
return Object.entries(this.props.sources).filter(([_, v]) => v.type === 'raster').map(([k, _]) => k); return Object.entries(this.props.sources).filter(([_, v]) => v.type === "raster").map(([k, _]) => k);
case 'heatmap': case "heatmap":
case 'circle': case "circle":
case 'fill': case "fill":
case 'fill-extrusion': case "fill-extrusion":
case 'line': case "line":
case 'symbol': case "symbol":
return Object.entries(this.props.sources).filter(([_, v]) => v.type === 'vector' || v.type === 'geojson').map(([k, _]) => k); return Object.entries(this.props.sources).filter(([_, v]) => v.type === "vector" || v.type === "geojson").map(([k, _]) => k);
default: default:
return []; return [];
} }
} }
@@ -147,7 +147,7 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
return <Modal return <Modal
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle} onOpenToggle={this.props.onOpenToggle}
title={t('Add Layer')} title={t("Add Layer")}
data-wd-key="modal:add-layer" data-wd-key="modal:add-layer"
className="maputnik-add-modal" className="maputnik-add-modal"
> >
@@ -157,7 +157,7 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
value={this.state.id} value={this.state.id}
wdKey="add-layer.layer-id" wdKey="add-layer.layer-id"
onChange={(v: string) => { onChange={(v: string) => {
this.setState({ id: v, error: null }) this.setState({ id: v, error: null });
}} }}
/> />
<FieldType <FieldType
@@ -165,7 +165,7 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
wdKey="add-layer.layer-type" wdKey="add-layer.layer-type"
onChange={(v: LayerSpecification["type"]) => this.setState({ type: v })} onChange={(v: LayerSpecification["type"]) => this.setState({ type: v })}
/> />
{this.state.type !== 'background' && {this.state.type !== "background" &&
<FieldSource <FieldSource
sourceIds={sources} sourceIds={sources}
wdKey="add-layer.layer-source-block" wdKey="add-layer.layer-source-block"
@@ -176,8 +176,8 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
{!NON_SOURCE_LAYERS.includes(this.state.type) && {!NON_SOURCE_LAYERS.includes(this.state.type) &&
<FieldSourceLayer <FieldSourceLayer
sourceLayerIds={layers} sourceLayerIds={layers}
value={this.state['source-layer']} value={this.state["source-layer"]}
onChange={(v: string) => this.setState({ 'source-layer': v })} onChange={(v: string) => this.setState({ "source-layer": v })}
/> />
} }
<InputButton <InputButton
@@ -188,7 +188,7 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
{t("Add Layer")} {t("Add Layer")}
</InputButton> </InputButton>
</div> </div>
</Modal> </Modal>;
} }
} }
@@ -1,7 +1,7 @@
import React from 'react' import React from "react";
import { Trans, type WithTranslation, withTranslation } from "react-i18next";
import { Trans, WithTranslation, withTranslation } from 'react-i18next'; import Modal from "./Modal";
import Modal from './Modal'
type ModalDebugInternalProps = { type ModalDebugInternalProps = {
@@ -9,7 +9,7 @@ type ModalDebugInternalProps = {
renderer: string renderer: string
onChangeMaplibreGlDebug(key: string, checked: boolean): unknown onChangeMaplibreGlDebug(key: string, checked: boolean): unknown
onChangeOpenlayersDebug(key: string, checked: boolean): unknown onChangeOpenlayersDebug(key: string, checked: boolean): unknown
onOpenToggle(value: boolean): unknown onOpenToggle(): void
maplibreGlDebugOptions?: object maplibreGlDebugOptions?: object
openlayersDebugOptions?: object openlayersDebugOptions?: object
mapView: { mapView: {
@@ -34,35 +34,35 @@ class ModalDebugInternal extends React.Component<ModalDebugInternalProps> {
data-wd-key="modal:debug" data-wd-key="modal:debug"
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle} onOpenToggle={this.props.onOpenToggle}
title={t('Debug')} title={t("Debug")}
> >
<section className="maputnik-modal-section maputnik-modal-shortcuts"> <section className="maputnik-modal-section maputnik-modal-shortcuts">
<h1>{t("Options")}</h1> <h1>{t("Options")}</h1>
{this.props.renderer === 'mlgljs' && {this.props.renderer === "mlgljs" &&
<ul> <ul>
{Object.entries(this.props.maplibreGlDebugOptions!).map(([key, val]) => { {Object.entries(this.props.maplibreGlDebugOptions!).map(([key, val]) => {
return <li key={key}> return <li key={key}>
<label> <label>
<input type="checkbox" checked={val} onChange={(e) => this.props.onChangeMaplibreGlDebug(key, e.target.checked)} /> {key} <input type="checkbox" checked={val} onChange={(e) => this.props.onChangeMaplibreGlDebug(key, e.target.checked)} /> {key}
</label> </label>
</li> </li>;
})} })}
</ul> </ul>
} }
{this.props.renderer === 'ol' && {this.props.renderer === "ol" &&
<ul> <ul>
{Object.entries(this.props.openlayersDebugOptions!).map(([key, val]) => { {Object.entries(this.props.openlayersDebugOptions!).map(([key, val]) => {
return <li key={key}> return <li key={key}>
<label> <label>
<input type="checkbox" checked={val} onChange={(e) => this.props.onChangeOpenlayersDebug(key, e.target.checked)} /> {key} <input type="checkbox" checked={val} onChange={(e) => this.props.onChangeOpenlayersDebug(key, e.target.checked)} /> {key}
</label> </label>
</li> </li>;
})} })}
</ul> </ul>
} }
</section> </section>
<section className="maputnik-modal-section"> <section className="maputnik-modal-section">
<h1>Links</h1> <h1>{t("Links")}</h1>
<p> <p>
<Trans t={t}> <Trans t={t}>
<a <a
@@ -71,11 +71,11 @@ class ModalDebugInternal extends React.Component<ModalDebugInternalProps> {
href={`https://www.openstreetmap.org/#map=${osmZoom}/${osmLat}/${osmLon}`} href={`https://www.openstreetmap.org/#map=${osmZoom}/${osmLat}/${osmLon}`}
> >
Open in OSM Open in OSM
</a> &mdash; Opens the current view on openstreetmap.org </a>. Opens the current view on openstreetmap.org
</Trans> </Trans>
</p> </p>
</section> </section>
</Modal> </Modal>;
} }
} }
@@ -1,17 +1,17 @@
import React from 'react' import React from "react";
import Slugify from 'slugify' import Slugify from "slugify";
import {saveAs} from 'file-saver' import {saveAs} from "file-saver";
import {version} from 'maplibre-gl/package.json' import {version} from "maplibre-gl/package.json";
import {format} from '@maplibre/maplibre-gl-style-spec' import {format} from "@maplibre/maplibre-gl-style-spec";
import {MdMap, MdSave} from 'react-icons/md' import {MdMap, MdSave} from "react-icons/md";
import {WithTranslation, withTranslation} from 'react-i18next'; import {type WithTranslation, withTranslation} from "react-i18next";
import FieldString from './FieldString' import FieldString from "../FieldString";
import InputButton from './InputButton' import InputButton from "../InputButton";
import Modal from './Modal' import Modal from "./Modal";
import style from '../libs/style' import style from "../../libs/style";
import fieldSpecAdditional from '../libs/field-spec-additional' import fieldSpecAdditional from "../../libs/field-spec-additional";
import type {OnStyleChangedCallback, StyleSpecificationWithId} from '../libs/definitions' import type {OnStyleChangedCallback, StyleSpecificationWithId} from "../../libs/definitions";
const MAPLIBRE_GL_VERSION = version; const MAPLIBRE_GL_VERSION = version;
@@ -22,7 +22,7 @@ type ModalExportInternalProps = {
mapStyle: StyleSpecificationWithId mapStyle: StyleSpecificationWithId
onStyleChanged: OnStyleChangedCallback onStyleChanged: OnStyleChangedCallback
isOpen: boolean isOpen: boolean
onOpenToggle(...args: unknown[]): unknown onOpenToggle(): void
onSetFileHandle(fileHandle: FileSystemFileHandle | null): unknown onSetFileHandle(fileHandle: FileSystemFileHandle | null): unknown
fileHandle: FileSystemFileHandle | null fileHandle: FileSystemFileHandle | null
} & WithTranslation; } & WithTranslation;
@@ -41,12 +41,12 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
exportName() { exportName() {
if (this.props.mapStyle.name) { if (this.props.mapStyle.name) {
return Slugify(this.props.mapStyle.name, { return Slugify(this.props.mapStyle.name, {
replacement: '_', replacement: "_",
remove: /[*\-+~.()'"!:]/g, remove: /[*\-+~.()'"!:]/g,
lower: true lower: true
}); });
} else { } else {
return this.props.mapStyle.id return this.props.mapStyle.id;
} }
} }
@@ -99,7 +99,7 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
let fileHandle = this.props.fileHandle; let fileHandle = this.props.fileHandle;
if (fileHandle == null) { if (fileHandle == null) {
fileHandle = await this.createFileHandle(); fileHandle = await this.createFileHandle();
this.props.onSetFileHandle(fileHandle) this.props.onSetFileHandle(fileHandle);
if (fileHandle == null) return; if (fileHandle == null) return;
} }
@@ -113,7 +113,7 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
const tokenStyle = this.tokenizedStyle(); const tokenStyle = this.tokenizedStyle();
const fileHandle = await this.createFileHandle(); const fileHandle = await this.createFileHandle();
this.props.onSetFileHandle(fileHandle) this.props.onSetFileHandle(fileHandle);
if (fileHandle == null) return; if (fileHandle == null) return;
const writable = await fileHandle.createWritable(); const writable = await fileHandle.createWritable();
@@ -134,7 +134,7 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
}; };
const fileHandle = await window.showSaveFilePicker(pickerOpts) as FileSystemFileHandle; const fileHandle = await window.showSaveFilePicker(pickerOpts) as FileSystemFileHandle;
this.props.onSetFileHandle(fileHandle) this.props.onSetFileHandle(fileHandle);
return fileHandle; return fileHandle;
} }
@@ -145,8 +145,8 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
...this.props.mapStyle.metadata as any, ...this.props.mapStyle.metadata as any,
[property]: value [property]: value
} }
} };
this.props.onStyleChanged(changedStyle) this.props.onStyleChanged(changedStyle);
} }
@@ -157,7 +157,7 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
data-wd-key="modal:export" data-wd-key="modal:export"
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle} onOpenToggle={this.props.onOpenToggle}
title={t('Save Style')} title={t("Save Style")}
className="maputnik-export-modal" className="maputnik-export-modal"
> >
@@ -171,25 +171,25 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
<FieldString <FieldString
label={fsa.maputnik.maptiler_access_token.label} label={fsa.maputnik.maptiler_access_token.label}
fieldSpec={fsa.maputnik.maptiler_access_token} fieldSpec={fsa.maputnik.maptiler_access_token}
value={(this.props.mapStyle.metadata || {} as any)['maputnik:openmaptiles_access_token']} value={(this.props.mapStyle.metadata || {} as any)["maputnik:openmaptiles_access_token"]}
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")} onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
/> />
<FieldString <FieldString
label={fsa.maputnik.thunderforest_access_token.label} label={fsa.maputnik.thunderforest_access_token.label}
fieldSpec={fsa.maputnik.thunderforest_access_token} fieldSpec={fsa.maputnik.thunderforest_access_token}
value={(this.props.mapStyle.metadata || {} as any)['maputnik:thunderforest_access_token']} value={(this.props.mapStyle.metadata || {} as any)["maputnik:thunderforest_access_token"]}
onChange={this.changeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")} onChange={this.changeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
/> />
<FieldString <FieldString
label={fsa.maputnik.stadia_access_token.label} label={fsa.maputnik.stadia_access_token.label}
fieldSpec={fsa.maputnik.stadia_access_token} fieldSpec={fsa.maputnik.stadia_access_token}
value={(this.props.mapStyle.metadata || {} as any)['maputnik:stadia_access_token']} value={(this.props.mapStyle.metadata || {} as any)["maputnik:stadia_access_token"]}
onChange={this.changeMetadataProperty.bind(this, "maputnik:stadia_access_token")} onChange={this.changeMetadataProperty.bind(this, "maputnik:stadia_access_token")}
/> />
<FieldString <FieldString
label={fsa.maputnik.locationiq_access_token.label} label={fsa.maputnik.locationiq_access_token.label}
fieldSpec={fsa.maputnik.locationiq_access_token} fieldSpec={fsa.maputnik.locationiq_access_token}
value={(this.props.mapStyle.metadata || {} as any)['maputnik:locationiq_access_token']} value={(this.props.mapStyle.metadata || {} as any)["maputnik:locationiq_access_token"]}
onChange={this.changeMetadataProperty.bind(this, "maputnik:locationiq_access_token")} onChange={this.changeMetadataProperty.bind(this, "maputnik:locationiq_access_token")}
/> />
</div> </div>
@@ -213,7 +213,7 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
</div> </div>
</section> </section>
</Modal> </Modal>;
} }
} }
+155
View File
@@ -0,0 +1,155 @@
import React from "react";
import { withTranslation, type WithTranslation } from "react-i18next";
import { MdDelete } from "react-icons/md";
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import Modal from "./Modal";
import FieldString from "../FieldString";
import InputButton from "../InputButton";
import { PiListPlusBold } from "react-icons/pi";
import { type StyleSpecificationWithId } from "../../libs/definitions";
import { type SchemaSpecification } from "maplibre-gl";
import Doc from "../Doc";
type ModalGlobalStateInternalProps = {
mapStyle: StyleSpecificationWithId;
isOpen: boolean;
onStyleChanged(style: StyleSpecificationWithId): void;
onOpenToggle(): void
} & WithTranslation;
type GlobalStateVariable = {
key: string;
value: any;
};
const ModalGlobalStateInternal: React.FC<ModalGlobalStateInternalProps> = (props) => {
const getGlobalStateVariables = (): GlobalStateVariable[] => {
const style = props.mapStyle;
const globalState = style.state || {};
return Object.entries(globalState).map(([key, value]) => ({
key,
value: value.default
}));
};
const setGlobalStateVariables = (variables: GlobalStateVariable[]) => {
const style = { ...props.mapStyle };
// Create the globalState object from the variables array
const globalState: Record<string, SchemaSpecification> = {};
for (const variable of variables) {
if (variable.key.trim() !== "") {
globalState[variable.key] = {
default: variable.value
};
}
}
style.state = Object.keys(globalState).length > 0 ? globalState : undefined;
props.onStyleChanged(style);
};
const onAddVariable = () => {
const variables = getGlobalStateVariables();
let index = 1;
while (variables.find(v => v.key === `key${index}`)) {
index++;
}
variables.push({ key: `key${index}`, value: "value" });
setGlobalStateVariables(variables);
};
const onRemoveVariable = (index: number) => {
const variables = getGlobalStateVariables();
variables.splice(index, 1);
setGlobalStateVariables(variables);
};
const onChangeVariableKey = (index: number, newKey: string) => {
const variables = getGlobalStateVariables();
variables[index].key = newKey || "";
setGlobalStateVariables(variables);
};
const onChangeVariableValue = (index: number, newValue: string) => {
const variables = getGlobalStateVariables();
variables[index].value = newValue || "";
setGlobalStateVariables(variables);
};
const variables = getGlobalStateVariables();
const variableFields = variables.map((variable, index) => (
<tr key={index}>
<td>
<FieldString
label={props.t("Key")}
value={variable.key}
onChange={(value) => onChangeVariableKey(index, value || "")}
data-wd-key={"global-state-variable-key:" + index}
/>
</td>
<td>
<FieldString
label={props.t("Value")}
value={variable.value}
onChange={(value) => onChangeVariableValue(index, value || "")}
data-wd-key={"global-state-variable-value:" + index}
/>
</td>
<td style={{ verticalAlign: "middle"}}>
<InputButton
onClick={() => onRemoveVariable(index)}
title={props.t("Remove variable")}
data-wd-key="global-state-remove-variable"
>
<MdDelete />
</InputButton>
</td>
</tr>
));
return (
<Modal
data-wd-key="modal:global-state"
isOpen={props.isOpen}
onOpenToggle={props.onOpenToggle}
title={props.t("Global State Variables")}
>
{variables.length === 0 &&
<div>
<p>{props.t("No global state variables defined. Add variables to create reusable values in your style.")}</p>
<div key="doc" className="maputnik-doc-inline">
<Doc fieldSpec={latest.$root.state} />
</div>
</div>
}
{variables.length > 0 &&
<table>
<thead>
</thead>
<tbody>
{variableFields}
</tbody>
</table>
}
<div>
<InputButton
onClick={onAddVariable}
data-wd-key="global-state-add-variable"
>
<PiListPlusBold />
{props.t("Add Variable")}
</InputButton>
</div>
</Modal>
);
};
const ModalGlobalState = withTranslation()(ModalGlobalStateInternal);
export default ModalGlobalState;
@@ -1,8 +1,8 @@
import React from 'react' import React from "react";
import { type WithTranslation, withTranslation } from "react-i18next";
import InputButton from './InputButton' import InputButton from "../InputButton";
import Modal from './Modal' import Modal from "./Modal";
import { WithTranslation, withTranslation } from 'react-i18next';
type ModalLoadingInternalProps = { type ModalLoadingInternalProps = {
@@ -14,21 +14,12 @@ type ModalLoadingInternalProps = {
class ModalLoadingInternal extends React.Component<ModalLoadingInternalProps> { class ModalLoadingInternal extends React.Component<ModalLoadingInternalProps> {
underlayOnClick(e: Event) {
// This stops click events falling through to underlying modals.
e.stopPropagation();
}
render() { render() {
const t = this.props.t; const t = this.props.t;
return <Modal return <Modal
data-wd-key="modal:loading" data-wd-key="modal:loading"
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
underlayClickExits={false} underlayClickExits={false}
underlayProps={{
// @ts-ignore
onClick: (e: Event) => underlayProps(e)
}}
title={this.props.title} title={this.props.title}
onOpenToggle={() => this.props.onCancel()} onOpenToggle={() => this.props.onCancel()}
> >
@@ -40,7 +31,7 @@ class ModalLoadingInternal extends React.Component<ModalLoadingInternalProps> {
{t("Cancel")} {t("Cancel")}
</InputButton> </InputButton>
</p> </p>
</Modal> </Modal>;
} }
} }
@@ -1,16 +1,15 @@
import React, { FormEvent } from 'react' import React, { type FormEvent } from "react";
import {MdFileUpload} from 'react-icons/md' import {MdFileUpload} from "react-icons/md";
import {MdAddCircleOutline} from 'react-icons/md' import {MdAddCircleOutline} from "react-icons/md";
import FileReaderInput, { Result } from 'react-file-reader-input' import { Trans, type WithTranslation, withTranslation } from "react-i18next";
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
import ModalLoading from './ModalLoading' import ModalLoading from "./ModalLoading";
import Modal from './Modal' import Modal from "./Modal";
import InputButton from './InputButton' import InputButton from "../InputButton";
import InputUrl from './InputUrl' import InputUrl from "../InputUrl";
import style from '../libs/style' import style from "../../libs/style";
import publicStyles from '../config/styles.json' import publicStyles from "../../config/styles.json";
type PublicStyleProps = { type PublicStyleProps = {
url: string url: string
@@ -39,13 +38,13 @@ class PublicStyle extends React.Component<PublicStyleProps> {
}} }}
></div> ></div>
</InputButton> </InputButton>
</div> </div>;
} }
} }
type ModalOpenInternalProps = { type ModalOpenInternalProps = {
isOpen: boolean isOpen: boolean
onOpenToggle(...args: unknown[]): unknown onOpenToggle(): void
onStyleOpen(...args: unknown[]): unknown onStyleOpen(...args: unknown[]): unknown
fileHandle: FileSystemFileHandle | null fileHandle: FileSystemFileHandle | null
} & WithTranslation; } & WithTranslation;
@@ -68,7 +67,7 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
clearError() { clearError() {
this.setState({ this.setState({
error: null error: null
}) });
} }
onCancelActiveRequest(e: Event) { onCancelActiveRequest(e: Event) {
@@ -90,7 +89,7 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
let canceled: boolean = false; let canceled: boolean = false;
fetch(styleUrl, { fetch(styleUrl, {
mode: 'cors', mode: "cors",
credentials: "same-origin" credentials: "same-origin"
}) })
.then(function(response) { .then(function(response) {
@@ -106,10 +105,10 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
activeRequestUrl: null activeRequestUrl: null
}); });
const mapStyle = style.ensureStyleValidity(body) const mapStyle = style.ensureStyleValidity(body);
console.log('Loaded style ', mapStyle.id) console.log("Loaded style ", mapStyle.id);
this.props.onStyleOpen(mapStyle) this.props.onStyleOpen(mapStyle);
this.onOpenToggle() this.onOpenToggle();
}) })
.catch((err) => { .catch((err) => {
this.setState({ this.setState({
@@ -118,8 +117,8 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
activeRequestUrl: null activeRequestUrl: null
}); });
console.error(err); console.error(err);
console.warn('Could not open the style URL', styleUrl) console.warn("Could not open the style URL", styleUrl);
}) });
this.setState({ this.setState({
activeRequest: { activeRequest: {
@@ -128,13 +127,13 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
} }
}, },
activeRequestUrl: styleUrl activeRequestUrl: styleUrl
}) });
} };
onSubmitUrl = (e: FormEvent<HTMLFormElement>) => { onSubmitUrl = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
this.onStyleSelect(this.state.styleUrl); this.onStyleSelect(this.state.styleUrl);
} };
onOpenFile = async () => { onOpenFile = async () => {
this.clearError(); this.clearError();
@@ -155,24 +154,26 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
let mapStyle; let mapStyle;
try { try {
mapStyle = JSON.parse(content) mapStyle = JSON.parse(content);
} catch (err) { } catch (err) {
this.setState({ this.setState({
error: (err as Error).toString() error: (err as Error).toString()
}); });
return; return;
} }
mapStyle = style.ensureStyleValidity(mapStyle) mapStyle = style.ensureStyleValidity(mapStyle);
this.props.onStyleOpen(mapStyle, fileHandle); this.props.onStyleOpen(mapStyle, fileHandle);
this.onOpenToggle(); this.onOpenToggle();
return file; return file;
} };
// it is not guaranteed that the File System Access API is available on all // 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. // browsers. If the function is not available, a fallback behavior is used.
onFileChanged = async (_: any, files: Result[]) => { onFileChanged = (files: FileList | null) => {
const [, file] = files[0]; if (!files) return;
if (files.length === 0) return;
const file = files[0];
const reader = new FileReader(); const reader = new FileReader();
this.clearError(); this.clearError();
@@ -180,7 +181,7 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
reader.onload = e => { reader.onload = e => {
let mapStyle; let mapStyle;
try { try {
mapStyle = JSON.parse(e.target?.result as string) mapStyle = JSON.parse(e.target?.result as string);
} }
catch(err) { catch(err) {
this.setState({ this.setState({
@@ -188,12 +189,12 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
}); });
return; return;
} }
mapStyle = style.ensureStyleValidity(mapStyle) mapStyle = style.ensureStyleValidity(mapStyle);
this.props.onStyleOpen(mapStyle); this.props.onStyleOpen(mapStyle);
this.onOpenToggle(); this.onOpenToggle();
} };
reader.onerror = e => console.log(e.target); reader.onerror = e => console.log(e.target);
} };
onOpenToggle() { onOpenToggle() {
this.setState({ this.setState({
@@ -207,7 +208,7 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
this.setState({ this.setState({
styleUrl: url, styleUrl: url,
}); });
} };
render() { render() {
const t = this.props.t; const t = this.props.t;
@@ -218,8 +219,8 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
title={style.title} title={style.title}
thumbnailUrl={style.thumbnail} thumbnailUrl={style.thumbnail}
onSelect={this.onStyleSelect} onSelect={this.onStyleSelect}
/> />;
}) });
let errorElement; let errorElement;
if(this.state.error) { if(this.state.error) {
@@ -237,7 +238,7 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
data-wd-key="modal:open" data-wd-key="modal:open"
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
onOpenToggle={() => this.onOpenToggle()} onOpenToggle={() => this.onOpenToggle()}
title={t('Open Style')} title={t("Open Style")}
> >
{errorElement} {errorElement}
<section className="maputnik-modal-section"> <section className="maputnik-modal-section">
@@ -246,13 +247,15 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
<div> <div>
{typeof window.showOpenFilePicker === "function" ? ( {typeof window.showOpenFilePicker === "function" ? (
<InputButton <InputButton
data-wd-key="modal:open.file.button"
className="maputnik-big-button" className="maputnik-big-button"
onClick={this.onOpenFile}><MdFileUpload/> {t("Open Style")} onClick={this.onOpenFile}><MdFileUpload/> {t("Open Style")}
</InputButton> </InputButton>
) : ( ) : (
<FileReaderInput onChange={this.onFileChanged} tabIndex={-1} aria-label={t("Open Style")}> <label>
<InputButton className="maputnik-upload-button"><MdFileUpload /> {t("Open Style")}</InputButton> <a className="maputnik-button maputnik-upload-button" aria-label={t("Open Style")}><MdFileUpload /> {t("Open Style")}</a>
</FileReaderInput> <input data-wd-key="modal:open.file.input" type="file" style={{ display: "none" }} onChange={(e) => this.onFileChanged(e.target.files)} />
</label>
)} )}
</div> </div>
</section> </section>
@@ -299,12 +302,12 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
<ModalLoading <ModalLoading
isOpen={!!this.state.activeRequest} isOpen={!!this.state.activeRequest}
title={t('Loading style')} title={t("Loading style")}
onCancel={(e: Event) => this.onCancelActiveRequest(e)} onCancel={(e: Event) => this.onCancelActiveRequest(e)}
message={t("Loading: {{requestUrl}}", { requestUrl: this.state.activeRequestUrl })} message={t("Loading: {{requestUrl}}", { requestUrl: this.state.activeRequestUrl })}
/> />
</div> </div>
) );
} }
} }
@@ -1,32 +1,32 @@
import React from 'react' import React from "react";
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json' 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 { WithTranslation, withTranslation } from 'react-i18next'; import { type WithTranslation, withTranslation } from "react-i18next";
import FieldArray from './FieldArray' import FieldArray from "../FieldArray";
import FieldNumber from './FieldNumber' import FieldNumber from "../FieldNumber";
import FieldString from './FieldString' import FieldString from "../FieldString";
import FieldUrl from './FieldUrl' import FieldUrl from "../FieldUrl";
import FieldSelect from './FieldSelect' import FieldSelect from "../FieldSelect";
import FieldEnum from './FieldEnum' import FieldEnum from "../FieldEnum";
import FieldColor from './FieldColor' import FieldColor from "../FieldColor";
import Modal from './Modal' import Modal from "./Modal";
import fieldSpecAdditional from '../libs/field-spec-additional' import fieldSpecAdditional from "../../libs/field-spec-additional";
import type {OnStyleChangedCallback, StyleSpecificationWithId} from '../libs/definitions'; import type {OnStyleChangedCallback, StyleSpecificationWithId} from "../../libs/definitions";
type ModalSettingsInternalProps = { type ModalSettingsInternalProps = {
mapStyle: StyleSpecificationWithId mapStyle: StyleSpecificationWithId
onStyleChanged: OnStyleChangedCallback onStyleChanged: OnStyleChangedCallback
onChangeMetadataProperty(...args: unknown[]): unknown onChangeMetadataProperty(...args: unknown[]): unknown
isOpen: boolean isOpen: boolean
onOpenToggle(...args: unknown[]): unknown onOpenToggle(): void
} & WithTranslation; } & WithTranslation;
class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps> { class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps> {
changeTransitionProperty(property: keyof TransitionSpecification, value: number | undefined) { changeTransitionProperty(property: keyof TransitionSpecification, value: number | undefined) {
const transition = { const transition = {
...this.props.mapStyle.transition, ...this.props.mapStyle.transition,
} };
if (value === undefined) { if (value === undefined) {
delete transition[property]; delete transition[property];
@@ -44,7 +44,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
changeLightProperty(property: keyof LightSpecification, value: any) { changeLightProperty(property: keyof LightSpecification, value: any) {
const light = { const light = {
...this.props.mapStyle.light, ...this.props.mapStyle.light,
} };
if (value === undefined) { if (value === undefined) {
delete light[property]; delete light[property];
@@ -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) { changeStyleProperty(property: keyof StyleSpecification | "owner", value: any) {
const changedStyle = { const changedStyle = {
...this.props.mapStyle, ...this.props.mapStyle,
@@ -103,12 +121,13 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
const light = this.props.mapStyle.light || {}; const light = this.props.mapStyle.light || {};
const transition = this.props.mapStyle.transition || {}; const transition = this.props.mapStyle.transition || {};
const terrain = this.props.mapStyle.terrain || {} as TerrainSpecification; const terrain = this.props.mapStyle.terrain || {} as TerrainSpecification;
const projection = this.props.mapStyle.projection || {} as ProjectionSpecification;
return <Modal return <Modal
data-wd-key="modal:settings" data-wd-key="modal:settings"
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle} onOpenToggle={this.props.onOpenToggle}
title={t('Style Settings')} title={t("Style Settings")}
> >
<div className="modal:settings"> <div className="modal:settings">
<FieldString <FieldString
@@ -116,21 +135,21 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.$root.name} fieldSpec={latest.$root.name}
data-wd-key="modal:settings.name" data-wd-key="modal:settings.name"
value={this.props.mapStyle.name} value={this.props.mapStyle.name}
onChange={this.changeStyleProperty.bind(this, "name")} onChange={(value) => this.changeStyleProperty("name", value)}
/> />
<FieldString <FieldString
label={t("Owner")} label={t("Owner")}
fieldSpec={{doc: t("Owner ID of the style. Used by Mapbox or future style APIs.")}} fieldSpec={{doc: t("Owner ID of the style. Used by Mapbox or future style APIs.")}}
data-wd-key="modal:settings.owner" data-wd-key="modal:settings.owner"
value={(this.props.mapStyle as any).owner} value={(this.props.mapStyle as any).owner}
onChange={this.changeStyleProperty.bind(this, "owner")} onChange={(value) => this.changeStyleProperty("owner", value)}
/> />
<FieldUrl <FieldUrl
fieldSpec={latest.$root.sprite} fieldSpec={latest.$root.sprite}
label={t("Sprite URL")} label={t("Sprite URL")}
data-wd-key="modal:settings.sprite" data-wd-key="modal:settings.sprite"
value={this.props.mapStyle.sprite as string} value={this.props.mapStyle.sprite as string}
onChange={this.changeStyleProperty.bind(this, "sprite")} onChange={(value) => this.changeStyleProperty("sprite", value)}
/> />
<FieldUrl <FieldUrl
@@ -138,39 +157,39 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.$root.glyphs} fieldSpec={latest.$root.glyphs}
data-wd-key="modal:settings.glyphs" data-wd-key="modal:settings.glyphs"
value={this.props.mapStyle.glyphs as string} value={this.props.mapStyle.glyphs as string}
onChange={this.changeStyleProperty.bind(this, "glyphs")} onChange={(value) => this.changeStyleProperty("glyphs", value)}
/> />
<FieldString <FieldString
label={fsa.maputnik.maptiler_access_token.label} label={fsa.maputnik.maptiler_access_token.label}
fieldSpec={fsa.maputnik.maptiler_access_token} fieldSpec={fsa.maputnik.maptiler_access_token}
data-wd-key="modal:settings.maputnik:openmaptiles_access_token" data-wd-key="modal:settings.maputnik:openmaptiles_access_token"
value={metadata['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 <FieldString
label={fsa.maputnik.thunderforest_access_token.label} label={fsa.maputnik.thunderforest_access_token.label}
fieldSpec={fsa.maputnik.thunderforest_access_token} fieldSpec={fsa.maputnik.thunderforest_access_token}
data-wd-key="modal:settings.maputnik:thunderforest_access_token" data-wd-key="modal:settings.maputnik:thunderforest_access_token"
value={metadata['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 <FieldString
label={fsa.maputnik.stadia_access_token.label} label={fsa.maputnik.stadia_access_token.label}
fieldSpec={fsa.maputnik.stadia_access_token} fieldSpec={fsa.maputnik.stadia_access_token}
data-wd-key="modal:settings.maputnik:stadia_access_token" data-wd-key="modal:settings.maputnik:stadia_access_token"
value={metadata['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 <FieldString
label={fsa.maputnik.locationiq_access_token.label} label={fsa.maputnik.locationiq_access_token.label}
fieldSpec={fsa.maputnik.locationiq_access_token} fieldSpec={fsa.maputnik.locationiq_access_token}
data-wd-key="modal:settings.maputnik:locationiq_access_token" data-wd-key="modal:settings.maputnik:locationiq_access_token"
value={metadata['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 <FieldArray
@@ -180,7 +199,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
type="number" type="number"
value={mapStyle.center || []} value={mapStyle.center || []}
default={[0, 0]} default={[0, 0]}
onChange={this.changeStyleProperty.bind(this, "center")} onChange={(value) => this.changeStyleProperty("center", value)}
/> />
<FieldNumber <FieldNumber
@@ -188,7 +207,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.$root.zoom} fieldSpec={latest.$root.zoom}
value={mapStyle.zoom} value={mapStyle.zoom}
default={0} default={0}
onChange={this.changeStyleProperty.bind(this, "zoom")} onChange={(value) => this.changeStyleProperty("zoom", value)}
/> />
<FieldNumber <FieldNumber
@@ -196,7 +215,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.$root.bearing} fieldSpec={latest.$root.bearing}
value={mapStyle.bearing} value={mapStyle.bearing}
default={latest.$root.bearing.default} default={latest.$root.bearing.default}
onChange={this.changeStyleProperty.bind(this, "bearing")} onChange={(value) => this.changeStyleProperty("bearing", value)}
/> />
<FieldNumber <FieldNumber
@@ -204,7 +223,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.$root.pitch} fieldSpec={latest.$root.pitch}
value={mapStyle.pitch} value={mapStyle.pitch}
default={latest.$root.pitch.default} default={latest.$root.pitch.default}
onChange={this.changeStyleProperty.bind(this, "pitch")} onChange={(value) => this.changeStyleProperty("pitch", value)}
/> />
<FieldEnum <FieldEnum
@@ -214,7 +233,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
value={light.anchor as string} value={light.anchor as string}
options={Object.keys(latest.light.anchor.values)} options={Object.keys(latest.light.anchor.values)}
default={latest.light.anchor.default} default={latest.light.anchor.default}
onChange={this.changeLightProperty.bind(this, "anchor")} onChange={(value) => this.changeLightProperty("anchor", value)}
/> />
<FieldColor <FieldColor
@@ -222,7 +241,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.light.color} fieldSpec={latest.light.color}
value={light.color as string} value={light.color as string}
default={latest.light.color.default} default={latest.light.color.default}
onChange={this.changeLightProperty.bind(this, "color")} onChange={(value) => this.changeLightProperty("color", value)}
/> />
<FieldNumber <FieldNumber
@@ -230,7 +249,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.light.intensity} fieldSpec={latest.light.intensity}
value={light.intensity as number} value={light.intensity as number}
default={latest.light.intensity.default} default={latest.light.intensity.default}
onChange={this.changeLightProperty.bind(this, "intensity")} onChange={(value) => this.changeLightProperty("intensity", value)}
/> />
<FieldArray <FieldArray
@@ -240,7 +259,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
length={latest.light.position.length} length={latest.light.position.length}
value={light.position as number[]} value={light.position as number[]}
default={latest.light.position.default} default={latest.light.position.default}
onChange={this.changeLightProperty.bind(this, "position")} onChange={(value) => this.changeLightProperty("position", value)}
/> />
<FieldString <FieldString
@@ -248,7 +267,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.terrain.source} fieldSpec={latest.terrain.source}
data-wd-key="modal:settings.maputnik:terrain_source" data-wd-key="modal:settings.maputnik:terrain_source"
value={terrain.source} value={terrain.source}
onChange={this.changeTerrainProperty.bind(this, "source")} onChange={(value) => this.changeTerrainProperty("source", value)}
/> />
<FieldNumber <FieldNumber
@@ -256,7 +275,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.terrain.exaggeration} fieldSpec={latest.terrain.exaggeration}
value={terrain.exaggeration} value={terrain.exaggeration}
default={latest.terrain.exaggeration.default} default={latest.terrain.exaggeration.default}
onChange={this.changeTerrainProperty.bind(this, "exaggeration")} onChange={(value) => this.changeTerrainProperty("exaggeration", value)}
/> />
<FieldNumber <FieldNumber
@@ -264,7 +283,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.transition.delay} fieldSpec={latest.transition.delay}
value={transition.delay} value={transition.delay}
default={latest.transition.delay.default} default={latest.transition.delay.default}
onChange={this.changeTransitionProperty.bind(this, "delay")} onChange={(value) => this.changeTransitionProperty("delay", value)}
/> />
<FieldNumber <FieldNumber
@@ -272,7 +291,20 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.transition.duration} fieldSpec={latest.transition.duration}
value={transition.duration} value={transition.duration}
default={latest.transition.duration.default} 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 <FieldSelect
@@ -280,16 +312,16 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={fsa.maputnik.style_renderer} fieldSpec={fsa.maputnik.style_renderer}
data-wd-key="modal:settings.maputnik:renderer" data-wd-key="modal:settings.maputnik:renderer"
options={[ options={[
['mlgljs', 'MapLibreGL JS'], ["mlgljs", "MapLibreGL JS"],
['ol', t('Open Layers (experimental)')], ["ol", t("Open Layers (experimental)")],
]} ]}
value={metadata['maputnik:renderer'] || 'mlgljs'} value={metadata["maputnik:renderer"] || "mlgljs"}
onChange={onChangeMetadataProperty.bind(this, 'maputnik:renderer')} onChange={(value) => onChangeMetadataProperty("maputnik:renderer", value)}
/> />
</div> </div>
</Modal> </Modal>;
} }
} }
const ModalSettings = withTranslation()(ModalSettingsInternal) const ModalSettings = withTranslation()(ModalSettingsInternal);
export default ModalSettings; export default ModalSettings;
@@ -1,12 +1,12 @@
import React from 'react' import React from "react";
import { Trans, WithTranslation, withTranslation } from 'react-i18next'; import { Trans, type WithTranslation, withTranslation } from "react-i18next";
import Modal from './Modal' import Modal from "./Modal";
type ModalShortcutsInternalProps = { type ModalShortcutsInternalProps = {
isOpen: boolean isOpen: boolean
onOpenToggle(...args: unknown[]): unknown onOpenToggle(): void
} & WithTranslation; } & WithTranslation;
@@ -46,7 +46,7 @@ class ModalShortcutsInternal extends React.Component<ModalShortcutsInternalProps
key: <kbd>!</kbd>, key: <kbd>!</kbd>,
text: t("Debug modal") text: t("Debug modal")
}, },
] ];
const mapShortcuts = [ const mapShortcuts = [
@@ -98,14 +98,14 @@ class ModalShortcutsInternal extends React.Component<ModalShortcutsInternalProps
key: <><kbd>Shift</kbd> + <kbd>Down</kbd></>, key: <><kbd>Shift</kbd> + <kbd>Down</kbd></>,
text: t("Decrease the pitch by 10 degrees.") text: t("Decrease the pitch by 10 degrees.")
}, },
] ];
return <Modal return <Modal
data-wd-key="modal:shortcuts" data-wd-key="modal:shortcuts"
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle} onOpenToggle={this.props.onOpenToggle}
title={t('Shortcuts')} title={t("Shortcuts")}
> >
<section className="maputnik-modal-section maputnik-modal-shortcuts"> <section className="maputnik-modal-section maputnik-modal-shortcuts">
<p> <p>
@@ -118,7 +118,7 @@ class ModalShortcutsInternal extends React.Component<ModalShortcutsInternalProps
return <div key={idx} className="maputnik-modal-shortcuts__shortcut"> return <div key={idx} className="maputnik-modal-shortcuts__shortcut">
<dt key={"dt"+idx}>{item.key}</dt> <dt key={"dt"+idx}>{item.key}</dt>
<dd key={"dd"+idx}>{item.text}</dd> <dd key={"dd"+idx}>{item.text}</dd>
</div> </div>;
})} })}
</dl> </dl>
<p>{t("If the Map is in focused you can use the following shortcuts")}</p> <p>{t("If the Map is in focused you can use the following shortcuts")}</p>
@@ -126,11 +126,11 @@ class ModalShortcutsInternal extends React.Component<ModalShortcutsInternalProps
{mapShortcuts.map((item, idx) => { {mapShortcuts.map((item, idx) => {
return <li key={idx}> return <li key={idx}>
<span>{item.key}</span> {item.text} <span>{item.key}</span> {item.text}
</li> </li>;
})} })}
</ul> </ul>
</section> </section>
</Modal> </Modal>;
} }
} }
@@ -1,19 +1,19 @@
import React from 'react' import React from "react";
import {MdAddCircleOutline, MdDelete} from 'react-icons/md' import {MdAddCircleOutline, MdDelete} from "react-icons/md";
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json' import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import type {GeoJSONSourceSpecification, RasterDEMSourceSpecification, RasterSourceSpecification, SourceSpecification, VectorSourceSpecification} from 'maplibre-gl' import type {GeoJSONSourceSpecification, RasterDEMSourceSpecification, RasterSourceSpecification, SourceSpecification, VectorSourceSpecification} from "maplibre-gl";
import { WithTranslation, withTranslation } from 'react-i18next'; import { type WithTranslation, withTranslation } from "react-i18next";
import Modal from './Modal' import Modal from "./Modal";
import InputButton from './InputButton' import InputButton from "../InputButton";
import FieldString from './FieldString' import FieldString from "../FieldString";
import FieldSelect from './FieldSelect' import FieldSelect from "../FieldSelect";
import ModalSourcesTypeEditor, { EditorMode } from './ModalSourcesTypeEditor' import ModalSourcesTypeEditor, { type EditorMode } from "./ModalSourcesTypeEditor";
import style from '../libs/style' import style from "../../libs/style";
import { deleteSource, addSource, changeSource } from '../libs/source' import { deleteSource, addSource, changeSource } from "../../libs/source";
import publicSources from '../config/tilesets.json' import publicSources from "../../config/tilesets.json";
import { OnStyleChangedCallback, StyleSpecificationWithId } from '../libs/definitions'; import { type OnStyleChangedCallback, type StyleSpecificationWithId } from "../../libs/definitions";
type PublicSourceProps = { type PublicSourceProps = {
@@ -37,39 +37,39 @@ class PublicSource extends React.Component<PublicSourceProps> {
<span className="maputnik-space" /> <span className="maputnik-space" />
<MdAddCircleOutline /> <MdAddCircleOutline />
</InputButton> </InputButton>
</div> </div>;
} }
} }
function editorMode(source: SourceSpecification) { function editorMode(source: SourceSpecification) {
if(source.type === 'raster') { if(source.type === "raster") {
if(source.tiles) return 'tile_raster' if(source.tiles) return "tile_raster";
return 'tilejson_raster' return "tilejson_raster";
} }
if(source.type === 'raster-dem') { if(source.type === "raster-dem") {
if(source.tiles) return 'tilexyz_raster-dem' if(source.tiles) return "tilexyz_raster-dem";
return 'tilejson_raster-dem' return "tilejson_raster-dem";
} }
if(source.type === 'vector') { if(source.type === "vector") {
if(source.tiles) return 'tile_vector' if(source.tiles) return "tile_vector";
if(source.url && source.url.startsWith("pmtiles://")) return 'pmtiles_vector' if(source.url && source.url.startsWith("pmtiles://")) return "pmtiles_vector";
return 'tilejson_vector' return "tilejson_vector";
} }
if(source.type === 'geojson') { if(source.type === "geojson") {
if (typeof(source.data) === "string") { if (typeof(source.data) === "string") {
return 'geojson_url'; return "geojson_url";
} }
else { else {
return 'geojson_json'; return "geojson_json";
} }
} }
if(source.type === 'image') { if(source.type === "image") {
return 'image'; return "image";
} }
if(source.type === 'video') { if(source.type === "video") {
return 'video'; return "video";
} }
return null return null;
} }
type ActiveModalSourcesTypeEditorProps = { type ActiveModalSourcesTypeEditorProps = {
@@ -90,7 +90,7 @@ class ActiveModalSourcesTypeEditor extends React.Component<ActiveModalSourcesTyp
aria-label={t("Remove '{{sourceId}}' source", {sourceId: this.props.sourceId})} aria-label={t("Remove '{{sourceId}}' source", {sourceId: this.props.sourceId})}
className="maputnik-active-source-type-editor-header-delete" className="maputnik-active-source-type-editor-header-delete"
onClick={()=> this.props.onDelete(this.props.sourceId)} onClick={()=> this.props.onDelete(this.props.sourceId)}
style={{backgroundColor: 'transparent'}} style={{backgroundColor: "transparent"}}
> >
<MdDelete /> <MdDelete />
</InputButton> </InputButton>
@@ -102,7 +102,7 @@ class ActiveModalSourcesTypeEditor extends React.Component<ActiveModalSourcesTyp
source={this.props.source} source={this.props.source}
/> />
</div> </div>
</div> </div>;
} }
} }
@@ -118,100 +118,100 @@ type AddSourceState = {
class AddSource extends React.Component<AddSourceProps, AddSourceState> { class AddSource extends React.Component<AddSourceProps, AddSourceState> {
constructor(props: AddSourceProps) { constructor(props: AddSourceProps) {
super(props) super(props);
this.state = { this.state = {
mode: 'tilejson_vector', mode: "tilejson_vector",
sourceId: style.generateId(), sourceId: style.generateId(),
source: this.defaultSource('tilejson_vector'), source: this.defaultSource("tilejson_vector"),
} };
} }
defaultSource(mode: EditorMode): SourceSpecification { defaultSource(mode: EditorMode): SourceSpecification {
const source = (this.state || {}).source || {} const source = (this.state || {}).source || {};
const {protocol} = window.location; const {protocol} = window.location;
switch(mode) { switch(mode) {
case 'pmtiles_vector': return { case "pmtiles_vector": return {
type: 'vector', type: "vector",
url: `${protocol}//localhost:3000/file.pmtiles` url: `${protocol}//localhost:3000/file.pmtiles`
} };
case 'geojson_url': return { case "geojson_url": return {
type: 'geojson', type: "geojson",
data: `${protocol}//localhost:3000/geojson.json` data: `${protocol}//localhost:3000/geojson.json`
} };
case 'geojson_json': return { case "geojson_json": return {
type: 'geojson', type: "geojson",
cluster: (source as GeoJSONSourceSpecification).cluster || false, cluster: (source as GeoJSONSourceSpecification).cluster || false,
data: '' data: ""
} };
case 'tilejson_vector': return { case "tilejson_vector": return {
type: 'vector', type: "vector",
url: (source as VectorSourceSpecification).url || `${protocol}//localhost:3000/tilejson.json` url: (source as VectorSourceSpecification).url || `${protocol}//localhost:3000/tilejson.json`
} };
case 'tile_vector': return { case "tile_vector": return {
type: 'vector', type: "vector",
tiles: (source as VectorSourceSpecification).tiles || [`${protocol}//localhost:3000/{x}/{y}/{z}.pbf`], tiles: (source as VectorSourceSpecification).tiles || [`${protocol}//localhost:3000/{x}/{y}/{z}.pbf`],
minzoom: (source as VectorSourceSpecification).minzoom || 0, minzoom: (source as VectorSourceSpecification).minzoom || 0,
maxzoom: (source as VectorSourceSpecification).maxzoom || 14, maxzoom: (source as VectorSourceSpecification).maxzoom || 14,
scheme: (source as VectorSourceSpecification).scheme || 'xyz' scheme: (source as VectorSourceSpecification).scheme || "xyz"
} };
case 'tilejson_raster': return { case "tilejson_raster": return {
type: 'raster', type: "raster",
url: (source as RasterSourceSpecification).url || `${protocol}//localhost:3000/tilejson.json` url: (source as RasterSourceSpecification).url || `${protocol}//localhost:3000/tilejson.json`
} };
case 'tile_raster': return { case "tile_raster": return {
type: 'raster', type: "raster",
tiles: (source as RasterSourceSpecification).tiles || [`${protocol}//localhost:3000/{x}/{y}/{z}.png`], tiles: (source as RasterSourceSpecification).tiles || [`${protocol}//localhost:3000/{x}/{y}/{z}.png`],
minzoom: (source as RasterSourceSpecification).minzoom || 0, minzoom: (source as RasterSourceSpecification).minzoom || 0,
maxzoom: (source as RasterSourceSpecification).maxzoom || 14, maxzoom: (source as RasterSourceSpecification).maxzoom || 14,
scheme: (source as RasterSourceSpecification).scheme || 'xyz', scheme: (source as RasterSourceSpecification).scheme || "xyz",
tileSize: (source as RasterSourceSpecification).tileSize || 512, tileSize: (source as RasterSourceSpecification).tileSize || 512,
} };
case 'tilejson_raster-dem': return { case "tilejson_raster-dem": return {
type: 'raster-dem', type: "raster-dem",
url: (source as RasterDEMSourceSpecification).url || `${protocol}//localhost:3000/tilejson.json` url: (source as RasterDEMSourceSpecification).url || `${protocol}//localhost:3000/tilejson.json`
} };
case 'tilexyz_raster-dem': return { case "tilexyz_raster-dem": return {
type: 'raster-dem', type: "raster-dem",
tiles: (source as RasterDEMSourceSpecification).tiles || [`${protocol}//localhost:3000/{x}/{y}/{z}.png`], tiles: (source as RasterDEMSourceSpecification).tiles || [`${protocol}//localhost:3000/{x}/{y}/{z}.png`],
minzoom: (source as RasterDEMSourceSpecification).minzoom || 0, minzoom: (source as RasterDEMSourceSpecification).minzoom || 0,
maxzoom: (source as RasterDEMSourceSpecification).maxzoom || 14, maxzoom: (source as RasterDEMSourceSpecification).maxzoom || 14,
tileSize: (source as RasterDEMSourceSpecification).tileSize || 512 tileSize: (source as RasterDEMSourceSpecification).tileSize || 512
} };
case 'image': return { case "image": return {
type: 'image', type: "image",
url: `${protocol}//localhost:3000/image.png`, url: `${protocol}//localhost:3000/image.png`,
coordinates: [ coordinates: [
[0,0], [0,0],
[0,0], [0,0],
[0,0], [0,0],
[0,0], [0,0],
], ],
} };
case 'video': return { case "video": return {
type: 'video', type: "video",
urls: [ urls: [
`${protocol}//localhost:3000/movie.mp4` `${protocol}//localhost:3000/movie.mp4`
], ],
coordinates: [ coordinates: [
[0,0], [0,0],
[0,0], [0,0],
[0,0], [0,0],
[0,0], [0,0],
], ],
} };
default: return {} as any default: return {} as any;
} }
} }
onAdd = () => { onAdd = () => {
const {source, sourceId} = this.state; const {source, sourceId} = this.state;
this.props.onAdd(sourceId, source); this.props.onAdd(sourceId, source);
} };
onChangeSource = (source: SourceSpecification) => { onChangeSource = (source: SourceSpecification) => {
this.setState({source}); this.setState({source});
} };
render() { render() {
const t = this.props.t; const t = this.props.t;
@@ -238,17 +238,17 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
label={t("Source Type")} label={t("Source Type")}
fieldSpec={sourceTypeFieldSpec} fieldSpec={sourceTypeFieldSpec}
options={[ options={[
['geojson_json', t('GeoJSON (JSON)')], ["geojson_json", t("GeoJSON (JSON)")],
['geojson_url', t('GeoJSON (URL)')], ["geojson_url", t("GeoJSON (URL)")],
['tilejson_vector', t('Vector (TileJSON URL)')], ["tilejson_vector", t("Vector (TileJSON URL)")],
['tile_vector', t('Vector (Tile URLs)')], ["tile_vector", t("Vector (Tile URLs)")],
['tilejson_raster', t('Raster (TileJSON URL)')], ["tilejson_raster", t("Raster (TileJSON URL)")],
['tile_raster', t('Raster (Tile URLs)')], ["tile_raster", t("Raster (Tile URLs)")],
['tilejson_raster-dem', t('Raster DEM (TileJSON URL)')], ["tilejson_raster-dem", t("Raster DEM (TileJSON URL)")],
['tilexyz_raster-dem', t('Raster DEM (XYZ URLs)')], ["tilexyz_raster-dem", t("Raster DEM (XYZ URLs)")],
['pmtiles_vector', t('Vector (PMTiles)')], ["pmtiles_vector", t("Vector (PMTiles)")],
['image', t('Image')], ["image", t("Image")],
['video', t('Video')], ["video", t("Video")],
]} ]}
onChange={mode => this.setState({mode: mode as EditorMode, source: this.defaultSource(mode as EditorMode)})} onChange={mode => this.setState({mode: mode as EditorMode, source: this.defaultSource(mode as EditorMode)})}
value={this.state.mode as string} value={this.state.mode as string}
@@ -266,29 +266,29 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
> >
{t("Add Source")} {t("Add Source")}
</InputButton> </InputButton>
</div> </div>;
} }
} }
type ModalSourcesInternalProps = { type ModalSourcesInternalProps = {
mapStyle: StyleSpecificationWithId mapStyle: StyleSpecificationWithId
isOpen: boolean isOpen: boolean
onOpenToggle(...args: unknown[]): unknown onOpenToggle(): void
onStyleChanged: OnStyleChangedCallback onStyleChanged: OnStyleChangedCallback
} & WithTranslation; } & WithTranslation;
class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> { class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {
stripTitle(source: SourceSpecification & {title?: string}): SourceSpecification { stripTitle(source: SourceSpecification & {title?: string}): SourceSpecification {
const strippedSource = {...source} const strippedSource = {...source};
delete strippedSource['title'] delete strippedSource["title"];
return strippedSource return strippedSource;
} }
render() { render() {
const {t, mapStyle} = this.props; const {t, mapStyle} = this.props;
const i18nProps = {t, i18n: this.props.i18n, tReady: this.props.tReady}; const i18nProps = {t, i18n: this.props.i18n, tReady: this.props.tReady};
const activeSources = Object.keys(mapStyle.sources).map(sourceId => { const activeSources = Object.keys(mapStyle.sources).map(sourceId => {
const source = mapStyle.sources[sourceId] const source = mapStyle.sources[sourceId];
return <ActiveModalSourcesTypeEditor return <ActiveModalSourcesTypeEditor
key={sourceId} key={sourceId}
sourceId={sourceId} sourceId={sourceId}
@@ -296,8 +296,8 @@ class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {
onChange={(src: SourceSpecification) => this.props.onStyleChanged(changeSource(mapStyle, sourceId, src))} onChange={(src: SourceSpecification) => this.props.onStyleChanged(changeSource(mapStyle, sourceId, src))}
onDelete={() => this.props.onStyleChanged(deleteSource(mapStyle, sourceId))} onDelete={() => this.props.onStyleChanged(deleteSource(mapStyle, sourceId))}
{...i18nProps} {...i18nProps}
/> />;
}) });
const tilesetOptions = Object.keys(publicSources).filter((sourceId: string) => !(sourceId in mapStyle.sources)).map((sourceId: string) => { const tilesetOptions = Object.keys(publicSources).filter((sourceId: string) => !(sourceId in mapStyle.sources)).map((sourceId: string) => {
const source = publicSources[sourceId as keyof typeof publicSources] as SourceSpecification & {title: string}; const source = publicSources[sourceId as keyof typeof publicSources] as SourceSpecification & {title: string};
@@ -307,14 +307,14 @@ class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {
type={source.type} type={source.type}
title={source.title} title={source.title}
onSelect={() => this.props.onStyleChanged(addSource(mapStyle, sourceId, this.stripTitle(source)))} onSelect={() => this.props.onStyleChanged(addSource(mapStyle, sourceId, this.stripTitle(source)))}
/> />;
}) });
return <Modal return <Modal
data-wd-key="modal:sources" data-wd-key="modal:sources"
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle} onOpenToggle={this.props.onOpenToggle}
title={t('Sources')} title={t("Sources")}
> >
<section className="maputnik-modal-section"> <section className="maputnik-modal-section">
<h1>{t("Active Sources")}</h1> <h1>{t("Active Sources")}</h1>
@@ -339,7 +339,7 @@ class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {
{...i18nProps} {...i18nProps}
/> />
</section> </section>
</Modal> </Modal>;
} }
} }

Some files were not shown because too many files have changed in this diff Show More