Compare commits

..

22 Commits

Author SHA1 Message Date
dependabot[bot] 5dac638f7e chore(deps-dev): Bump vite from 7.3.2 to 8.1.5
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.3.2 to 8.1.5.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.1.5/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.1.5
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 13:54:15 +00:00
dependabot[bot] 2b5f03f3c2 chore(deps): Bump github/codeql-action/analyze from 4.36.3 to 4.37.2 (#2029)
Bumps
[github/codeql-action/analyze](https://github.com/github/codeql-action)
from 4.36.3 to 4.37.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/github/codeql-action/releases">github/codeql-action/analyze's
releases</a>.</em></p>
<blockquote>
<h2>v4.37.2</h2>
<ul>
<li>The new address format for the <code>config-file</code> input that
was introduced in CodeQL Action 4.37.0 is now enabled by default. In
addition to the format described there, the <code>remote=</code> prefix
can now be used to explicitly indicate that the input refers to a remote
file. All previous input formats continue to be accepted as well. <a
href="https://redirect.github.com/github/codeql-action/pull/4023">#4023</a></li>
<li>The CodeQL Action can now make use of <a
href="https://docs.github.com/en/code-security/how-tos/secure-at-scale/configure-organization-security/manage-usage-and-access/giving-org-access-private-registries">configured
private registries</a> in Default Setup to retrieve CodeQL configuration
files from remote repositories that require authentication. This will
allow customers to store their CodeQL configuration in a single
repository that can then be referenced by Default Setup workflows in
other repositories. We expect to roll this and other, related changes
out to everyone in July. <a
href="https://redirect.github.com/github/codeql-action/pull/4007">#4007</a></li>
</ul>
<h2>v4.37.1</h2>
<ul>
<li><em>Upcoming breaking change</em>: Add a deprecation warning for
customers using CodeQL version 2.20.6 and earlier. These versions of
CodeQL were discontinued on 1 July 2026 alongside GitHub Enterprise
Server 3.16, and will be unsupported by the next minor release of the
CodeQL Action. <a
href="https://redirect.github.com/github/codeql-action/pull/3956">#3956</a></li>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.1">2.26.1</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/4019">#4019</a></li>
</ul>
<h2>v4.37.0</h2>
<ul>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.0">2.26.0</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/3995">#3995</a></li>
<li>In addition to the existing input format, the
<code>config-file</code> input for the <code>codeql-action/init</code>
step will soon support a new <code>[owner/]repo[@ref][:path]</code>
format. All components except the repository name are optional. If
omitted, <code>owner</code> defaults to the same owner as the repository
the analysis is running for, <code>ref</code> to <code>main</code>, and
<code>path</code> to <code>.github/codeql-action.yaml</code>. Support
for this format ships in this version of the CodeQL Action, but will
only be enabled over the coming weeks. <a
href="https://redirect.github.com/github/codeql-action/pull/3973">#3973</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/github/codeql-action/blob/main/CHANGELOG.md">github/codeql-action/analyze's
changelog</a>.</em></p>
<blockquote>
<h1>CodeQL Action Changelog</h1>
<p>See the <a
href="https://github.com/github/codeql-action/releases">releases
page</a> for the relevant changes to the CodeQL CLI and language
packs.</p>
<h2>[UNRELEASED]</h2>
<p>No user facing changes.</p>
<h2>4.37.3 - 22 Jul 2026</h2>
<p>No user facing changes.</p>
<h2>4.37.2 - 21 Jul 2026</h2>
<ul>
<li>The new address format for the <code>config-file</code> input that
was introduced in CodeQL Action 4.37.0 is now enabled by default. In
addition to the format described there, the <code>remote=</code> prefix
can now be used to explicitly indicate that the input refers to a remote
file. All previous input formats continue to be accepted as well. <a
href="https://redirect.github.com/github/codeql-action/pull/4023">#4023</a></li>
<li>The CodeQL Action can now make use of <a
href="https://docs.github.com/en/code-security/how-tos/secure-at-scale/configure-organization-security/manage-usage-and-access/giving-org-access-private-registries">configured
private registries</a> in Default Setup to retrieve CodeQL configuration
files from remote repositories that require authentication. This will
allow customers to store their CodeQL configuration in a single
repository that can then be referenced by Default Setup workflows in
other repositories. We expect to roll this and other, related changes
out to everyone in July. <a
href="https://redirect.github.com/github/codeql-action/pull/4007">#4007</a></li>
</ul>
<h2>4.37.1 - 16 Jul 2026</h2>
<ul>
<li><em>Upcoming breaking change</em>: Add a deprecation warning for
customers using CodeQL version 2.20.6 and earlier. These versions of
CodeQL were discontinued on 1 July 2026 alongside GitHub Enterprise
Server 3.16, and will be unsupported by the next minor release of the
CodeQL Action. <a
href="https://redirect.github.com/github/codeql-action/pull/3956">#3956</a></li>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.1">2.26.1</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/4019">#4019</a></li>
</ul>
<h2>4.37.0 - 08 Jul 2026</h2>
<ul>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.0">2.26.0</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/3995">#3995</a></li>
<li>In addition to the existing input format, the
<code>config-file</code> input for the <code>codeql-action/init</code>
step will soon support a new <code>[owner/]repo[@ref][:path]</code>
format. All components except the repository name are optional. If
omitted, <code>owner</code> defaults to the same owner as the repository
the analysis is running for, <code>ref</code> to <code>main</code>, and
<code>path</code> to <code>.github/codeql-action.yaml</code>. Support
for this format ships in this version of the CodeQL Action, but will
only be enabled over the coming weeks. <a
href="https://redirect.github.com/github/codeql-action/pull/3973">#3973</a></li>
</ul>
<h2>4.36.3 - 01 Jul 2026</h2>
<p>No user facing changes.</p>
<h2>4.36.2 - 04 Jun 2026</h2>
<ul>
<li>Cache CodeQL CLI version information across Actions steps. <a
href="https://redirect.github.com/github/codeql-action/pull/3943">#3943</a></li>
<li>Reduce requests while waiting for analysis processing by using
exponential backoff when polling SARIF processing status. <a
href="https://redirect.github.com/github/codeql-action/pull/3937">#3937</a></li>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.25.6">2.25.6</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/3948">#3948</a></li>
</ul>
<h2>4.36.1 - 02 Jun 2026</h2>
<p>No user facing changes.</p>
<h2>4.36.0 - 22 May 2026</h2>
<ul>
<li><em>Breaking change</em>: Bump the minimum required CodeQL bundle
version to 2.19.4. <a
href="https://redirect.github.com/github/codeql-action/pull/3894">#3894</a></li>
<li>Add support for SHA-256 Git object IDs. <a
href="https://redirect.github.com/github/codeql-action/pull/3893">#3893</a></li>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.25.5">2.25.5</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/3926">#3926</a></li>
</ul>
<h2>4.35.5 - 15 May 2026</h2>
<ul>
<li>We have improved how the JavaScript bundles for the CodeQL Action
are generated to avoid duplication across bundles and reduce the size of
the repository by around 70%. This should have no effect on the runtime
behaviour of the CodeQL Action. <a
href="https://redirect.github.com/github/codeql-action/pull/3899">#3899</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/github/codeql-action/commit/e0647621c2984b5ed2f768cb892365bf2a616ad1"><code>e064762</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/4027">#4027</a>
from github/update-v4.37.2-385bcdc5a</li>
<li><a
href="https://github.com/github/codeql-action/commit/e0faed839190caa67a5cd42f1cc16246028ca3df"><code>e0faed8</code></a>
Add a couple of change notes</li>
<li><a
href="https://github.com/github/codeql-action/commit/73aad0eaa9df172668665a150d17b8bc5a650c20"><code>73aad0e</code></a>
Update changelog for v4.37.2</li>
<li><a
href="https://github.com/github/codeql-action/commit/385bcdc5afb0b5a06bd1dae7809fda5453c7664b"><code>385bcdc</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/4026">#4026</a>
from github/dependabot/npm_and_yarn/tar-7.5.20</li>
<li><a
href="https://github.com/github/codeql-action/commit/de0229cea17fd9b594946f05dccc18e9c33a15d1"><code>de0229c</code></a>
Merge branch 'main' into dependabot/npm_and_yarn/tar-7.5.20</li>
<li><a
href="https://github.com/github/codeql-action/commit/115e8cbbe9419ab1ea002e5c0c6d4797d7820bf8"><code>115e8cb</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/4010">#4010</a>
from github/mbg/ts/update-release-branch</li>
<li><a
href="https://github.com/github/codeql-action/commit/dbdf0b0c7d63778e4b807cb4cef00f090e7dcd4b"><code>dbdf0b0</code></a>
Bump tar from 7.5.16 to 7.5.20</li>
<li><a
href="https://github.com/github/codeql-action/commit/830c23121c8d6124ad817115f9d68da98f44eba0"><code>830c231</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/4023">#4023</a>
from github/mbg/ff/remove-new-remote-file-addresses-ff</li>
<li><a
href="https://github.com/github/codeql-action/commit/69fd9e97ba2f4e95024e587c495eec0f1c9186ea"><code>69fd9e9</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/4024">#4024</a>
from github/mbg/private-registry/make-docker-available</li>
<li><a
href="https://github.com/github/codeql-action/commit/b85568788a5dbb01732f9d4e0d0882e5429bb804"><code>b855687</code></a>
Always make <code>docker_registry</code> registries available</li>
<li>Additional commits viewable in <a
href="https://github.com/github/codeql-action/compare/54f647b7e1bb85c95cddabcd46b0c578ec92bc1a...e0647621c2984b5ed2f768cb892365bf2a616ad1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github/codeql-action/analyze&package-manager=github_actions&previous-version=4.36.3&new-version=4.37.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 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>
2026-07-24 13:52:38 +00:00
dependabot[bot] ccfb9a3111 chore(deps-dev): Bump typescript-eslint from 8.64.0 to 8.65.0 (#2028)
Bumps
[typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint)
from 8.64.0 to 8.65.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/releases">typescript-eslint's
releases</a>.</em></p>
<blockquote>
<h2>v8.65.0</h2>
<h2>8.65.0 (2026-07-20)</h2>
<h3>🚀 Features</h3>
<ul>
<li>add warning when TS 7 is detected (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12529">#12529</a>)</li>
<li><strong>eslint-plugin:</strong> [no-restricted-imports] deprecate
extension rule (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12527">#12527</a>,
<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/issues/19562">#19562</a>,
<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/issues/11889">#11889</a>)</li>
<li><strong>eslint-plugin:</strong> [no-shadow] specialized error on
enum declaration and member shadowing (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12578">#12578</a>)</li>
<li><strong>parser:</strong> add onUnsupportedTypeScriptVersion option
to error on unsupported TypeScript versions (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12465">#12465</a>)</li>
<li><strong>typescript-estree:</strong> throw for invalid import defer
syntax (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12552">#12552</a>)</li>
</ul>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>eslint-plugin:</strong> [prefer-string-starts-ends-with]
handle escaped $ ending regex literals (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12515">#12515</a>)</li>
<li><strong>eslint-plugin:</strong> [unbound-method] report unbound
methods accessed via member expression on union types (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12448">#12448</a>)</li>
<li><strong>eslint-plugin:</strong>
[no-unnecessary-parameter-property-assignment] don't flag computed
assignments with a variable key (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12568">#12568</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Evyatar Daud <a
href="https://github.com/StyleShit"><code>@​StyleShit</code></a></li>
<li>Josh Goldberg </li>
<li>Kirk Waiblinger <a
href="https://github.com/kirkwaiblinger"><code>@​kirkwaiblinger</code></a></li>
<li>Serhii Leniv <a
href="https://github.com/Serhii-Leniv"><code>@​Serhii-Leniv</code></a></li>
<li>송재욱</li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.65.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md">typescript-eslint's
changelog</a>.</em></p>
<blockquote>
<h2>8.65.0 (2026-07-20)</h2>
<h3>🚀 Features</h3>
<ul>
<li>add warning when TS 7 is detected (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12529">#12529</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Josh Goldberg </li>
<li>Kirk Waiblinger <a
href="https://github.com/kirkwaiblinger"><code>@​kirkwaiblinger</code></a></li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.65.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/typescript-eslint/typescript-eslint/commit/63ba81b6acfa0d663c29aa0013d4672bf3b0426c"><code>63ba81b</code></a>
chore(release): publish 8.65.0</li>
<li><a
href="https://github.com/typescript-eslint/typescript-eslint/commit/eaf457631ac381aadfee747c2d098c8ac4df9d63"><code>eaf4576</code></a>
feat: add warning when TS 7 is detected (<a
href="https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint/issues/12529">#12529</a>)</li>
<li><a
href="https://github.com/typescript-eslint/typescript-eslint/commit/0d06406f3181f063041dbd1621d71984127ddae1"><code>0d06406</code></a>
chore: add attw validation to repo (<a
href="https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint/issues/12437">#12437</a>)</li>
<li><a
href="https://github.com/typescript-eslint/typescript-eslint/commit/c2386e49ec6347aa92d53a1f32143ca45984cc13"><code>c2386e4</code></a>
chore(deps): update dependency prettier to v3.9.5 (<a
href="https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint/issues/12486">#12486</a>)</li>
<li>See full diff in <a
href="https://github.com/typescript-eslint/typescript-eslint/commits/v8.65.0/packages/typescript-eslint">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=typescript-eslint&package-manager=npm_and_yarn&previous-version=8.64.0&new-version=8.65.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 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>
2026-07-24 13:52:21 +00:00
dependabot[bot] 23ce685a91 chore(deps-dev): Bump postcss from 8.5.20 to 8.5.21 (#2027)
[//]: # (dependabot-start)
⚠️  **Dependabot is rebasing this PR** ⚠️ 

Rebasing might not happen immediately, so don't worry if this takes some
time.

Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.

---

[//]: # (dependabot-end)

Bumps [postcss](https://github.com/postcss/postcss) from 8.5.20 to
8.5.21.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/postcss/postcss/releases">postcss's
releases</a>.</em></p>
<blockquote>
<h2>8.5.21</h2>
<ul>
<li>Fixed childless at-rule losing semicolon before comment (by <a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
<li>Fixed docs (by <a
href="https://github.com/isker"><code>@​isker</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/postcss/postcss/blob/main/CHANGELOG.md">postcss's
changelog</a>.</em></p>
<blockquote>
<h2>8.5.21</h2>
<ul>
<li>Fixed childless at-rule losing semicolon before comment (by <a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
<li>Fixed docs (by <a
href="https://github.com/isker"><code>@​isker</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/postcss/postcss/commit/28e0daf8f2fe5ba9e19ea3f8c27c8fe176f9419e"><code>28e0daf</code></a>
Release 8.5.21 version</li>
<li><a
href="https://github.com/postcss/postcss/commit/3d2b4e43e38274f233b5609d09687cadad8215d9"><code>3d2b4e4</code></a>
Update dependencies</li>
<li><a
href="https://github.com/postcss/postcss/commit/d197327e82e1a7d6dd9272effa3e4bfa91fdd20e"><code>d197327</code></a>
Fix childless at-rule losing its semicolon before a comment (<a
href="https://redirect.github.com/postcss/postcss/issues/2115">#2115</a>)</li>
<li><a
href="https://github.com/postcss/postcss/commit/0e360b749aa17a5a89baccf9c8e5db3c2978c21e"><code>0e360b7</code></a>
Fix mismatched JSDoc comments on Position (<a
href="https://redirect.github.com/postcss/postcss/issues/2114">#2114</a>)</li>
<li>See full diff in <a
href="https://github.com/postcss/postcss/compare/8.5.20...8.5.21">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=postcss&package-manager=npm_and_yarn&previous-version=8.5.20&new-version=8.5.21)](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 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>
2026-07-24 13:52:06 +00:00
dependabot[bot] ea67387e65 chore(deps): Bump @maplibre/maplibre-gl-style-spec from 26.1.0 to 26.2.1 (#2026)
Bumps
[@maplibre/maplibre-gl-style-spec](https://github.com/maplibre/maplibre-style-spec)
from 26.1.0 to 26.2.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/maplibre/maplibre-style-spec/releases">@​maplibre/maplibre-gl-style-spec's
releases</a>.</em></p>
<blockquote>
<h2>v26.2.1</h2>
<h3> Features and improvements</h3>
<h3>🐞 Bug fixes</h3>
<ul>
<li>Fix <code>fill-extrusion-rounded-corner-distance</code> being
documented as an <code>paint</code> instead of an <code>layout</code>
property as implemnented (<a
href="https://redirect.github.com/maplibre/maplibre-style-spec/pull/1784">#1784</a>)
(by <a
href="https://github.com/CommanderStorm"><code>@​CommanderStorm</code></a>)</li>
</ul>
<h2>v26.2.0</h2>
<h3> Features and improvements</h3>
<ul>
<li>Add fill-extrusion-rounded-corner-distance (<a
href="https://redirect.github.com/maplibre/maplibre-style-spec/pull/1781">#1781</a>,
<a
href="https://redirect.github.com/maplibre/maplibre-style-spec/pull/1782">#1782</a>)
(by <a
href="https://github.com/louwers"><code>@​louwers</code></a>)</li>
</ul>
<h3>🐞 Bug fixes</h3>
<ul>
<li>Support <code>global-state</code> (and other
<code>value</code>-typed) expressions in
<code>projectionDefinition</code> properties such as
<code>projection.type</code> (<a
href="https://redirect.github.com/maplibre/maplibre-style-spec/pull/1772">#1772</a>)
(by <a
href="https://github.com/CommanderStorm"><code>@​CommanderStorm</code></a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/maplibre/maplibre-style-spec/blob/main/CHANGELOG.md">@​maplibre/maplibre-gl-style-spec's
changelog</a>.</em></p>
<blockquote>
<h2>26.2.1</h2>
<h3> Features and improvements</h3>
<h3>🐞 Bug fixes</h3>
<ul>
<li>Fix <code>fill-extrusion-rounded-corner-distance</code> being
documented as an <code>paint</code> instead of an <code>layout</code>
property as implemnented (<a
href="https://redirect.github.com/maplibre/maplibre-style-spec/pull/1784">#1784</a>)
(by <a
href="https://github.com/CommanderStorm"><code>@​CommanderStorm</code></a>)</li>
</ul>
<h2>26.2.0</h2>
<h3> Features and improvements</h3>
<ul>
<li>Add fill-extrusion-rounded-corner-distance (<a
href="https://redirect.github.com/maplibre/maplibre-style-spec/pull/1781">#1781</a>,
<a
href="https://redirect.github.com/maplibre/maplibre-style-spec/pull/1782">#1782</a>)
(by <a
href="https://github.com/louwers"><code>@​louwers</code></a>)</li>
</ul>
<h3>🐞 Bug fixes</h3>
<ul>
<li>Support <code>global-state</code> (and other
<code>value</code>-typed) expressions in
<code>projectionDefinition</code> properties such as
<code>projection.type</code> (<a
href="https://redirect.github.com/maplibre/maplibre-style-spec/pull/1772">#1772</a>)
(by <a
href="https://github.com/CommanderStorm"><code>@​CommanderStorm</code></a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/maplibre/maplibre-style-spec/commit/7a2420b0a076a1a013149720a511d63fc9502340"><code>7a2420b</code></a>
Bump js version to 26.2.1 (<a
href="https://redirect.github.com/maplibre/maplibre-style-spec/issues/1785">#1785</a>)</li>
<li><a
href="https://github.com/maplibre/maplibre-style-spec/commit/688a88cd1903cd8039a48ce8efc63b3f46f4008b"><code>688a88c</code></a>
fix: move fill-extrusion-rounded-corner-distance to layout properties
(<a
href="https://redirect.github.com/maplibre/maplibre-style-spec/issues/1784">#1784</a>)</li>
<li><a
href="https://github.com/maplibre/maplibre-style-spec/commit/08a02a3b9a5d9cdda9cfda1de309b812f712cdf8"><code>08a02a3</code></a>
docs: document global state support for paint and layout (<a
href="https://redirect.github.com/maplibre/maplibre-style-spec/issues/1770">#1770</a>)</li>
<li><a
href="https://github.com/maplibre/maplibre-style-spec/commit/dda297b6e3578068209e996ab716afc144759735"><code>dda297b</code></a>
Bump js version to 26.2.0 (<a
href="https://redirect.github.com/maplibre/maplibre-style-spec/issues/1783">#1783</a>)</li>
<li><a
href="https://github.com/maplibre/maplibre-style-spec/commit/add55f47717c9dcc7e8442be98c08f0cc356d243"><code>add55f4</code></a>
docs: document sky.* and light.* global state and rendering support (<a
href="https://redirect.github.com/maplibre/maplibre-style-spec/issues/1771">#1771</a>)</li>
<li><a
href="https://github.com/maplibre/maplibre-style-spec/commit/dc8a92b68d0818ce0848a16bc2cf6006e38a80f6"><code>dc8a92b</code></a>
implement global-state and projection interop (<a
href="https://redirect.github.com/maplibre/maplibre-style-spec/issues/1772">#1772</a>)</li>
<li><a
href="https://github.com/maplibre/maplibre-style-spec/commit/faaa9fe3b4f543decacb4015a88c8ef125348490"><code>faaa9fe</code></a>
Fix Android/iOS version
<code>fill-extrusion-rounded-corner-distance</code> property (<a
href="https://redirect.github.com/maplibre/maplibre-style-spec/issues/1">#1</a>...</li>
<li><a
href="https://github.com/maplibre/maplibre-style-spec/commit/28fe948d47b325ae0c2688257840b9881f87fa8a"><code>28fe948</code></a>
build(deps-dev): bump oxfmt from 0.58.0 to 0.59.0 (<a
href="https://redirect.github.com/maplibre/maplibre-style-spec/issues/1777">#1777</a>)</li>
<li><a
href="https://github.com/maplibre/maplibre-style-spec/commit/f20786eb870a9520a68ab39629bc45cb19211e9b"><code>f20786e</code></a>
build(deps-dev): bump <code>@​typescript-eslint/parser</code> from
8.64.0 to 8.65.0 (<a
href="https://redirect.github.com/maplibre/maplibre-style-spec/issues/1779">#1779</a>)</li>
<li><a
href="https://github.com/maplibre/maplibre-style-spec/commit/d885670d68e21e4e4921b77984e45d02743a4624"><code>d885670</code></a>
build(deps-dev): bump rolldown from 1.1.5 to 1.2.0 (<a
href="https://redirect.github.com/maplibre/maplibre-style-spec/issues/1775">#1775</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/maplibre/maplibre-style-spec/compare/v26.1.0...v26.2.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@maplibre/maplibre-gl-style-spec&package-manager=npm_and_yarn&previous-version=26.1.0&new-version=26.2.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 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>
2026-07-24 13:51:47 +00:00
dependabot[bot] 30bcfc2265 chore(deps-dev): Bump sass from 1.101.0 to 1.101.3 (#2025)
Bumps [sass](https://github.com/sass/dart-sass) from 1.101.0 to 1.101.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/sass/dart-sass/releases">sass's
releases</a>.</em></p>
<blockquote>
<h2>Dart Sass 1.101.3</h2>
<p>To install Sass 1.101.3, download one of the packages below and <a
href="https://katiek2.github.io/path-doc/">add it to your PATH</a>, or
see <a href="https://sass-lang.com/install">the Sass website</a> for
full installation instructions.</p>
<h1>Changes</h1>
<ul>
<li>No user-visible changes.</li>
</ul>
<p>See the <a
href="https://github.com/sass/dart-sass/blob/master/CHANGELOG.md#11013">full
changelog</a> for changes in earlier releases.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/sass/dart-sass/blob/main/CHANGELOG.md">sass's
changelog</a>.</em></p>
<blockquote>
<h2>1.101.3</h2>
<ul>
<li>No user-visible changes.</li>
</ul>
<h2>1.101.2</h2>
<ul>
<li>Fix a bug where the deprecation warning for vendor-prefixed
<code>expression()</code>
functions would incorrectly indicate whether or not the function would
be
invalid Sass in Dart Sass 2.0.0.</li>
</ul>
<h2>1.101.1</h2>
<ul>
<li>Sass stack trace entries are now always either absolute URLs,
absolute paths,
or paths relative to the current working directory. Previously, if a
stylesheet was loaded using a relative URL (as from a load path), that
relative URL was listed even if it couldn't be resolved relative to the
current working directory. However, this created potential ambiguities,
so
this behavior has been removed.</li>
</ul>
<h3>Command Line Interface</h3>
<ul>
<li><code>--watch</code> mode now handles atomically-written files more
gracefully.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/sass/dart-sass/commit/e8c12331ea5304a1d641d6a6bd4cb526cb3800b9"><code>e8c1233</code></a>
Fix some GitHub actions issues introduced by the Zizmor refactor (<a
href="https://redirect.github.com/sass/dart-sass/issues/2798">#2798</a>)</li>
<li><a
href="https://github.com/sass/dart-sass/commit/d676118ace4b6b196383f26cf5ffda4f9c61b239"><code>d676118</code></a>
Merge pull request <a
href="https://redirect.github.com/sass/dart-sass/issues/2797">#2797</a>
from sass/lints</li>
<li><a
href="https://github.com/sass/dart-sass/commit/548e6604c5eed5137b148a839513c7444582eb3c"><code>548e660</code></a>
Prefer interpolation to compose strings</li>
<li><a
href="https://github.com/sass/dart-sass/commit/b443ffe759f7235170b1fd6e540dcb8e459f3dca"><code>b443ffe</code></a>
Use function type syntax</li>
<li><a
href="https://github.com/sass/dart-sass/commit/3d28f6a83ff8c16e7b1980f6a4e46d62f1505fce"><code>3d28f6a</code></a>
Add <code>@override</code></li>
<li><a
href="https://github.com/sass/dart-sass/commit/e182ba21e1459c5efad0247e9bc5ba212a870db3"><code>e182ba2</code></a>
Expand dependency constraints (<a
href="https://redirect.github.com/sass/dart-sass/issues/2796">#2796</a>)</li>
<li><a
href="https://github.com/sass/dart-sass/commit/66f9d41f79231eebb4380a126bbc662c13f2d7d0"><code>66f9d41</code></a>
Bump analyzer from 10.2.0 to 13.3.0 (<a
href="https://redirect.github.com/sass/dart-sass/issues/2795">#2795</a>)</li>
<li><a
href="https://github.com/sass/dart-sass/commit/ab4026df5efe9eef1d7accc2653bdf050c3c1c14"><code>ab4026d</code></a>
Use the latest lints package (<a
href="https://redirect.github.com/sass/dart-sass/issues/2789">#2789</a>)</li>
<li><a
href="https://github.com/sass/dart-sass/commit/fbb1fe0533cc78c14b848c709b30d599c5bf86e1"><code>fbb1fe0</code></a>
Bump zizmorcore/zizmor-action from 0.5.6 to 0.5.7 (<a
href="https://redirect.github.com/sass/dart-sass/issues/2793">#2793</a>)</li>
<li><a
href="https://github.com/sass/dart-sass/commit/8abfa4d23d447bcada1efe415066458a08a0a155"><code>8abfa4d</code></a>
Fix <code>--watch</code> not recompiling files modified via atomic save
(<a
href="https://redirect.github.com/sass/dart-sass/issues/2791">#2791</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/sass/dart-sass/compare/1.101.0...1.101.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=sass&package-manager=npm_and_yarn&previous-version=1.101.0&new-version=1.101.3)](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 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>
2026-07-24 13:50:21 +00:00
dependabot[bot] a408437cf8 chore(deps-dev): Bump stylelint from 17.14.0 to 17.14.1 (#2024)
Bumps [stylelint](https://github.com/stylelint/stylelint) from 17.14.0
to 17.14.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/stylelint/stylelint/releases">stylelint's
releases</a>.</em></p>
<blockquote>
<h2>17.14.1</h2>
<p>It fixes 4 bugs.</p>
<ul>
<li>Fixed: <code>quiet</code> option suppresses <code>report*</code>
warning reports (<a
href="https://redirect.github.com/stylelint/stylelint/issues/9387">#9387</a>)
(<a
href="https://github.com/ychampion"><code>@​ychampion</code></a>).</li>
<li>Fixed: reported range of unknown rules (<a
href="https://redirect.github.com/stylelint/stylelint/issues/9385">#9385</a>)
(<a
href="https://github.com/ybiquitous"><code>@​ybiquitous</code></a>).</li>
<li>Fixed:
<code>declaration-block-no-redundant-longhand-properties</code> autofix
producing invalid <code>background</code> shorthand when
<code>background-size</code> is present (<a
href="https://redirect.github.com/stylelint/stylelint/issues/9364">#9364</a>)
(<a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
<li>Fixed: <code>rule-empty-line-before</code> false positives for
shared-line comments with <code>except:
[&quot;after-single-line-comment&quot;]</code> (<a
href="https://redirect.github.com/stylelint/stylelint/issues/9394">#9394</a>)
(<a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/stylelint/stylelint/blob/main/CHANGELOG.md">stylelint's
changelog</a>.</em></p>
<blockquote>
<h2>17.14.1 - 2026-07-20</h2>
<p>It fixes 4 bugs.</p>
<ul>
<li>Fixed: <code>quiet</code> option suppresses <code>report*</code>
warning reports (<a
href="https://redirect.github.com/stylelint/stylelint/pull/9387">#9387</a>)
(<a
href="https://github.com/ychampion"><code>@​ychampion</code></a>).</li>
<li>Fixed: reported range of unknown rules (<a
href="https://redirect.github.com/stylelint/stylelint/pull/9385">#9385</a>)
(<a
href="https://github.com/ybiquitous"><code>@​ybiquitous</code></a>).</li>
<li>Fixed:
<code>declaration-block-no-redundant-longhand-properties</code> autofix
producing invalid <code>background</code> shorthand when
<code>background-size</code> is present (<a
href="https://redirect.github.com/stylelint/stylelint/pull/9364">#9364</a>)
(<a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
<li>Fixed: <code>rule-empty-line-before</code> false positives for
shared-line comments with <code>except:
[&quot;after-single-line-comment&quot;]</code> (<a
href="https://redirect.github.com/stylelint/stylelint/pull/9394">#9394</a>)
(<a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/stylelint/stylelint/commit/cd66b035087270dd62d33542154463266cc5e81a"><code>cd66b03</code></a>
Release 17.14.1 (<a
href="https://redirect.github.com/stylelint/stylelint/issues/9397">#9397</a>)</li>
<li><a
href="https://github.com/stylelint/stylelint/commit/7c30fc5987c18c7c017416299d84ad162a53e725"><code>7c30fc5</code></a>
Refactor to replace <code>void</code> with <code>undefined</code> in
union types (<a
href="https://redirect.github.com/stylelint/stylelint/issues/9395">#9395</a>)</li>
<li><a
href="https://github.com/stylelint/stylelint/commit/57c8d0e3c54d83342d23895991d0400ae47e2c14"><code>57c8d0e</code></a>
Fix <code>rule-empty-line-before</code> false positives for shared-line
comments with `e...</li>
<li><a
href="https://github.com/stylelint/stylelint/commit/226f10a68f8fe420f4f32060a23920723c0943ba"><code>226f10a</code></a>
Bump <code>@​vscode/windows-process-tree</code> from 0.7.0 to 0.8.0 (<a
href="https://redirect.github.com/stylelint/stylelint/issues/9392">#9392</a>)</li>
<li><a
href="https://github.com/stylelint/stylelint/commit/13c3c5c7e95853d58247f77e84f342e60d9f88ca"><code>13c3c5c</code></a>
Bump globby from 16.2.0 to 16.2.1 (<a
href="https://redirect.github.com/stylelint/stylelint/issues/9393">#9393</a>)</li>
<li><a
href="https://github.com/stylelint/stylelint/commit/6dafcf5cfd7bbd8c62e19601cca3be7235ce75d4"><code>6dafcf5</code></a>
Document unknown-effort contributions (<a
href="https://redirect.github.com/stylelint/stylelint/issues/9391">#9391</a>)</li>
<li><a
href="https://github.com/stylelint/stylelint/commit/54c2bc8ac9e1008419ea5fd8f3ee4fddb8ff4a96"><code>54c2bc8</code></a>
Fix test for Yarn PnP with npm v12 (<a
href="https://redirect.github.com/stylelint/stylelint/issues/9390">#9390</a>)</li>
<li><a
href="https://github.com/stylelint/stylelint/commit/3e7eb56f721a28cc60070cec25d3776715bfb1a5"><code>3e7eb56</code></a>
Fix <code>declaration-block-no-redundant-longhand-properties</code>
invalid <code>background</code>...</li>
<li><a
href="https://github.com/stylelint/stylelint/commit/438196ede78f9a8be3413b0dedecc25f10dd8ab0"><code>438196e</code></a>
Bump postcss from 8.5.15 to 8.5.16 in the postcss group across 1
directory (#...</li>
<li><a
href="https://github.com/stylelint/stylelint/commit/c1e1489dee8d214f9df51945bc9bac0e66b666a6"><code>c1e1489</code></a>
Fix <code>quiet</code> option to suppress <code>report*</code> warning
reports (<a
href="https://redirect.github.com/stylelint/stylelint/issues/9387">#9387</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/stylelint/stylelint/compare/17.14.0...17.14.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=stylelint&package-manager=npm_and_yarn&previous-version=17.14.0&new-version=17.14.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 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>
2026-07-23 13:51:28 +00:00
dependabot[bot] 01e58e0895 chore(deps): Bump fast-uri from 3.1.2 to 3.1.4 (#2023)
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.2 to
3.1.4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/fastify/fast-uri/releases">fast-uri's
releases</a>.</em></p>
<blockquote>
<h2>v3.1.4</h2>
<h2>⚠️ Security Release</h2>
<p>Fix for <a
href="https://github.com/fastify/fast-uri/security/advisories/GHSA-v2hh-gcrm-f6hx">https://github.com/fastify/fast-uri/security/advisories/GHSA-v2hh-gcrm-f6hx</a></p>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/fastify/fast-uri/compare/v3.1.3...v3.1.4">https://github.com/fastify/fast-uri/compare/v3.1.3...v3.1.4</a></p>
<h2>v3.1.3</h2>
<h2>⚠️ Security Release</h2>
<ul>
<li>Fixes: <a
href="https://github.com/fastify/fast-uri/security/advisories/GHSA-4c8g-83qw-93j6">https://github.com/fastify/fast-uri/security/advisories/GHSA-4c8g-83qw-93j6</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/fastify/fast-uri/compare/v3.1.2...v3.1.3">https://github.com/fastify/fast-uri/compare/v3.1.2...v3.1.3</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/fastify/fast-uri/commit/6aeece669e4166b2446a89f17c07a3b15dfb7ed4"><code>6aeece6</code></a>
Bumped v3.1.4</li>
<li><a
href="https://github.com/fastify/fast-uri/commit/2d50fbabc80e4d0884fe0f6a98fe118ce6faa353"><code>2d50fba</code></a>
fix: reject literal backslash in URI authority</li>
<li><a
href="https://github.com/fastify/fast-uri/commit/0549fe35b0d482233f3be2816439f3ec803603fa"><code>0549fe3</code></a>
Bumped v3.1.3</li>
<li><a
href="https://github.com/fastify/fast-uri/commit/2a6d357a18a68e6d812824379fd3388a1ae50d05"><code>2a6d357</code></a>
Merge commit from fork</li>
<li>See full diff in <a
href="https://github.com/fastify/fast-uri/compare/v3.1.2...v3.1.4">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=fast-uri&package-manager=npm_and_yarn&previous-version=3.1.2&new-version=3.1.4)](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 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)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/maplibre/maputnik/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-22 14:01:04 +00:00
dependabot[bot] 8128fef161 chore(deps-dev): Bump immutable from 5.1.5 to 5.1.9 (#2022)
Bumps [immutable](https://github.com/immutable-js/immutable-js) from
5.1.5 to 5.1.9.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/immutable-js/immutable-js/releases">immutable's
releases</a>.</em></p>
<blockquote>
<h2>v5.1.9</h2>
<h2>What's Changed</h2>
<ul>
<li>fix(List): preserve undefined values when grown past 32 elements by
<a href="https://github.com/spokodev"><code>@​spokodev</code></a> / <a
href="https://github.com/jdeniau"><code>@​jdeniau</code></a> in <a
href="https://redirect.github.com/immutable-js/immutable-js/pull/2235">immutable-js/immutable-js#2235</a>
(originated from <a
href="https://redirect.github.com/immutable-js/immutable-js/pull/2230">immutable-js/immutable-js#2230</a>
)</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/immutable-js/immutable-js/compare/v5.1.8...v5.1.9">https://github.com/immutable-js/immutable-js/compare/v5.1.8...v5.1.9</a></p>
<h2>v5.1.8</h2>
<h2>What's Changed</h2>
<ul>
<li>fix(List): guard oversized bounds in setListBounds. Fixes CVE <a
href="https://github.com/immutable-js/immutable-js/security/advisories/GHSA-v56q-mh7h-f735">https://github.com/immutable-js/immutable-js/security/advisories/GHSA-v56q-mh7h-f735</a></li>
<li>perf(Map): index large hash-collision buckets for faster lookups.
Fixes CVE <a
href="https://github.com/immutable-js/immutable-js/security/advisories/GHSA-xvcm-6775-5m9r">https://github.com/immutable-js/immutable-js/security/advisories/GHSA-xvcm-6775-5m9r</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/immutable-js/immutable-js/compare/v5.1.7...v5.1.8">https://github.com/immutable-js/immutable-js/compare/v5.1.7...v5.1.8</a></p>
<h2>v5.1.7</h2>
<h2>What's Changed</h2>
<ul>
<li>fix(Repeat): lastIndexOf returned size instead of size - 1 by <a
href="https://github.com/chatman-media"><code>@​chatman-media</code></a>
in <a
href="https://redirect.github.com/immutable-js/immutable-js/pull/2227">immutable-js/immutable-js#2227</a></li>
</ul>
<h2>internal</h2>
<ul>
<li>Backport tests from 6.x branch by <a
href="https://github.com/jdeniau"><code>@​jdeniau</code></a> in <a
href="https://redirect.github.com/immutable-js/immutable-js/pull/2199">immutable-js/immutable-js#2199</a></li>
<li>fix(IndexedCollection): <code>has(index)</code> on a lazy
<code>Seq</code> of unknown size now checks index existence instead of
searching for a value equal to the index by <a
href="https://github.com/jdeniau"><code>@​jdeniau</code></a> in <a
href="https://redirect.github.com/immutable-js/immutable-js/pull/2203">immutable-js/immutable-js#2203</a></li>
<li>Better type for reduce if not ininitial value is given by <a
href="https://github.com/jdeniau"><code>@​jdeniau</code></a> in <a
href="https://redirect.github.com/immutable-js/immutable-js/pull/2205">immutable-js/immutable-js#2205</a></li>
<li>Backport test from <a
href="https://redirect.github.com/immutable-js/immutable-js/issues/2193">#2193</a>
by <a href="https://github.com/jdeniau"><code>@​jdeniau</code></a> in <a
href="https://redirect.github.com/immutable-js/immutable-js/pull/2207">immutable-js/immutable-js#2207</a></li>
<li>backport tests from 6.x by <a
href="https://github.com/jdeniau"><code>@​jdeniau</code></a> in <a
href="https://redirect.github.com/immutable-js/immutable-js/pull/2213">immutable-js/immutable-js#2213</a></li>
<li>Firefox link to non-French page by <a
href="https://github.com/jdeniau"><code>@​jdeniau</code></a> in <a
href="https://redirect.github.com/immutable-js/immutable-js/pull/2219">immutable-js/immutable-js#2219</a></li>
<li>Bump esbuild and <code>@​size-limit/preset-small-lib</code> by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/immutable-js/immutable-js/pull/2217">immutable-js/immutable-js#2217</a></li>
<li>backport tests from <a
href="https://redirect.github.com/immutable-js/immutable-js/issues/2210">#2210</a>
by <a href="https://github.com/jdeniau"><code>@​jdeniau</code></a> in <a
href="https://redirect.github.com/immutable-js/immutable-js/pull/2214">immutable-js/immutable-js#2214</a></li>
<li>Bump shell-quote from 1.8.2 to 1.8.4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/immutable-js/immutable-js/pull/2211">immutable-js/immutable-js#2211</a></li>
<li>Bump <code>@​tootallnate/once</code> from 2.0.0 to 2.0.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/immutable-js/immutable-js/pull/2190">immutable-js/immutable-js#2190</a></li>
<li>Bump minimatch from 9.0.5 to 9.0.9 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/immutable-js/immutable-js/pull/2189">immutable-js/immutable-js#2189</a></li>
<li>Bump <code>@​codemirror/theme-one-dark</code> from 6.1.2 to 6.1.3 by
<a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/immutable-js/immutable-js/pull/2168">immutable-js/immutable-js#2168</a></li>
<li>Bump form-data from 4.0.5 to 4.0.6 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/immutable-js/immutable-js/pull/2220">immutable-js/immutable-js#2220</a></li>
<li>Bump typescript-eslint from 8.33.0 to 8.61.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/immutable-js/immutable-js/pull/2222">immutable-js/immutable-js#2222</a></li>
<li>Bump <code>@​rollup/plugin-commonjs</code> from 28.0.2 to 29.0.3 by
<a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/immutable-js/immutable-js/pull/2224">immutable-js/immutable-js#2224</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/chatman-media"><code>@​chatman-media</code></a>
made their first contribution in <a
href="https://redirect.github.com/immutable-js/immutable-js/pull/2227">immutable-js/immutable-js#2227</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/immutable-js/immutable-js/compare/v5.1.6...v5.1.7">https://github.com/immutable-js/immutable-js/compare/v5.1.6...v5.1.7</a></p>
<h2>v5.1.6</h2>
<h2>What's Changed</h2>
<ul>
<li>fix(reverseFactory): read reversedSequence.size in __iterator
instead of this by <a
href="https://github.com/jdeniau"><code>@​jdeniau</code></a> in <a
href="https://redirect.github.com/immutable-js/immutable-js/pull/2196">immutable-js/immutable-js#2196</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/immutable-js/immutable-js/blob/main/CHANGELOG.md">immutable's
changelog</a>.</em></p>
<blockquote>
<h2>5.1.9</h2>
<ul>
<li>fix(List): a <code>List</code> grown past 32 elements while all its
values are <code>undefined</code> no longer reads those values back as
<code>null</code> (affected <code>get</code>, iteration,
<code>toArray</code>, <code>equals</code> and
<code>hashCode</code>)</li>
</ul>
<h2>5.1.8</h2>
<ul>
<li>fix(List): guard oversized bounds in setListBounds. Fixes CVE <a
href="https://github.com/immutable-js/immutable-js/security/advisories/GHSA-v56q-mh7h-f735">https://github.com/immutable-js/immutable-js/security/advisories/GHSA-v56q-mh7h-f735</a></li>
<li>perf(Map): index large hash-collision buckets for faster lookups.
Fixes CVE <a
href="https://github.com/immutable-js/immutable-js/security/advisories/GHSA-xvcm-6775-5m9r">https://github.com/immutable-js/immutable-js/security/advisories/GHSA-xvcm-6775-5m9r</a></li>
</ul>
<h2>4.3.9</h2>
<ul>
<li>fix(List): guard oversized bounds in setListBounds. Fixes CVE <a
href="https://github.com/immutable-js/immutable-js/security/advisories/GHSA-v56q-mh7h-f735">https://github.com/immutable-js/immutable-js/security/advisories/GHSA-v56q-mh7h-f735</a></li>
<li>perf(Map): index large hash-collision buckets for faster lookups.
Fixes CVE <a
href="https://github.com/immutable-js/immutable-js/security/advisories/GHSA-xvcm-6775-5m9r">https://github.com/immutable-js/immutable-js/security/advisories/GHSA-xvcm-6775-5m9r</a></li>
</ul>
<h2>5.1.7</h2>
<ul>
<li>fix(Repeat): lastIndexOf returned size instead of size - 1 by <a
href="https://github.com/chatman-media"><code>@​chatman-media</code></a>
in <a
href="https://redirect.github.com/immutable-js/immutable-js/pull/2227">immutable-js/immutable-js#2227</a>.
Fixes CVE <a
href="https://github.com/immutable-js/immutable-js/security/advisories/GHSA-wf6x-7x77-mvgw">CVE-2026-29063
</a></li>
<li>fix(IndexedCollection): <code>has(index)</code> on a lazy
<code>Seq</code> of unknown size now checks index existence instead of
searching for a value equal to the index <a
href="https://redirect.github.com/immutable-js/immutable-js/pull/2203">#2203</a></li>
<li>[TypeScript]: <code>reduce</code>/<code>reduceRight</code> without
an initial value now infer the result type from the collection's values
when the reducer returns a value (e.g. <code>list.reduce((a, b) =&gt; a
+ b)</code> infers <code>number</code>), matching
<code>Array#reduce</code>. Previously an explicit type argument was
required. <a
href="https://redirect.github.com/immutable-js/immutable-js/pull/2205">#2205</a></li>
</ul>
<h2>5.1.6</h2>
<ul>
<li>fix(reverseFactory): read <code>reversedSequence.size</code> in
<code>__iterator</code> instead of this <a
href="https://redirect.github.com/immutable-js/immutable-js/pull/2196">#2196</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/immutable-js/immutable-js/commit/329f7a680efa262c310b938a343295880eefe4fc"><code>329f7a6</code></a>
5.1.9</li>
<li><a
href="https://github.com/immutable-js/immutable-js/commit/21fabd92bc6cd6771918fc966004034fc10a06e0"><code>21fabd9</code></a>
changelog</li>
<li><a
href="https://github.com/immutable-js/immutable-js/commit/009164facb3c69870c6ad4c6472b1baeb02fcdf7"><code>009164f</code></a>
Merge pull request <a
href="https://redirect.github.com/immutable-js/immutable-js/issues/2235">#2235</a>
from immutable-js/fix/avoid-null-when-setsize33</li>
<li><a
href="https://github.com/immutable-js/immutable-js/commit/5b65bfbd583472eaa9686137a61db2a29d63cd36"><code>5b65bfb</code></a>
fix(List): preserve undefined values when grown past 32 elements</li>
<li><a
href="https://github.com/immutable-js/immutable-js/commit/50bf39e94105f83f1132cce02b9ee6c45cc7d9ca"><code>50bf39e</code></a>
Update CHANGELOG.md for versions 5.1.8 and 4.3.9</li>
<li><a
href="https://github.com/immutable-js/immutable-js/commit/6496539bd1e9dfa75ce11b053e7d70d79e9ac6f7"><code>6496539</code></a>
5.1.8</li>
<li><a
href="https://github.com/immutable-js/immutable-js/commit/808a83a071c9e22b8199ac256dc7e6e764198aac"><code>808a83a</code></a>
Merge commit from fork</li>
<li><a
href="https://github.com/immutable-js/immutable-js/commit/e51d49fc612ded5ec4dfb94ff294d22074269b0f"><code>e51d49f</code></a>
perf(Map): index large hash-collision buckets for faster lookups</li>
<li><a
href="https://github.com/immutable-js/immutable-js/commit/25c58b094f112dc32e9b0bca79c84f448d0d2a38"><code>25c58b0</code></a>
fix typescript in tests</li>
<li><a
href="https://github.com/immutable-js/immutable-js/commit/a1a1ee412dcaa380ab325196283d06594ffe4b84"><code>a1a1ee4</code></a>
Merge commit from fork</li>
<li>Additional commits viewable in <a
href="https://github.com/immutable-js/immutable-js/compare/v5.1.5...v5.1.9">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=immutable&package-manager=npm_and_yarn&previous-version=5.1.5&new-version=5.1.9)](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 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)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/maplibre/maputnik/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-22 14:00:36 +00:00
dependabot[bot] 283ef008a6 chore(deps-dev): Bump postcss from 8.5.19 to 8.5.20 (#2021)
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.19 to
8.5.20.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/postcss/postcss/releases">postcss's
releases</a>.</em></p>
<blockquote>
<h2>8.5.20</h2>
<ul>
<li>Fixed missing space if <code>AtRule#params</code> is set after (by
<a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
<li>Fixed mixing AST error on warnings (by <a
href="https://github.com/MahinAnowar"><code>@​MahinAnowar</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/postcss/postcss/blob/main/CHANGELOG.md">postcss's
changelog</a>.</em></p>
<blockquote>
<h2>8.5.20</h2>
<ul>
<li>Fixed missing space if <code>AtRule#params</code> is set after (by
<a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
<li>Fixed mixing AST error on warnings (by <a
href="https://github.com/MahinAnowar"><code>@​MahinAnowar</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/postcss/postcss/commit/c4ac725d5920916d35be44002b49b7f66f8b1dc8"><code>c4ac725</code></a>
Release 8.5.20 version</li>
<li><a
href="https://github.com/postcss/postcss/commit/c30586d7863d0563e2f2707bd89461636e37f6f6"><code>c30586d</code></a>
Fix missing space when AtRule#params is set after parsing (<a
href="https://redirect.github.com/postcss/postcss/issues/2113">#2113</a>)</li>
<li><a
href="https://github.com/postcss/postcss/commit/5bfc3b9e7463936fdd4898f92dd43c358bfdef62"><code>5bfc3b9</code></a>
Update dependencies</li>
<li><a
href="https://github.com/postcss/postcss/commit/24733fdbfe9abe4a949eb0a10e53db8c87fc9277"><code>24733fd</code></a>
Move back to latest 11 pnpm</li>
<li><a
href="https://github.com/postcss/postcss/commit/728127c427e076ae15f4b4107cae29f9e60d2db4"><code>728127c</code></a>
Update pnpm to check that CI is fixed</li>
<li><a
href="https://github.com/postcss/postcss/commit/806fe21198e210cc85fd87d4afd0c949b04e0c1f"><code>806fe21</code></a>
Rebuild nodes from another PostCSS copy in Warning (<a
href="https://redirect.github.com/postcss/postcss/issues/2112">#2112</a>)</li>
<li><a
href="https://github.com/postcss/postcss/commit/337cb7ef092da6066ec845973af243ed8e378550"><code>337cb7e</code></a>
Improve CI security</li>
<li><a
href="https://github.com/postcss/postcss/commit/7a8ca2d0e5044fbde3df33e2b9730c14528270e3"><code>7a8ca2d</code></a>
Remove patch after updating dependency</li>
<li>See full diff in <a
href="https://github.com/postcss/postcss/compare/8.5.19...8.5.20">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=postcss&package-manager=npm_and_yarn&previous-version=8.5.19&new-version=8.5.20)](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 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>
2026-07-22 13:51:15 +00:00
dependabot[bot] d021155991 chore(deps): Bump actions/checkout from 7.0.0 to 7.0.1 (#2020)
Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0
to 7.0.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/releases">actions/checkout's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.1</h2>
<h2>What's Changed</h2>
<ul>
<li>skip running unsafe pr check if input is default by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2518">actions/checkout#2518</a></li>
<li>trim only ascii whitespace for branch by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2521">actions/checkout#2521</a></li>
<li>escape values passed to --unset by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2530">actions/checkout#2530</a></li>
<li>Various dependency updates</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v7...v7.0.1">https://github.com/actions/checkout/compare/v7...v7.0.1</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/blob/main/CHANGELOG.md">actions/checkout's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>v7.0.1</h2>
<ul>
<li>Skip running unsafe pr check if input is default by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2518">actions/checkout#2518</a></li>
<li>Trim only ascii whitespace for branch by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2521">actions/checkout#2521</a></li>
<li>Escape values passed to --unset by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2530">actions/checkout#2530</a></li>
<li>Various dependency updates</li>
</ul>
<h2>v7.0.0</h2>
<ul>
<li>Block checking out fork PR for pull_request_target and workflow_run
by <a href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2454">actions/checkout#2454</a></li>
<li>Various dependency updates</li>
</ul>
<h2>v6.0.3</h2>
<ul>
<li>Fix checkout init for SHA-256 repositories by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2439">actions/checkout#2439</a></li>
<li>fix: expand merge commit SHA regex and add SHA-256 test cases by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
</ul>
<h2>v6.0.2</h2>
<ul>
<li>Fix tag handling: preserve annotations and explicit fetch-tags by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2356">actions/checkout#2356</a></li>
</ul>
<h2>v6.0.1</h2>
<ul>
<li>Add worktree support for persist-credentials includeIf by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2327">actions/checkout#2327</a></li>
</ul>
<h2>v6.0.0</h2>
<ul>
<li>Persist creds to a separate file by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2286">actions/checkout#2286</a></li>
<li>Update README to include Node.js 24 support details and requirements
by <a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2248">actions/checkout#2248</a></li>
</ul>
<h2>v5.0.1</h2>
<ul>
<li>Port v6 cleanup to v5 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2301">actions/checkout#2301</a></li>
</ul>
<h2>v5.0.0</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
</ul>
<h2>v4.3.1</h2>
<ul>
<li>Port v6 cleanup to v4 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2305">actions/checkout#2305</a></li>
</ul>
<h2>v4.3.0</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<h2>v4.2.2</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<h2>v4.2.1</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/checkout/commit/3d3c42e5aac5ba805825da76410c181273ba90b1"><code>3d3c42e</code></a>
prep v7.0.1 release (<a
href="https://redirect.github.com/actions/checkout/issues/2531">#2531</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/28802689a136bfcdb721715abd713740beecbe07"><code>2880268</code></a>
escape values passed to --unset (<a
href="https://redirect.github.com/actions/checkout/issues/2530">#2530</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/12cd2235efa0937479335606d7c3ac9f6c0973b1"><code>12cd223</code></a>
trim only ascii whitespace for branch (<a
href="https://redirect.github.com/actions/checkout/issues/2521">#2521</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/62661c4e71a304b2823ed026347b8d34c3eac541"><code>62661c4</code></a>
skip running unsafe pr check if input is default (<a
href="https://redirect.github.com/actions/checkout/issues/2518">#2518</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/e8d4307400f9427dba7cb98e488d6ab85f1cec5f"><code>e8d4307</code></a>
Bump the minor-actions-dependencies group with 2 updates (<a
href="https://redirect.github.com/actions/checkout/issues/2499">#2499</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/631c942040754b6e095e929c1677c07e10ed4f87"><code>631c942</code></a>
eslint 9 (<a
href="https://redirect.github.com/actions/checkout/issues/2474">#2474</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/4f1f4aec02e41874fa0262ea8ff5172d7978ad1e"><code>4f1f4ae</code></a>
Bump actions/upload-artifact from 4 to 7 (<a
href="https://redirect.github.com/actions/checkout/issues/2476">#2476</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/ba097532fb203f7e88c9c3c0b899b49469908a92"><code>ba09753</code></a>
Bump actions/checkout from 6 to 7 (<a
href="https://redirect.github.com/actions/checkout/issues/2488">#2488</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/b9e0990d219a03df7633c93f6f005a8fecbcab22"><code>b9e0990</code></a>
Bump docker/login-action from 3.3.0 to 4.2.0 (<a
href="https://redirect.github.com/actions/checkout/issues/2479">#2479</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/e8cb398be4a550817e382abf69e4c12c76fce1f2"><code>e8cb398</code></a>
Bump docker/build-push-action from 6.5.0 to 7.2.0 (<a
href="https://redirect.github.com/actions/checkout/issues/2478">#2478</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/checkout/compare/9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0...3d3c42e5aac5ba805825da76410c181273ba90b1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=7.0.0&new-version=7.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 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>
2026-07-21 13:53:31 +00:00
dependabot[bot] d06a3e7e1e chore(deps): Bump actions/setup-go from 6.5.0 to 7.0.0 (#2016)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6.5.0
to 7.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/setup-go/releases">actions/setup-go's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Migrate to ESM and upgrade dependencies by <a
href="https://github.com/priyagupta108"><code>@​priyagupta108</code></a>
in <a
href="https://redirect.github.com/actions/setup-go/pull/763">actions/setup-go#763</a></li>
<li>chore(deps): bump <code>@​actions/cache</code> to 6.2.0 by <a
href="https://github.com/philip-gai"><code>@​philip-gai</code></a> in <a
href="https://redirect.github.com/actions/setup-go/pull/771">actions/setup-go#771</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/philip-gai"><code>@​philip-gai</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/setup-go/pull/771">actions/setup-go#771</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-go/compare/v6...v7.0.0">https://github.com/actions/setup-go/compare/v6...v7.0.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/setup-go/commit/b7ad1dad31e06c5925ef5d2fc7ad053ef454303e"><code>b7ad1da</code></a>
chore(deps): bump <code>@​actions/cache</code> to 6.2.0 (<a
href="https://redirect.github.com/actions/setup-go/issues/771">#771</a>)</li>
<li><a
href="https://github.com/actions/setup-go/commit/0778a10ce47b5d450cf60fb94fafad4330008a35"><code>0778a10</code></a>
Migrate to ESM and upgrade dependencies (<a
href="https://redirect.github.com/actions/setup-go/issues/763">#763</a>)</li>
<li>See full diff in <a
href="https://github.com/actions/setup-go/compare/924ae3a1cded613372ab5595356fb5720e22ba16...b7ad1dad31e06c5925ef5d2fc7ad053ef454303e">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-go&package-manager=github_actions&previous-version=6.5.0&new-version=7.0.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 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>
2026-07-20 13:53:46 +00:00
dependabot[bot] e0f27214b7 chore(deps): Bump github/codeql-action/init from 4.37.0 to 4.37.1 (#2015)
Bumps
[github/codeql-action/init](https://github.com/github/codeql-action)
from 4.37.0 to 4.37.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/github/codeql-action/releases">github/codeql-action/init's
releases</a>.</em></p>
<blockquote>
<h2>v4.37.1</h2>
<ul>
<li><em>Upcoming breaking change</em>: Add a deprecation warning for
customers using CodeQL version 2.20.6 and earlier. These versions of
CodeQL were discontinued on 1 July 2026 alongside GitHub Enterprise
Server 3.16, and will be unsupported by the next minor release of the
CodeQL Action. <a
href="https://redirect.github.com/github/codeql-action/pull/3956">#3956</a></li>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.1">2.26.1</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/4019">#4019</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/github/codeql-action/blob/main/CHANGELOG.md">github/codeql-action/init's
changelog</a>.</em></p>
<blockquote>
<h1>CodeQL Action Changelog</h1>
<p>See the <a
href="https://github.com/github/codeql-action/releases">releases
page</a> for the relevant changes to the CodeQL CLI and language
packs.</p>
<h2>[UNRELEASED]</h2>
<p>No user facing changes.</p>
<h2>4.37.1 - 16 Jul 2026</h2>
<ul>
<li><em>Upcoming breaking change</em>: Add a deprecation warning for
customers using CodeQL version 2.20.6 and earlier. These versions of
CodeQL were discontinued on 1 July 2026 alongside GitHub Enterprise
Server 3.16, and will be unsupported by the next minor release of the
CodeQL Action. <a
href="https://redirect.github.com/github/codeql-action/pull/3956">#3956</a></li>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.1">2.26.1</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/4019">#4019</a></li>
</ul>
<h2>4.37.0 - 08 Jul 2026</h2>
<ul>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.0">2.26.0</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/3995">#3995</a></li>
<li>In addition to the existing input format, the
<code>config-file</code> input for the <code>codeql-action/init</code>
step will soon support a new <code>[owner/]repo[@ref][:path]</code>
format. All components except the repository name are optional. If
omitted, <code>owner</code> defaults to the same owner as the repository
the analysis is running for, <code>ref</code> to <code>main</code>, and
<code>path</code> to <code>.github/codeql-action.yaml</code>. Support
for this format ships in this version of the CodeQL Action, but will
only be enabled over the coming weeks. <a
href="https://redirect.github.com/github/codeql-action/pull/3973">#3973</a></li>
</ul>
<h2>4.36.3 - 01 Jul 2026</h2>
<p>No user facing changes.</p>
<h2>4.36.2 - 04 Jun 2026</h2>
<ul>
<li>Cache CodeQL CLI version information across Actions steps. <a
href="https://redirect.github.com/github/codeql-action/pull/3943">#3943</a></li>
<li>Reduce requests while waiting for analysis processing by using
exponential backoff when polling SARIF processing status. <a
href="https://redirect.github.com/github/codeql-action/pull/3937">#3937</a></li>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.25.6">2.25.6</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/3948">#3948</a></li>
</ul>
<h2>4.36.1 - 02 Jun 2026</h2>
<p>No user facing changes.</p>
<h2>4.36.0 - 22 May 2026</h2>
<ul>
<li><em>Breaking change</em>: Bump the minimum required CodeQL bundle
version to 2.19.4. <a
href="https://redirect.github.com/github/codeql-action/pull/3894">#3894</a></li>
<li>Add support for SHA-256 Git object IDs. <a
href="https://redirect.github.com/github/codeql-action/pull/3893">#3893</a></li>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.25.5">2.25.5</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/3926">#3926</a></li>
</ul>
<h2>4.35.5 - 15 May 2026</h2>
<ul>
<li>We have improved how the JavaScript bundles for the CodeQL Action
are generated to avoid duplication across bundles and reduce the size of
the repository by around 70%. This should have no effect on the runtime
behaviour of the CodeQL Action. <a
href="https://redirect.github.com/github/codeql-action/pull/3899">#3899</a></li>
<li>For performance and accuracy reasons, <a
href="https://redirect.github.com/github/roadmap/issues/1158">improved
incremental analysis</a> will now only be enabled on a pull request when
diff-informed analysis is also enabled for that run. If diff-informed
analysis is unavailable (for example, because the PR diff ranges could
not be computed), the action will fall back to a full analysis. <a
href="https://redirect.github.com/github/codeql-action/pull/3791">#3791</a></li>
<li>If multiple inputs are provided for the GitHub-internal
<code>analysis-kinds</code> input, only <code>code-scanning</code> will
be enabled. The <code>analysis-kinds</code> input is experimental, for
GitHub-internal use only, and may change without notice at any time. <a
href="https://redirect.github.com/github/codeql-action/pull/3892">#3892</a></li>
<li>Added an experimental change which, when running a Code Scanning
analysis for a PR with <a
href="https://redirect.github.com/github/roadmap/issues/1158">improved
incremental analysis</a> enabled, prefers CodeQL CLI versions that have
a cached overlay-base database for the configured languages. This speeds
up analysis for a repository when there is not yet a cached overlay-base
database for the latest CLI version. We expect to roll this change out
to everyone in May. <a
href="https://redirect.github.com/github/codeql-action/pull/3880">#3880</a></li>
</ul>
<h2>4.35.4 - 07 May 2026</h2>
<ul>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.25.4">2.25.4</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/3881">#3881</a></li>
</ul>
<h2>4.35.3 - 01 May 2026</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/github/codeql-action/commit/7188fc363630916deb702c7fdcf4e481b751f97a"><code>7188fc3</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/4020">#4020</a>
from github/update-v4.37.1-9e7c07009</li>
<li><a
href="https://github.com/github/codeql-action/commit/c8b5f69be686908c3dfd844428137d56fe80c936"><code>c8b5f69</code></a>
Update changelog for v4.37.1</li>
<li><a
href="https://github.com/github/codeql-action/commit/9e7c070092090e89e8b3d62f977d4456e0732cd7"><code>9e7c070</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/4014">#4014</a>
from github/mbg/explicit-remote-prefix</li>
<li><a
href="https://github.com/github/codeql-action/commit/3492b7e9ab96e28b1d8b971345d30e929c6f8fee"><code>3492b7e</code></a>
Change <code>REMOTE_PATH_PREFIX</code> to <code>remote=</code></li>
<li><a
href="https://github.com/github/codeql-action/commit/3654baa924bc6456db54002581cb7c1c877548c4"><code>3654baa</code></a>
Merge remote-tracking branch 'origin/main' into
mbg/explicit-remote-prefix</li>
<li><a
href="https://github.com/github/codeql-action/commit/2d682ac05f1b3588aaff3814826bede39b9ba6bb"><code>2d682ac</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/4017">#4017</a>
from github/dependabot/github_actions/dot-github/wor...</li>
<li><a
href="https://github.com/github/codeql-action/commit/23f6a50753a88efd9b7ae8687b29f6bdb65f6250"><code>23f6a50</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/4009">#4009</a>
from github/mbg/action-state/additions</li>
<li><a
href="https://github.com/github/codeql-action/commit/1ee3c75d1988ab8621f01ebb165115c38d56df91"><code>1ee3c75</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/4018">#4018</a>
from github/dependabot/github_actions/dot-github/wor...</li>
<li><a
href="https://github.com/github/codeql-action/commit/e053684dc500899b0b5520edc8549ac0f1ed730b"><code>e053684</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/4015">#4015</a>
from github/dependabot/npm_and_yarn/npm-minor-fd2e83...</li>
<li><a
href="https://github.com/github/codeql-action/commit/6803c5671d2f87a83ed96e151c441b1cb3bdc66a"><code>6803c56</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/4019">#4019</a>
from github/update-bundle/codeql-bundle-v2.26.1</li>
<li>Additional commits viewable in <a
href="https://github.com/github/codeql-action/compare/99df26d4f13ea111d4ec1a7dddef6063f76b97e9...7188fc363630916deb702c7fdcf4e481b751f97a">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github/codeql-action/init&package-manager=github_actions&previous-version=4.37.0&new-version=4.37.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 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>
2026-07-20 13:53:09 +00:00
dependabot[bot] 167f83abe9 chore(deps-dev): Bump i18next-cli from 1.65.0 to 1.66.2 (#2014)
Bumps [i18next-cli](https://github.com/i18next/i18next-cli) from 1.65.0
to 1.66.2.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/i18next/i18next-cli/blob/main/CHANGELOG.md">i18next-cli's
changelog</a>.</em></p>
<blockquote>
<h2>1.66.2</h2>
<ul>
<li>fix(extract): ignore <code>&lt;Trans&gt;</code> components that have
neither an <code>i18nKey</code>
expression nor non-whitespace children, preventing <code>&lt;Trans
/&gt;</code> and
spread-only variants from creating an empty-string translation key
(<a
href="https://redirect.github.com/i18next/i18next-cli/issues/274">#274</a>).</li>
</ul>
<h2>1.66.1</h2>
<ul>
<li>fix(logger): <code>ConsoleLogger.warn</code>/<code>error</code> no
longer swallow their optional
second argument — plugin-hook failures (e.g.
<code>Plugin my-plugin onVisitNode failed:</code>) now print the caught
error instead
of a bare message
(<a
href="https://redirect.github.com/i18next/i18next-cli/pull/273">#273</a>,
thanks
<a
href="https://github.com/pjrobertson"><code>@​pjrobertson</code></a>).</li>
</ul>
<h2>1.66.0</h2>
<ul>
<li>fix(funnel): never print locize funnel/tip messages in
non-interactive
contexts — they are now suppressed when <code>CI=true</code> or stdout
is not a TTY,
and the once-per-24h cooldown applies unconditionally (newly created
translation files no longer bypass it). CI pipelines and first
extractions
no longer get promotional output
(follow-up to <a
href="https://redirect.github.com/i18next/i18next-cli/issues/20">#20</a>
/
<a
href="https://redirect.github.com/i18next/i18next-cli/issues/222">#222</a>).</li>
<li>feat(extract): new <code>sort: 'locize'</code> preset that mirrors
the key order of
locize-published files (plain UTF-16 code-unit sort, case-sensitive, no
canonical plural-form reordering), making <code>extract</code> followed
by
<code>locize-sync</code>/<code>locize-download</code> round-trip without
spurious reorder diffs
(follow-up to <a
href="https://redirect.github.com/i18next/i18next-cli/issues/106">#106</a>).</li>
<li>test(locize): assert <code>--cdn-type</code> is forwarded to
<code>download</code> and <code>migrate</code>,
guarding against the <a
href="https://redirect.github.com/i18next/i18next-cli/issues/156">#156</a>
class of regression.</li>
<li>docs(readme): new &quot;Dynamic Keys&quot; section documenting the
built-in TS
type-aware resolution of finite dynamic keys shipped in v1.49
(<a
href="https://redirect.github.com/i18next/i18next-cli/issues/210">#210</a>)
with
<code>preservePatterns</code> reserved for truly runtime-dynamic keys;
note in
<code>migrate-config</code> that i18next-cli requires Node.js &gt;= 22
(i18next-parser
still supported Node 18/20).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/i18next/i18next-cli/commit/34f2b96dca5502c536b3e181ddfea80b43a66dcb"><code>34f2b96</code></a>
1.66.2</li>
<li><a
href="https://github.com/i18next/i18next-cli/commit/6761334ce2f4a4a105be3163c8b2acaa1e8b7796"><code>6761334</code></a>
fix(extract): skip empty Trans component keys (<a
href="https://redirect.github.com/i18next/i18next-cli/issues/274">#274</a>)</li>
<li><a
href="https://github.com/i18next/i18next-cli/commit/66acdcb18094054c8703fb73d7e82fbf6db9ddb1"><code>66acdcb</code></a>
1.66.1</li>
<li><a
href="https://github.com/i18next/i18next-cli/commit/8a5a136a0902063ef573156ccb9227c32527a3af"><code>8a5a136</code></a>
fix(logger): simplify error signature to unknown; changelog for 1.66.1
(<a
href="https://redirect.github.com/i18next/i18next-cli/issues/273">#273</a>)</li>
<li><a
href="https://github.com/i18next/i18next-cli/commit/cada9ca78fe0c52f0172f6e90a25842ccaaaa36a"><code>cada9ca</code></a>
Allow passing extra args (e.g. exceptions to the logger warn/error) (<a
href="https://redirect.github.com/i18next/i18next-cli/issues/273">#273</a>)</li>
<li><a
href="https://github.com/i18next/i18next-cli/commit/6555ff065e0de0aa42e9f350008e6d0de98e9833"><code>6555ff0</code></a>
docs: Code-Splitting Translations section (one namespace per route, lazy
chun...</li>
<li><a
href="https://github.com/i18next/i18next-cli/commit/69bb99618527ddfd91f5003dfc128f177b05dbe0"><code>69bb996</code></a>
1.66.0</li>
<li><a
href="https://github.com/i18next/i18next-cli/commit/d94a1468fac9a83cffa6c8c1241b3b9ec0cd1258"><code>d94a146</code></a>
feat(extract): sort: 'locize' preset for locize round-trip key order (<a
href="https://redirect.github.com/i18next/i18next-cli/issues/106">#106</a>);
...</li>
<li><a
href="https://github.com/i18next/i18next-cli/commit/3a278ee443805ea1468e7e377004d7ede968381a"><code>3a278ee</code></a>
fix(funnel): suppress promotional output in CI/non-TTY and always
respect the...</li>
<li><a
href="https://github.com/i18next/i18next-cli/commit/8c69ab921ba394a5f09fcac878a3feaf345f5d97"><code>8c69ab9</code></a>
docs: surface localize --print-agent-prompt in the top callout</li>
<li>See full diff in <a
href="https://github.com/i18next/i18next-cli/compare/v1.65.0...v1.66.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=i18next-cli&package-manager=npm_and_yarn&previous-version=1.65.0&new-version=1.66.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 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>
2026-07-20 13:51:49 +00:00
dependabot[bot] 0936afc2f9 chore(deps-dev): Bump typescript-eslint from 8.62.1 to 8.64.0 (#2013)
Bumps
[typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint)
from 8.62.1 to 8.64.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/releases">typescript-eslint's
releases</a>.</em></p>
<blockquote>
<h2>v8.64.0</h2>
<h2>8.64.0 (2026-07-13)</h2>
<h3>🚀 Features</h3>
<ul>
<li>support parsing <code>import defer</code> (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12513">#12513</a>)</li>
<li><strong>eslint-plugin:</strong> [no-loop-func] support
<code>using</code> / <code>await using</code> declarations and deprecate
the rule (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12500">#12500</a>)</li>
<li><strong>typescript-estree:</strong> throw for invalid definite
assignment in class properties (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12543">#12543</a>)</li>
</ul>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>eslint-plugin:</strong> [require-array-sort-compare] handle
constrained arrays (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12512">#12512</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Evyatar Daud <a
href="https://github.com/StyleShit"><code>@​StyleShit</code></a></li>
<li>송재욱</li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.64.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
<h2>v8.63.0</h2>
<h2>8.63.0 (2026-07-06)</h2>
<h3>🚀 Features</h3>
<ul>
<li><strong>eslint-plugin:</strong> [no-misused-promises] detect async
usage of a sync dispose usage (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12426">#12426</a>)</li>
</ul>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>eslint-plugin:</strong> [method-signature-style] suggest
converting readonly function properties instead of emitting invalid
syntax (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12447">#12447</a>,
<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/issues/12446">#12446</a>)</li>
<li><strong>eslint-plugin:</strong> [no-unnecessary-type-assertion]
handle optional-chained calls to overloaded functions (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12491">#12491</a>,
<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/issues/12485">#12485</a>)</li>
<li><strong>eslint-plugin:</strong> [no-base-to-string] don't flag a
shadowed String() call (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12492">#12492</a>)</li>
<li><strong>scope-manager:</strong> export ClassStaticBlockScope (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12460">#12460</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Evyatar Daud <a
href="https://github.com/StyleShit"><code>@​StyleShit</code></a></li>
<li>Kristjan <a
href="https://github.com/KristjanTammekivi"><code>@​KristjanTammekivi</code></a></li>
<li>Michael Naumov <a
href="https://github.com/mnaoumov"><code>@​mnaoumov</code></a></li>
<li>Serhii Leniv <a
href="https://github.com/Serhii-Leniv"><code>@​Serhii-Leniv</code></a></li>
<li>송재욱</li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.63.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md">typescript-eslint's
changelog</a>.</em></p>
<blockquote>
<h2>8.64.0 (2026-07-13)</h2>
<p>This was a version bump only for typescript-eslint to align it with
other projects, there were no code changes.</p>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.64.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
<h2>8.63.0 (2026-07-06)</h2>
<p>This was a version bump only for typescript-eslint to align it with
other projects, there were no code changes.</p>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.63.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/typescript-eslint/typescript-eslint/commit/414d9abbf66f77796ab12ec7e75b07722e592832"><code>414d9ab</code></a>
chore(release): publish 8.64.0</li>
<li><a
href="https://github.com/typescript-eslint/typescript-eslint/commit/290cf6cdcc5ffb00c5b8f3e1e0e9f2fd8cc96374"><code>290cf6c</code></a>
chore(release): publish 8.63.0</li>
<li>See full diff in <a
href="https://github.com/typescript-eslint/typescript-eslint/commits/v8.64.0/packages/typescript-eslint">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=typescript-eslint&package-manager=npm_and_yarn&previous-version=8.62.1&new-version=8.64.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 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>
2026-07-17 13:51:37 +00:00
dependabot[bot] b664caa13e chore(deps-dev): Bump postcss from 8.5.17 to 8.5.19 (#2011)
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.17 to
8.5.19.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/postcss/postcss/releases">postcss's
releases</a>.</em></p>
<blockquote>
<h2>8.5.19</h2>
<ul>
<li>Fixed cleaning <code>before</code> for new nodes inserted to
<code>Root</code> (by <a
href="https://github.com/MahinAnowar"><code>@​MahinAnowar</code></a>).</li>
</ul>
<h2>8.5.18</h2>
<ul>
<li>Restricted loading previous source maps file to the
<code>opts.from</code> folder for security reasons (use <code>unsafeMap:
true</code> to disable the check).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/postcss/postcss/blob/main/CHANGELOG.md">postcss's
changelog</a>.</em></p>
<blockquote>
<h2>8.5.19</h2>
<ul>
<li>Fixed cleaning <code>before</code> for new nodes inserted to
<code>Root</code> (by <a
href="https://github.com/MahinAnowar"><code>@​MahinAnowar</code></a>).</li>
</ul>
<h2>8.5.18</h2>
<ul>
<li>Restricted loading previous source maps file to the
<code>opts.from</code> folder for security reasons (use <code>unsafeMap:
true</code> to disable the check).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/postcss/postcss/commit/9543b22769bef5bcd47600fbca752204c106cda8"><code>9543b22</code></a>
Release 8.5.19 version</li>
<li><a
href="https://github.com/postcss/postcss/commit/3d13bf9360652922de0535f6257e1648187363f0"><code>3d13bf9</code></a>
Fix CI on Windows too</li>
<li><a
href="https://github.com/postcss/postcss/commit/00d0dd2322162f6083d507ea6954685e1c92f165"><code>00d0dd2</code></a>
Keep explicitly set raws.before when inserting nodes into root (<a
href="https://redirect.github.com/postcss/postcss/issues/2111">#2111</a>)</li>
<li><a
href="https://github.com/postcss/postcss/commit/7a05b33e7a15d6f80d90098784170ad3dca39180"><code>7a05b33</code></a>
Temporary fix CI</li>
<li><a
href="https://github.com/postcss/postcss/commit/4c0d194c136fd374495d0993c890d794cab65b81"><code>4c0d194</code></a>
Release 8.5.18 version</li>
<li><a
href="https://github.com/postcss/postcss/commit/92b4e7891ec7b811821d01acc8aa0f010caf41e2"><code>92b4e78</code></a>
Update dependencies</li>
<li><a
href="https://github.com/postcss/postcss/commit/95663d3eb7ba26f4854dd19d3b4f4425760cf56c"><code>95663d3</code></a>
Limit where source map can be loaded for security reasons</li>
<li>See full diff in <a
href="https://github.com/postcss/postcss/compare/8.5.17...8.5.19">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=postcss&package-manager=npm_and_yarn&previous-version=8.5.17&new-version=8.5.19)](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 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>
2026-07-16 13:51:58 +00:00
dependabot[bot] d90d7b743b chore(deps): Bump actions/setup-node from 6.4.0 to 7.0.0 (#2012)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from
6.4.0 to 7.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/setup-node/releases">actions/setup-node's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.0</h2>
<h2>What's Changed</h2>
<h3>Enhancements:</h3>
<ul>
<li>Add cache-primary-key and cache-matched-key as outputs by <a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a> in
<a
href="https://redirect.github.com/actions/setup-node/pull/1577">actions/setup-node#1577</a></li>
<li>Migrate to ESM and upgrade dependencies by <a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a> in
<a
href="https://redirect.github.com/actions/setup-node/pull/1574">actions/setup-node#1574</a></li>
</ul>
<h3>Bug fixes:</h3>
<ul>
<li>Remove dummy NODE_AUTH_TOKEN export by <a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a> in
<a
href="https://redirect.github.com/actions/setup-node/pull/1558">actions/setup-node#1558</a></li>
<li>Only use <code>mirrorToken</code> in <code>getManifest</code> if
it's provided by <a
href="https://github.com/deiga"><code>@​deiga</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1548">actions/setup-node#1548</a></li>
</ul>
<h3>Documentation updates:</h3>
<ul>
<li>Add documentation for publishing to npm with Trusted Publisher
(OIDC) by <a
href="https://github.com/chiranjib-swain"><code>@​chiranjib-swain</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1536">actions/setup-node#1536</a></li>
<li>docs: Update restore-only cache documentation by <a
href="https://github.com/priya-kinthali"><code>@​priya-kinthali</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1550">actions/setup-node#1550</a></li>
<li>docs: Update caching recommendations to mitigate cache poisoning
risks by <a
href="https://github.com/chiranjib-swain"><code>@​chiranjib-swain</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1567">actions/setup-node#1567</a></li>
</ul>
<h3>Dependency update:</h3>
<ul>
<li>Upgrade <code>@​actions/cache</code> to 5.1.0, log cache write
denied by <a
href="https://github.com/jasongin"><code>@​jasongin</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1569">actions/setup-node#1569</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/chiranjib-swain"><code>@​chiranjib-swain</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1536">actions/setup-node#1536</a></li>
<li><a href="https://github.com/deiga"><code>@​deiga</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1548">actions/setup-node#1548</a></li>
<li><a href="https://github.com/jasongin"><code>@​jasongin</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1569">actions/setup-node#1569</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-node/compare/v6...v7.0.0">https://github.com/actions/setup-node/compare/v6...v7.0.0</a></p>
<h2>v6.5.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update <code>@​actions/cache</code> to 5.1.0 and add security
overrides for undici and fast-xml-parser by <a
href="https://github.com/HarithaVattikuti"><code>@​HarithaVattikuti</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1579">actions/setup-node#1579</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-node/compare/v6.4.0...v6.5.0">https://github.com/actions/setup-node/compare/v6.4.0...v6.5.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/setup-node/commit/820762786026740c76f36085b0efc47a31fe5020"><code>8207627</code></a>
Migrate to ESM and upgrade dependencies (<a
href="https://redirect.github.com/actions/setup-node/issues/1574">#1574</a>)</li>
<li><a
href="https://github.com/actions/setup-node/commit/04be95cf3511ea51ebf9f224ddfb99cc7ab87cd4"><code>04be95c</code></a>
Add cache-primary-key and cache-matched-key as outputs (<a
href="https://redirect.github.com/actions/setup-node/issues/1577">#1577</a>)</li>
<li><a
href="https://github.com/actions/setup-node/commit/7c2c68d20d402ed6a201ada70a81341941093140"><code>7c2c68d</code></a>
docs: Update caching recommendations to mitigate cache poisoning risks
(<a
href="https://redirect.github.com/actions/setup-node/issues/1567">#1567</a>)</li>
<li><a
href="https://github.com/actions/setup-node/commit/6a61c0375d66246de94630495909f12cf8dac84d"><code>6a61c03</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/setup-node/issues/1569">#1569</a>
from jasongin/update-actions-cache-5.1.0</li>
<li><a
href="https://github.com/actions/setup-node/commit/30eb73b41ded577900c1ebf968ef95cdf8f7434f"><code>30eb73b</code></a>
Resolve high-severity audit issues</li>
<li><a
href="https://github.com/actions/setup-node/commit/4e1a87a501d0302f99e30e2748568adcb388d09f"><code>4e1a87a</code></a>
Update dist</li>
<li><a
href="https://github.com/actions/setup-node/commit/360237f0c01778d0c17291f75c56d6feae4f7574"><code>360237f</code></a>
Strict equality</li>
<li><a
href="https://github.com/actions/setup-node/commit/4f8aac5beb2f0854bc79651567a18c67eb0b9de3"><code>4f8aac5</code></a>
Bump <code>@​actions/cache</code> to 5.1.0, log cache write denied</li>
<li><a
href="https://github.com/actions/setup-node/commit/f4a67bbeca970f103397d3d2b9462cf787cd2980"><code>f4a67bb</code></a>
Only use <code>mirrorToken</code> in <code>getManifest</code> if it's
provided (<a
href="https://redirect.github.com/actions/setup-node/issues/1548">#1548</a>)</li>
<li><a
href="https://github.com/actions/setup-node/commit/0355742c943ddb13ca8a6b700f824231caa91e75"><code>0355742</code></a>
Remove dummy NODE_AUTH_TOKEN export (<a
href="https://redirect.github.com/actions/setup-node/issues/1558">#1558</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/setup-node/compare/48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e...820762786026740c76f36085b0efc47a31fe5020">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-node&package-manager=github_actions&previous-version=6.4.0&new-version=7.0.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 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>
2026-07-16 13:51:07 +00:00
dependabot[bot] 0152a02858 chore(deps-dev): Bump postcss from 8.5.16 to 8.5.17 (#2009)
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.16 to
8.5.17.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/postcss/postcss/releases">postcss's
releases</a>.</em></p>
<blockquote>
<h2>8.5.17</h2>
<ul>
<li>Fixed <code>Maximum call stack size exceeded</code> error.</li>
<li>Fixed Prototype hijacking for <code>postcss.fromJSON()</code>.</li>
<li>Fixed <code>Input#origin()</code> for unmapped end position (by <a
href="https://github.com/chatman-media"><code>@​chatman-media</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/postcss/postcss/blob/main/CHANGELOG.md">postcss's
changelog</a>.</em></p>
<blockquote>
<h2>8.5.17</h2>
<ul>
<li>Fixed <code>Maximum call stack size exceeded</code> error.</li>
<li>Fixed Prototype hijacking for <code>postcss.fromJSON()</code>.</li>
<li>Fixed <code>Input#origin()</code> for unmapped end position (by <a
href="https://github.com/chatman-media"><code>@​chatman-media</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/postcss/postcss/commit/74e25ae9f4efaa56a41a449064a655d7da78072c"><code>74e25ae</code></a>
Release 8.5.17 version</li>
<li><a
href="https://github.com/postcss/postcss/commit/d1518afd5a88f42728b30b87f8917210f363f9f1"><code>d1518af</code></a>
Fix Maximum call stack size exceeded error</li>
<li><a
href="https://github.com/postcss/postcss/commit/2421312ffea96ba77b35ce24a1b2d9c2e22b5e83"><code>2421312</code></a>
Fix linter</li>
<li><a
href="https://github.com/postcss/postcss/commit/a50352c583df991710f92ccac25b36304695161a"><code>a50352c</code></a>
Fix CI</li>
<li><a
href="https://github.com/postcss/postcss/commit/33948f0969bb858acdd52c9692e3a785a3ed0a73"><code>33948f0</code></a>
Prevent prototype hijacking in fromJSON</li>
<li><a
href="https://github.com/postcss/postcss/commit/2131909351161cd2c5fc2be58b14919a873ea824"><code>2131909</code></a>
Update dependencies</li>
<li><a
href="https://github.com/postcss/postcss/commit/93440abcca92793b31c5d1fdf5f2da7b58b27599"><code>93440ab</code></a>
Fix non-closed <code>\&lt;div align=&quot;center&quot;&gt;</code> in
README (<a
href="https://redirect.github.com/postcss/postcss/issues/2110">#2110</a>)</li>
<li><a
href="https://github.com/postcss/postcss/commit/1bf9076867e70f5a01776aebebee5aacbf9771ce"><code>1bf9076</code></a>
Remove old sponsors</li>
<li><a
href="https://github.com/postcss/postcss/commit/2d44be0923873ea6af2c3a1a191148e2ce9d733f"><code>2d44be0</code></a>
docs: fix invalid JS in writing-a-plugin examples (<a
href="https://redirect.github.com/postcss/postcss/issues/2107">#2107</a>)</li>
<li><a
href="https://github.com/postcss/postcss/commit/3298727755a676beced3cbe67015d42e70396e4f"><code>3298727</code></a>
Fix <code>Input#origin()</code> mixing null and undefined for unmapped
end position (<a
href="https://redirect.github.com/postcss/postcss/issues/2106">#2106</a>)</li>
<li>See full diff in <a
href="https://github.com/postcss/postcss/compare/8.5.16...8.5.17">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=postcss&package-manager=npm_and_yarn&previous-version=8.5.16&new-version=8.5.17)](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 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>
2026-07-15 13:50:43 +00:00
dependabot[bot] 0b076e3896 chore(deps-dev): Bump eslint from 10.6.0 to 10.7.0 (#2007)
Bumps [eslint](https://github.com/eslint/eslint) from 10.6.0 to 10.7.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/eslint/eslint/releases">eslint's
releases</a>.</em></p>
<blockquote>
<h2>v10.7.0</h2>
<h2>Features</h2>
<ul>
<li><a
href="https://github.com/eslint/eslint/commit/cf2a9bf2d982642f760370c15f1196f4658f4e27"><code>cf2a9bf</code></a>
feat: add errorClassNames option to preserve-caught-error rule (<a
href="https://redirect.github.com/eslint/eslint/issues/21032">#21032</a>)
(sethamus)</li>
<li><a
href="https://github.com/eslint/eslint/commit/f8b873aae53610b80b5c1005606716e05ed3b91f"><code>f8b873a</code></a>
feat: max-nested-callbacks option for constructor callbacks (<a
href="https://redirect.github.com/eslint/eslint/issues/21063">#21063</a>)
(fnx)</li>
<li><a
href="https://github.com/eslint/eslint/commit/557fde8bc633d26ea4fe287d4483f287e8e6080f"><code>557fde8</code></a>
feat: support computed <code>Number.parseInt</code> member access in
<code>radix</code> rule (<a
href="https://redirect.github.com/eslint/eslint/issues/21041">#21041</a>)
(Pixel)</li>
<li><a
href="https://github.com/eslint/eslint/commit/0b4a73b65808f5a8fd192f15db2bf5e47002b9f7"><code>0b4a73b</code></a>
feat: add suggestions to no-compare-neg-zero (<a
href="https://redirect.github.com/eslint/eslint/issues/21034">#21034</a>)
(den$)</li>
<li><a
href="https://github.com/eslint/eslint/commit/96cdd427db5f26c6403fba92b0f7ae4556c595ec"><code>96cdd42</code></a>
feat: report invalid signed numeric radix values in <code>radix</code>
rule (<a
href="https://redirect.github.com/eslint/eslint/issues/21030">#21030</a>)
(Pixel)</li>
</ul>
<h2>Bug Fixes</h2>
<ul>
<li><a
href="https://github.com/eslint/eslint/commit/3e7bf15e69e6d3a2c8832356bcc2e9903cc4eede"><code>3e7bf15</code></a>
fix: apply <code>ignoreClassesWithImplements</code> to class expressions
(<a
href="https://redirect.github.com/eslint/eslint/issues/21069">#21069</a>)
(Pixel)</li>
<li><a
href="https://github.com/eslint/eslint/commit/0d7d70cf013ec5bf1e4f5a0fcf7128bdf059b2d4"><code>0d7d70c</code></a>
fix: insert cause outside wrapping parens in preserve-caught-error (<a
href="https://redirect.github.com/eslint/eslint/issues/21062">#21062</a>)
(Mahin Anowar)</li>
<li><a
href="https://github.com/eslint/eslint/commit/75ec753226010867270787b412f3dae412e421e6"><code>75ec753</code></a>
fix: handle static template literals in <code>eqeqeq</code> rule (<a
href="https://redirect.github.com/eslint/eslint/issues/21058">#21058</a>)
(Pixel)</li>
<li><a
href="https://github.com/eslint/eslint/commit/b717a22e2408389a2ad40af66543e46390669eb6"><code>b717a22</code></a>
fix: prevent <code>eqeqeq</code> null option from reporting non-equality
operators (<a
href="https://redirect.github.com/eslint/eslint/issues/21057">#21057</a>)
(Pixel)</li>
<li><a
href="https://github.com/eslint/eslint/commit/e35b05f1961dcd691611bd68b6ff8a87072d6f76"><code>e35b05f</code></a>
fix: avoid <code>no-invalid-regexp</code> false positive for shadowed
RegExp (<a
href="https://redirect.github.com/eslint/eslint/issues/21051">#21051</a>)
(Pixel)</li>
<li><a
href="https://github.com/eslint/eslint/commit/a3172b69c7db63ea0321355543e3f527c7d8b76a"><code>a3172b6</code></a>
fix: avoid <code>no-control-regex</code> false positive for shadowed
RegExp (<a
href="https://redirect.github.com/eslint/eslint/issues/21050">#21050</a>)
(Pixel)</li>
<li><a
href="https://github.com/eslint/eslint/commit/d1f637eca27e523d613991c6bea5b8726b810e4c"><code>d1f637e</code></a>
fix: parenthesize sequence expression operands in no-implicit-coercion
(<a
href="https://redirect.github.com/eslint/eslint/issues/21045">#21045</a>)
(spokodev)</li>
<li><a
href="https://github.com/eslint/eslint/commit/8859bafb018e2b23b5110d52e491b69b94ad890a"><code>8859baf</code></a>
fix: avoid prefer-numeric-literals false positive for shadowed globals
(<a
href="https://redirect.github.com/eslint/eslint/issues/21047">#21047</a>)
(한국)</li>
<li><a
href="https://github.com/eslint/eslint/commit/a9e5961050676ef29dba9649dfcd7233d21760c7"><code>a9e5961</code></a>
fix: use-isnan false positive on shadowed NaN/Number (<a
href="https://redirect.github.com/eslint/eslint/issues/20958">#20958</a>)
(sethamus)</li>
<li><a
href="https://github.com/eslint/eslint/commit/8a240a76108f30f2476e61a925e4d32d378cce5e"><code>8a240a7</code></a>
fix: avoid false positives in <code>radix</code> rule for spread
arguments (<a
href="https://redirect.github.com/eslint/eslint/issues/21044">#21044</a>)
(Pixel)</li>
</ul>
<h2>Documentation</h2>
<ul>
<li><a
href="https://github.com/eslint/eslint/commit/c30d80801ca561bdf65c6c3eba4ee57dced278cf"><code>c30d808</code></a>
docs: Update README (GitHub Actions Bot)</li>
<li><a
href="https://github.com/eslint/eslint/commit/5139800b23e77fdd1a76664e1b36f478942eae55"><code>5139800</code></a>
docs: document ESLint migration codemods in v9 and v10 guides (<a
href="https://redirect.github.com/eslint/eslint/issues/20980">#20980</a>)
(Alex Bit)</li>
<li><a
href="https://github.com/eslint/eslint/commit/04174cbfaac1f9555400b5332d69056e6aab9cc6"><code>04174cb</code></a>
docs: Update README (GitHub Actions Bot)</li>
<li><a
href="https://github.com/eslint/eslint/commit/026e1304080512bcddfd2af3557fff86474ff949"><code>026e130</code></a>
docs: update semver policy for bug fixes (<a
href="https://redirect.github.com/eslint/eslint/issues/21048">#21048</a>)
(Milos Djermanovic)</li>
<li><a
href="https://github.com/eslint/eslint/commit/9d42fefb0e05ecd0b256c2ebed479fc868862f0c"><code>9d42fef</code></a>
docs: Update README (GitHub Actions Bot)</li>
<li><a
href="https://github.com/eslint/eslint/commit/b23015955c8d6e6516076190730f538c86927f26"><code>b230159</code></a>
docs: Update README (GitHub Actions Bot)</li>
<li><a
href="https://github.com/eslint/eslint/commit/0129972ba122e95bece6040fda44d20f6799246a"><code>0129972</code></a>
docs: correct <code>**/.js</code> glob to <code>**/*.js</code> in config
files guide (<a
href="https://redirect.github.com/eslint/eslint/issues/21036">#21036</a>)
(EduardF1)</li>
</ul>
<h2>Chores</h2>
<ul>
<li><a
href="https://github.com/eslint/eslint/commit/948937986f2aa3cccb27a4c80694a85e5b0a5f64"><code>9489379</code></a>
chore: update dependency <code>@​eslint/eslintrc</code> to ^3.3.6 (<a
href="https://redirect.github.com/eslint/eslint/issues/21076">#21076</a>)
(renovate[bot])</li>
<li><a
href="https://github.com/eslint/eslint/commit/81a4774a928211bd0c01a06a1517736ece594522"><code>81a4774</code></a>
chore: updates for v9.39.5 release (Jenkins)</li>
<li><a
href="https://github.com/eslint/eslint/commit/983541491393323d17717683f6a2e6dbef4bc3f4"><code>9835414</code></a>
chore: enable <code>$ExpectType</code> annotations in all TypeScript
files (<a
href="https://redirect.github.com/eslint/eslint/issues/21071">#21071</a>)
(Francesco Trotta)</li>
<li><a
href="https://github.com/eslint/eslint/commit/72adf6bd214c142b38aabb4b8f6f908079c5301a"><code>72adf6b</code></a>
chore: restrict <code>markdownlint-cli2</code> updates in renovate (<a
href="https://redirect.github.com/eslint/eslint/issues/21067">#21067</a>)
(lumir)</li>
<li><a
href="https://github.com/eslint/eslint/commit/833ec10fd702644e94334edd3cd2aa313174a958"><code>833ec10</code></a>
chore: update dependency prettier to v3.9.4 (<a
href="https://redirect.github.com/eslint/eslint/issues/21061">#21061</a>)
(renovate[bot])</li>
<li><a
href="https://github.com/eslint/eslint/commit/7ea106ddcb6af7db8f36d62a28dbaba94d33598e"><code>7ea106d</code></a>
chore: update ecosystem plugins (<a
href="https://redirect.github.com/eslint/eslint/issues/21059">#21059</a>)
(ESLint Bot)</li>
<li><a
href="https://github.com/eslint/eslint/commit/8fb550e7c257fbdc4108fc8e63db43c1d914d472"><code>8fb550e</code></a>
chore: add prettier update commit to <code>.git-blame-ignore-revs</code>
(<a
href="https://redirect.github.com/eslint/eslint/issues/21056">#21056</a>)
(lumir)</li>
<li><a
href="https://github.com/eslint/eslint/commit/e4e11668ead6b950328c71e894563a13ef72bf84"><code>e4e1166</code></a>
chore: update dependency prettier to v3.9.1 (<a
href="https://redirect.github.com/eslint/eslint/issues/21055">#21055</a>)
(renovate[bot])</li>
<li><a
href="https://github.com/eslint/eslint/commit/0493f53f3fcec4ed69a1808dffa153dcff1380d3"><code>0493f53</code></a>
chore: update prettier to v3.9.0 (<a
href="https://redirect.github.com/eslint/eslint/issues/21054">#21054</a>)
(Pixel)</li>
<li><a
href="https://github.com/eslint/eslint/commit/1056a996c657b377dc16a58c28890d2940394408"><code>1056a99</code></a>
chore: update dependency prettier to v3.8.5 (<a
href="https://redirect.github.com/eslint/eslint/issues/21049">#21049</a>)
(renovate[bot])</li>
<li><a
href="https://github.com/eslint/eslint/commit/4d4155df4a21d711e8dd21a467de085e770bad35"><code>4d4155d</code></a>
ci: run ecosystem tests on pull requests (<a
href="https://redirect.github.com/eslint/eslint/issues/21027">#21027</a>)
(sethamus)</li>
<li><a
href="https://github.com/eslint/eslint/commit/993539fd45d3d01aad3fbc13963e2245fdf7ee0d"><code>993539f</code></a>
chore: update dependency <code>@​eslint/json</code> to ^2.0.1 (<a
href="https://redirect.github.com/eslint/eslint/issues/21042">#21042</a>)
(renovate[bot])</li>
<li><a
href="https://github.com/eslint/eslint/commit/53f8b69c15af7398c989071ee4788b88edd48bdd"><code>53f8b69</code></a>
test: add error locations to <code>no-constant-binary-expression</code>
(<a
href="https://redirect.github.com/eslint/eslint/issues/21039">#21039</a>)
(lumir)</li>
<li><a
href="https://github.com/eslint/eslint/commit/5ab71d52f9d860df04e626fcd819a36257f21f9e"><code>5ab71d5</code></a>
refactor: clean up radix rule internals (<a
href="https://redirect.github.com/eslint/eslint/issues/21015">#21015</a>)
(Pixel)</li>
<li><a
href="https://github.com/eslint/eslint/commit/a80a9a461478838930c5545f840473840b26b0e6"><code>a80a9a4</code></a>
chore: update ecosystem plugins (<a
href="https://redirect.github.com/eslint/eslint/issues/21035">#21035</a>)
(ESLint Bot)</li>
<li><a
href="https://github.com/eslint/eslint/commit/7c9a02933cf1c231237babb8a7a32150ab72e263"><code>7c9a029</code></a>
ci: add Node.js 26 to CI (<a
href="https://redirect.github.com/eslint/eslint/issues/20847">#20847</a>)
(lumir)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/eslint/eslint/commit/fabd99bb74eedcb950df51ec3280414ff1d68d38"><code>fabd99b</code></a>
10.7.0</li>
<li><a
href="https://github.com/eslint/eslint/commit/37c5e75949ff2ae1f0085f547e35ce2bdf9d524d"><code>37c5e75</code></a>
Build: changelog update for 10.7.0</li>
<li><a
href="https://github.com/eslint/eslint/commit/948937986f2aa3cccb27a4c80694a85e5b0a5f64"><code>9489379</code></a>
chore: update dependency <code>@​eslint/eslintrc</code> to ^3.3.6 (<a
href="https://redirect.github.com/eslint/eslint/issues/21076">#21076</a>)</li>
<li><a
href="https://github.com/eslint/eslint/commit/81a4774a928211bd0c01a06a1517736ece594522"><code>81a4774</code></a>
chore: updates for v9.39.5 release</li>
<li><a
href="https://github.com/eslint/eslint/commit/3e7bf15e69e6d3a2c8832356bcc2e9903cc4eede"><code>3e7bf15</code></a>
fix: apply <code>ignoreClassesWithImplements</code> to class expressions
(<a
href="https://redirect.github.com/eslint/eslint/issues/21069">#21069</a>)</li>
<li><a
href="https://github.com/eslint/eslint/commit/983541491393323d17717683f6a2e6dbef4bc3f4"><code>9835414</code></a>
chore: enable <code>$ExpectType</code> annotations in all TypeScript
files (<a
href="https://redirect.github.com/eslint/eslint/issues/21071">#21071</a>)</li>
<li><a
href="https://github.com/eslint/eslint/commit/cf2a9bf2d982642f760370c15f1196f4658f4e27"><code>cf2a9bf</code></a>
feat: add errorClassNames option to preserve-caught-error rule (<a
href="https://redirect.github.com/eslint/eslint/issues/21032">#21032</a>)</li>
<li><a
href="https://github.com/eslint/eslint/commit/c30d80801ca561bdf65c6c3eba4ee57dced278cf"><code>c30d808</code></a>
docs: Update README</li>
<li><a
href="https://github.com/eslint/eslint/commit/f8b873aae53610b80b5c1005606716e05ed3b91f"><code>f8b873a</code></a>
feat: max-nested-callbacks option for constructor callbacks (<a
href="https://redirect.github.com/eslint/eslint/issues/21063">#21063</a>)</li>
<li><a
href="https://github.com/eslint/eslint/commit/72adf6bd214c142b38aabb4b8f6f908079c5301a"><code>72adf6b</code></a>
chore: restrict <code>markdownlint-cli2</code> updates in renovate (<a
href="https://redirect.github.com/eslint/eslint/issues/21067">#21067</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/eslint/eslint/compare/v10.6.0...v10.7.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=eslint&package-manager=npm_and_yarn&previous-version=10.6.0&new-version=10.7.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 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>
2026-07-14 13:52:48 +00:00
Harel M cf54b6fdeb Update style-spec package to 26.1 (#2005)
## Launch Checklist

Update style-spec package to version 26.1
This should solve a problem related to expression validation.

 - [x] Briefly describe the changes in this PR.
 - [x] Link to related issues.
2026-07-14 14:14:28 +03:00
dependabot[bot] e5e09e846a chore(deps): Bump github/codeql-action/init from 4.36.3 to 4.37.0 (#2002)
Bumps
[github/codeql-action/init](https://github.com/github/codeql-action)
from 4.36.3 to 4.37.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/github/codeql-action/releases">github/codeql-action/init's
releases</a>.</em></p>
<blockquote>
<h2>v4.37.0</h2>
<ul>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.0">2.26.0</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/3995">#3995</a></li>
<li>In addition to the existing input format, the
<code>config-file</code> input for the <code>codeql-action/init</code>
step will soon support a new <code>[owner/]repo[@ref][:path]</code>
format. All components except the repository name are optional. If
omitted, <code>owner</code> defaults to the same owner as the repository
the analysis is running for, <code>ref</code> to <code>main</code>, and
<code>path</code> to <code>.github/codeql-action.yaml</code>. Support
for this format ships in this version of the CodeQL Action, but will
only be enabled over the coming weeks. <a
href="https://redirect.github.com/github/codeql-action/pull/3973">#3973</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/github/codeql-action/blob/main/CHANGELOG.md">github/codeql-action/init's
changelog</a>.</em></p>
<blockquote>
<h1>CodeQL Action Changelog</h1>
<p>See the <a
href="https://github.com/github/codeql-action/releases">releases
page</a> for the relevant changes to the CodeQL CLI and language
packs.</p>
<h2>[UNRELEASED]</h2>
<ul>
<li><em>Upcoming breaking change</em>: Add a deprecation warning for
customers using CodeQL version 2.20.6 and earlier. These versions of
CodeQL were discontinued on 1 July 2026 alongside GitHub Enterprise
Server 3.16, and will be unsupported by the next minor release of the
CodeQL Action. <a
href="https://redirect.github.com/github/codeql-action/pull/3956">#3956</a></li>
</ul>
<h2>4.37.0 - 08 Jul 2026</h2>
<ul>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.0">2.26.0</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/3995">#3995</a></li>
<li>In addition to the existing input format, the
<code>config-file</code> input for the <code>codeql-action/init</code>
step will soon support a new <code>[owner/]repo[@ref][:path]</code>
format. All components except the repository name are optional. If
omitted, <code>owner</code> defaults to the same owner as the repository
the analysis is running for, <code>ref</code> to <code>main</code>, and
<code>path</code> to <code>.github/codeql-action.yaml</code>. Support
for this format ships in this version of the CodeQL Action, but will
only be enabled over the coming weeks. <a
href="https://redirect.github.com/github/codeql-action/pull/3973">#3973</a></li>
</ul>
<h2>4.36.3 - 01 Jul 2026</h2>
<p>No user facing changes.</p>
<h2>4.36.2 - 04 Jun 2026</h2>
<ul>
<li>Cache CodeQL CLI version information across Actions steps. <a
href="https://redirect.github.com/github/codeql-action/pull/3943">#3943</a></li>
<li>Reduce requests while waiting for analysis processing by using
exponential backoff when polling SARIF processing status. <a
href="https://redirect.github.com/github/codeql-action/pull/3937">#3937</a></li>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.25.6">2.25.6</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/3948">#3948</a></li>
</ul>
<h2>4.36.1 - 02 Jun 2026</h2>
<p>No user facing changes.</p>
<h2>4.36.0 - 22 May 2026</h2>
<ul>
<li><em>Breaking change</em>: Bump the minimum required CodeQL bundle
version to 2.19.4. <a
href="https://redirect.github.com/github/codeql-action/pull/3894">#3894</a></li>
<li>Add support for SHA-256 Git object IDs. <a
href="https://redirect.github.com/github/codeql-action/pull/3893">#3893</a></li>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.25.5">2.25.5</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/3926">#3926</a></li>
</ul>
<h2>4.35.5 - 15 May 2026</h2>
<ul>
<li>We have improved how the JavaScript bundles for the CodeQL Action
are generated to avoid duplication across bundles and reduce the size of
the repository by around 70%. This should have no effect on the runtime
behaviour of the CodeQL Action. <a
href="https://redirect.github.com/github/codeql-action/pull/3899">#3899</a></li>
<li>For performance and accuracy reasons, <a
href="https://redirect.github.com/github/roadmap/issues/1158">improved
incremental analysis</a> will now only be enabled on a pull request when
diff-informed analysis is also enabled for that run. If diff-informed
analysis is unavailable (for example, because the PR diff ranges could
not be computed), the action will fall back to a full analysis. <a
href="https://redirect.github.com/github/codeql-action/pull/3791">#3791</a></li>
<li>If multiple inputs are provided for the GitHub-internal
<code>analysis-kinds</code> input, only <code>code-scanning</code> will
be enabled. The <code>analysis-kinds</code> input is experimental, for
GitHub-internal use only, and may change without notice at any time. <a
href="https://redirect.github.com/github/codeql-action/pull/3892">#3892</a></li>
<li>Added an experimental change which, when running a Code Scanning
analysis for a PR with <a
href="https://redirect.github.com/github/roadmap/issues/1158">improved
incremental analysis</a> enabled, prefers CodeQL CLI versions that have
a cached overlay-base database for the configured languages. This speeds
up analysis for a repository when there is not yet a cached overlay-base
database for the latest CLI version. We expect to roll this change out
to everyone in May. <a
href="https://redirect.github.com/github/codeql-action/pull/3880">#3880</a></li>
</ul>
<h2>4.35.4 - 07 May 2026</h2>
<ul>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.25.4">2.25.4</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/3881">#3881</a></li>
</ul>
<h2>4.35.3 - 01 May 2026</h2>
<ul>
<li><em>Upcoming breaking change</em>: Add a deprecation warning for
customers using CodeQL version 2.19.3 and earlier. These versions of
CodeQL were discontinued on 9 April 2026 alongside GitHub Enterprise
Server 3.15, and will be unsupported by the next minor release of the
CodeQL Action. <a
href="https://redirect.github.com/github/codeql-action/pull/3837">#3837</a></li>
<li>Configurations for private registries that use Cloudsmith or GCP
OIDC are now accepted. <a
href="https://redirect.github.com/github/codeql-action/pull/3850">#3850</a></li>
<li>Best-effort connection tests for private registries now use
<code>GET</code> requests instead of <code>HEAD</code> for better
compatibility with various registry implementations. For NuGet feeds,
the test is now always performed against the service index. <a
href="https://redirect.github.com/github/codeql-action/pull/3853">#3853</a></li>
<li>Fixed a bug where two diagnostics produced within the same
millisecond could overwrite each other on disk, causing one of them to
be lost. <a
href="https://redirect.github.com/github/codeql-action/pull/3852">#3852</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/github/codeql-action/commit/99df26d4f13ea111d4ec1a7dddef6063f76b97e9"><code>99df26d</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/3996">#3996</a>
from github/update-v4.37.0-c7c896d71</li>
<li><a
href="https://github.com/github/codeql-action/commit/31c27074fda95256cda077009907f8a6022dd7c0"><code>31c2707</code></a>
Add changenote for <a
href="https://redirect.github.com/github/codeql-action/issues/3973">#3973</a></li>
<li><a
href="https://github.com/github/codeql-action/commit/72df2181aac054d1f4b44264399d2aac12cf11c6"><code>72df218</code></a>
Update changelog for v4.37.0</li>
<li><a
href="https://github.com/github/codeql-action/commit/c7c896d71b3055d36f2aff93b16bcc6c69923b91"><code>c7c896d</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/3995">#3995</a>
from github/update-bundle/codeql-bundle-v2.26.0</li>
<li><a
href="https://github.com/github/codeql-action/commit/3f34ff0ea3f5153c96071437b7cbf71ea3757146"><code>3f34ff0</code></a>
Add changelog note</li>
<li><a
href="https://github.com/github/codeql-action/commit/43bec09f1dc368b430cab4b5d69799bc904079d1"><code>43bec09</code></a>
Update default bundle to codeql-bundle-v2.26.0</li>
<li><a
href="https://github.com/github/codeql-action/commit/f58f0d11ebf5dedd870fab2f999275f7602cfa46"><code>f58f0d1</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/3973">#3973</a>
from github/mbg/repo-props/config-file-shorthands</li>
<li><a
href="https://github.com/github/codeql-action/commit/7dc37cbb5b3e37f0e1cd1f18b61e0ea849898fb8"><code>7dc37cb</code></a>
Merge remote-tracking branch 'origin/main' into
mbg/repo-props/config-file-sh...</li>
<li><a
href="https://github.com/github/codeql-action/commit/8e22350a7e28c34c82a5a499fc241923301c2c4f"><code>8e22350</code></a>
Thread <code>ActionState</code> to <code>initConfig</code></li>
<li><a
href="https://github.com/github/codeql-action/commit/69c9e8c7d918cf2fee13b8b72fdde15883ff155b"><code>69c9e8c</code></a>
Mark some <code>status-report</code> imports as <code>type</code>-only
to avoid circular dependencies</li>
<li>Additional commits viewable in <a
href="https://github.com/github/codeql-action/compare/54f647b7e1bb85c95cddabcd46b0c578ec92bc1a...99df26d4f13ea111d4ec1a7dddef6063f76b97e9">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github/codeql-action/init&package-manager=github_actions&previous-version=4.36.3&new-version=4.37.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 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>
2026-07-13 13:53:39 +00:00
dependabot[bot] 43bb23801c chore(deps): Bump i18next from 26.3.4 to 26.3.6 (#2001)
Bumps [i18next](https://github.com/i18next/i18next) from 26.3.4 to
26.3.6.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/i18next/i18next/releases">i18next's
releases</a>.</em></p>
<blockquote>
<h2>v26.3.6</h2>
<ul>
<li>fix: allow TypeScript 7 in the optional <code>typescript</code> peer
dependency range (<code>^5 || ^6 || ^7</code>). With
<code>typescript@7.0.2</code> in a project, <code>npm install</code>
failed with an <code>ERESOLVE</code> peer conflict. The published types
are TS7-compatible as-is: every <code>test/typescript</code> suite
produces identical results under 6.0 and 7.0.2. Reported in <a
href="https://redirect.github.com/i18next/react-i18next/issues/1927">react-i18next#1927</a>,
thanks <a
href="https://github.com/andikapradanaarif"><code>@​andikapradanaarif</code></a>.</li>
</ul>
<h2>v26.3.5</h2>
<ul>
<li>fix: <code>$t()</code> nesting options blocks that span multiple
lines are now parsed. <code>nest()</code> decided where the nested key
ends by testing <code>match[1]</code> with <code>/{.*}/</code>, whose
dot does not cross line breaks — so a <code>$t(key, { ... })</code>
options object containing a newline was treated as having no options,
mis-split as formatters, and the nested lookup ran without its options
(placeholders stayed unresolved). The nesting regexp itself already
matches newlines inside <code>$t(...)</code>; adding the <code>s</code>
(dotAll) flag makes multiline options behave like the single-line form.
Thanks <a href="https://github.com/spokodev"><code>@​spokodev</code></a>
(<a
href="https://redirect.github.com/i18next/i18next/pull/2440">#2440</a>).</li>
<li>fix: <code>getUsedParamsDetails</code> (the <code>returnDetails:
true</code> path) no longer mutates the passed <code>replace</code>
object. It wrote <code>count</code> straight onto
<code>options.replace</code> so the returned <code>usedParams</code>
would include it — a caller reusing one <code>replace</code> object
across <code>t()</code> calls then carried a stale <code>count</code>
into later interpolations (e.g. a previous call's <code>count: 5</code>
rendered instead of the current call's value). The details are now built
from a copy; <code>usedParams</code> still includes <code>count</code>.
Thanks <a href="https://github.com/spokodev"><code>@​spokodev</code></a>
(<a
href="https://redirect.github.com/i18next/i18next/pull/2441">#2441</a>).</li>
<li>fix: with the default <code>skipOnVariables: true</code> +
<code>escapeValue: true</code>, a <code>{{placeholder}}</code> carried
inside an interpolated value now stays literal even when the value
contains escapable characters. The skip logic advanced the regex
<code>lastIndex</code> by the raw value length, but the escaped text
written into the string is longer, so <code>lastIndex</code> landed
inside the inserted value and a trailing <code>{{placeholder}}</code> in
it got interpolated — leaking another in-scope variable that should have
stayed literal (values without escapable characters were already skipped
correctly). The advance now uses the escaped length that is actually
written, and the regex-safe <code>$</code>-doubling is applied only at
the <code>String.replace</code> call so it can't distort the length
arithmetic. Thanks <a
href="https://github.com/spokodev"><code>@​spokodev</code></a> (<a
href="https://redirect.github.com/i18next/i18next/pull/2442">#2442</a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/i18next/i18next/blob/master/CHANGELOG.md">i18next's
changelog</a>.</em></p>
<blockquote>
<h2>26.3.6</h2>
<ul>
<li>fix: allow TypeScript 7 in the optional <code>typescript</code> peer
dependency range (<code>^5 || ^6 || ^7</code>). With
<code>typescript@7.0.2</code> in a project, <code>npm install</code>
failed with an <code>ERESOLVE</code> peer conflict. The published types
are TS7-compatible as-is: every <code>test/typescript</code> suite
produces identical results under 6.0 and 7.0.2. Reported in <a
href="https://redirect.github.com/i18next/react-i18next/issues/1927">react-i18next#1927</a>,
thanks <a
href="https://github.com/andikapradanaarif"><code>@​andikapradanaarif</code></a>.</li>
</ul>
<h2>26.3.5</h2>
<ul>
<li>fix: <code>$t()</code> nesting options blocks that span multiple
lines are now parsed. <code>nest()</code> decided where the nested key
ends by testing <code>match[1]</code> with <code>/{.*}/</code>, whose
dot does not cross line breaks — so a <code>$t(key, { ... })</code>
options object containing a newline was treated as having no options,
mis-split as formatters, and the nested lookup ran without its options
(placeholders stayed unresolved). The nesting regexp itself already
matches newlines inside <code>$t(...)</code>; adding the <code>s</code>
(dotAll) flag makes multiline options behave like the single-line form.
Thanks <a href="https://github.com/spokodev"><code>@​spokodev</code></a>
(<a
href="https://redirect.github.com/i18next/i18next/pull/2440">#2440</a>).</li>
<li>fix: <code>getUsedParamsDetails</code> (the <code>returnDetails:
true</code> path) no longer mutates the passed <code>replace</code>
object. It wrote <code>count</code> straight onto
<code>options.replace</code> so the returned <code>usedParams</code>
would include it — a caller reusing one <code>replace</code> object
across <code>t()</code> calls then carried a stale <code>count</code>
into later interpolations (e.g. a previous call's <code>count: 5</code>
rendered instead of the current call's value). The details are now built
from a copy; <code>usedParams</code> still includes <code>count</code>.
Thanks <a href="https://github.com/spokodev"><code>@​spokodev</code></a>
(<a
href="https://redirect.github.com/i18next/i18next/pull/2441">#2441</a>).</li>
<li>fix: with the default <code>skipOnVariables: true</code> +
<code>escapeValue: true</code>, a <code>{{placeholder}}</code> carried
inside an interpolated value now stays literal even when the value
contains escapable characters. The skip logic advanced the regex
<code>lastIndex</code> by the raw value length, but the escaped text
written into the string is longer, so <code>lastIndex</code> landed
inside the inserted value and a trailing <code>{{placeholder}}</code> in
it got interpolated — leaking another in-scope variable that should have
stayed literal (values without escapable characters were already skipped
correctly). The advance now uses the escaped length that is actually
written, and the regex-safe <code>$</code>-doubling is applied only at
the <code>String.replace</code> call so it can't distort the length
arithmetic. Thanks <a
href="https://github.com/spokodev"><code>@​spokodev</code></a> (<a
href="https://redirect.github.com/i18next/i18next/pull/2442">#2442</a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/i18next/i18next/commit/e1c60d4dd28a16f91be7f55b3685ffcf9760619b"><code>e1c60d4</code></a>
26.3.6</li>
<li><a
href="https://github.com/i18next/i18next/commit/04da43e08cb799f2e6f9a5578f3674fe2ef49595"><code>04da43e</code></a>
fix: allow typescript 7 in optional peerDependencies range
(react-i18next#1927)</li>
<li><a
href="https://github.com/i18next/i18next/commit/8eed4accc6c69398e9e25f79b617249fd3f7b1ec"><code>8eed4ac</code></a>
build</li>
<li><a
href="https://github.com/i18next/i18next/commit/573ae735682a082ad5a581dea3f2d5f517c53772"><code>573ae73</code></a>
26.3.5</li>
<li><a
href="https://github.com/i18next/i18next/commit/cc54b05b5cab9bf76cf66557c222c978bbb3c082"><code>cc54b05</code></a>
docs(changelog): 26.3.5 — multiline $t() options, replace mutation,
escaped-l...</li>
<li><a
href="https://github.com/i18next/i18next/commit/3180d67291a8fff10950f79660420c106c047f94"><code>3180d67</code></a>
fix: skip interpolation of placeholders inside escaped values (<a
href="https://redirect.github.com/i18next/i18next/issues/2442">#2442</a>)</li>
<li><a
href="https://github.com/i18next/i18next/commit/d16f5a2da748082155007ba0677d32b877b1102e"><code>d16f5a2</code></a>
fix: stop mutating the passed replace object when returning details (<a
href="https://redirect.github.com/i18next/i18next/issues/2441">#2441</a>)</li>
<li><a
href="https://github.com/i18next/i18next/commit/bed56c115979de52bd85c46d9b7a7b13361bf736"><code>bed56c1</code></a>
fix: parse $t() nesting options block that spans multiple lines (<a
href="https://redirect.github.com/i18next/i18next/issues/2440">#2440</a>)</li>
<li><a
href="https://github.com/i18next/i18next/commit/c19e45864f3abbbe90e30be44cc131f2248394bf"><code>c19e458</code></a>
docs(changelog): link GHSA advisory for deepExtend fix</li>
<li><a
href="https://github.com/i18next/i18next/commit/7bb87d09f907b4395ed0825638d7d2c4356f7a89"><code>7bb87d0</code></a>
docs(changelog): reference security advisory for deepExtend fix</li>
<li>Additional commits viewable in <a
href="https://github.com/i18next/i18next/compare/v26.3.4...v26.3.6">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=i18next&package-manager=npm_and_yarn&previous-version=26.3.4&new-version=26.3.6)](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 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>
2026-07-13 13:52:30 +00:00
64 changed files with 5711 additions and 5629 deletions
+11 -11
View File
@@ -21,9 +21,9 @@ jobs:
os: [ ubuntu-latest, windows-latest, macos-latest ] os: [ ubuntu-latest, windows-latest, macos-latest ]
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: { persist-credentials: false } with: { persist-credentials: false }
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
- run: npm ci - run: npm ci
@@ -41,9 +41,9 @@ jobs:
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }} if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: { persist-credentials: false } with: { persist-credentials: false }
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
- run: npm ci - run: npm ci
@@ -56,7 +56,7 @@ jobs:
# Build and upload desktop CLI artifacts # Build and upload desktop CLI artifacts
- name: Set up Go - name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with: with:
go-version: ^1.23.x go-version: ^1.23.x
cache-dependency-path: desktop/go.sum cache-dependency-path: desktop/go.sum
@@ -90,9 +90,9 @@ jobs:
contents: read contents: read
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: { persist-credentials: false } with: { persist-credentials: false }
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
- run: npm ci - run: npm ci
@@ -111,9 +111,9 @@ jobs:
contents: read contents: read
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: { persist-credentials: false } with: { persist-credentials: false }
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
- run: npm ci - run: npm ci
@@ -139,9 +139,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: { persist-credentials: false } with: { persist-credentials: false }
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
- run: npm ci - run: npm ci
+3 -3
View File
@@ -38,11 +38,11 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@@ -67,4 +67,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 uses: github/codeql-action/analyze@e0647621c2984b5ed2f768cb892365bf2a616ad1 # v4.37.2
+2 -2
View File
@@ -16,13 +16,13 @@ jobs:
run: run:
shell: bash shell: bash
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
fetch-depth: 0 fetch-depth: 0
ref: main ref: main
- name: Use Node.js from nvmrc - name: Use Node.js from nvmrc
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
+3 -3
View File
@@ -12,11 +12,11 @@ jobs:
contents: write contents: write
if: ${{ github.event_name == 'push' }} if: ${{ github.event_name == 'push' }}
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: { persist-credentials: false } with: { persist-credentials: false }
- name: Use Node.js from nvmrc - name: Use Node.js from nvmrc
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
@@ -50,6 +50,6 @@ jobs:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- run: docker build -t ghcr.io/maplibre/maputnik:main . - run: docker build -t ghcr.io/maplibre/maputnik:main .
- run: docker push ghcr.io/maplibre/maputnik:main - run: docker push ghcr.io/maplibre/maputnik:main
+5 -5
View File
@@ -15,14 +15,14 @@ jobs:
permissions: permissions:
contents: read contents: read
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
fetch-depth: 0 fetch-depth: 0
ref: main ref: main
persist-credentials: false persist-credentials: false
- name: Use Node.js from nvmrc - name: Use Node.js from nvmrc
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
@@ -42,19 +42,19 @@ jobs:
run: run:
shell: bash shell: bash
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
fetch-depth: 0 fetch-depth: 0
ref: main ref: main
- name: Use Node.js from nvmrc - name: Use Node.js from nvmrc
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
registry-url: "https://registry.npmjs.org" registry-url: "https://registry.npmjs.org"
- name: Set up Go for desktop build - name: Set up Go for desktop build
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with: with:
go-version: ^1.23.x go-version: ^1.23.x
cache-dependency-path: desktop/go.sum cache-dependency-path: desktop/go.sum
+2 -2
View File
@@ -32,10 +32,10 @@ export default defineConfig({
"@stylistic": stylisticTs "@stylistic": stylisticTs
}, },
rules: { rules: {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"react-refresh/only-export-components": [ "react-refresh/only-export-components": [
"warn", "warn",
// Many components are exported as withTranslation()(Component); without
// this the rule cannot tell the HOC's result is still a component.
{ allowConstantExport: true, extraHOCs: ["withTranslation"] } { allowConstantExport: true, extraHOCs: ["withTranslation"] }
], ],
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
+699 -941
View File
File diff suppressed because it is too large Load Diff
+9 -9
View File
@@ -37,7 +37,7 @@
"@mapbox/mapbox-gl-rtl-text": "^0.4.0", "@mapbox/mapbox-gl-rtl-text": "^0.4.0",
"@maplibre/maplibre-gl-geocoder": "^1.9.4", "@maplibre/maplibre-gl-geocoder": "^1.9.4",
"@maplibre/maplibre-gl-inspect": "^1.8.2", "@maplibre/maplibre-gl-inspect": "^1.8.2",
"@maplibre/maplibre-gl-style-spec": "^25.0.2", "@maplibre/maplibre-gl-style-spec": "^26.2.1",
"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",
@@ -47,7 +47,7 @@
"downshift": "^9.4.0", "downshift": "^9.4.0",
"events": "^3.3.0", "events": "^3.3.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"i18next": "^26.3.4", "i18next": "^26.3.6",
"i18next-browser-languagedetector": "^8.2.1", "i18next-browser-languagedetector": "^8.2.1",
"i18next-resources-to-backend": "^1.2.1", "i18next-resources-to-backend": "^1.2.1",
"json-stringify-pretty-compact": "^4.0.0", "json-stringify-pretty-compact": "^4.0.0",
@@ -126,24 +126,24 @@
"@vitejs/plugin-react": "5.2", "@vitejs/plugin-react": "5.2",
"@vitest/coverage-v8": "^4.1.10", "@vitest/coverage-v8": "^4.1.10",
"cors": "^2.8.6", "cors": "^2.8.6",
"eslint": "^10.6.0", "eslint": "^10.7.0",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.3", "eslint-plugin-react-refresh": "^0.5.3",
"i18next-cli": "^1.65.0", "i18next-cli": "^1.66.2",
"istanbul": "^0.4.5", "istanbul": "^0.4.5",
"istanbul-lib-coverage": "^3.2.2", "istanbul-lib-coverage": "^3.2.2",
"nyc": "^18.0.0", "nyc": "^18.0.0",
"postcss": "^8.5.16", "postcss": "^8.5.21",
"react-hot-loader": "^4.13.1", "react-hot-loader": "^4.13.1",
"sass": "^1.101.0", "sass": "^1.101.3",
"stylelint": "^17.14.0", "stylelint": "^17.14.1",
"stylelint-config-recommended-scss": "^17.0.1", "stylelint-config-recommended-scss": "^17.0.1",
"stylelint-scss": "^7.2.0", "stylelint-scss": "^7.2.0",
"typescript": "^6.0.3", "typescript": "^6.0.3",
"typescript-eslint": "^8.62.1", "typescript-eslint": "^8.65.0",
"uuid": "^14.0.1", "uuid": "^14.0.1",
"vite": "^7.3.2", "vite": "^8.1.5",
"vite-plugin-istanbul": "^9.0.1", "vite-plugin-istanbul": "^9.0.1",
"vitest": "^4.1.10" "vitest": "^4.1.10"
} }
+577 -620
View File
File diff suppressed because it is too large Load Diff
+31 -28
View File
@@ -13,38 +13,41 @@ type AppLayoutInternalProps = {
modals?: React.ReactNode modals?: React.ReactNode
} & WithTranslation; } & WithTranslation;
const AppLayoutInternal: React.FC<AppLayoutInternalProps> = (props) => { class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
document.body.dir = props.i18n.dir();
return <IconContext.Provider value={{size: "14px"}}> render() {
<div className="maputnik-layout"> document.body.dir = this.props.i18n.dir();
{props.toolbar}
<div className="maputnik-layout-main"> return <IconContext.Provider value={{size: "14px"}}>
{props.codeEditor && <div className="maputnik-layout-code-editor"> <div className="maputnik-layout">
<ScrollContainer> {this.props.toolbar}
{props.codeEditor} <div className="maputnik-layout-main">
</ScrollContainer> {this.props.codeEditor && <div className="maputnik-layout-code-editor">
</div>
}
{!props.codeEditor && <>
<div className="maputnik-layout-list">
{props.layerList}
</div>
<div className="maputnik-layout-drawer">
<ScrollContainer> <ScrollContainer>
{props.layerEditor} {this.props.codeEditor}
</ScrollContainer> </ScrollContainer>
</div> </div>
</>} }
{props.map} {!this.props.codeEditor && <>
<div className="maputnik-layout-list">
{this.props.layerList}
</div>
<div className="maputnik-layout-drawer">
<ScrollContainer>
{this.props.layerEditor}
</ScrollContainer>
</div>
</>}
{this.props.map}
</div>
{this.props.bottom && <div className="maputnik-layout-bottom">
{this.props.bottom}
</div>
}
{this.props.modals}
</div> </div>
{props.bottom && <div className="maputnik-layout-bottom"> </IconContext.Provider>;
{props.bottom} }
</div> }
}
{props.modals}
</div>
</IconContext.Provider>;
};
export const AppLayout = withTranslation()(AppLayoutInternal); export const AppLayout = withTranslation()(AppLayoutInternal);
+45 -42
View File
@@ -13,49 +13,52 @@ type AppMessagePanelInternalProps = {
selectedLayerIndex?: number selectedLayerIndex?: number
} & WithTranslation; } & WithTranslation;
const AppMessagePanelInternal: React.FC<AppMessagePanelInternalProps> = ({ class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalProps> {
onLayerSelect = () => { }, static defaultProps = {
...props onLayerSelect: () => { },
}) => { };
const { t, selectedLayerIndex } = props;
const errors = props.errors?.map((error, idx) => {
let content;
if (error.parsed && error.parsed.type === "layer") {
const { parsed } = error;
const layerId = props.mapStyle?.layers[parsed.data.index].id;
content = (
<>
{t("Layer")} <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
{selectedLayerIndex !== parsed.data.index &&
<>
&nbsp;&mdash;&nbsp;
<button
className="maputnik-message-panel__switch-button"
onClick={() => onLayerSelect!(parsed.data.index)}
>
{t("switch to layer")}
</button>
</>
}
</>
);
}
else {
content = error.message;
}
return <p key={"error-" + idx} className="maputnik-message-panel-error">
{content}
</p>;
});
const infos = props.infos?.map((m, i) => { render() {
return <p key={"info-" + i}>{m}</p>; const { t, selectedLayerIndex } = this.props;
}); const errors = this.props.errors?.map((error, idx) => {
let content;
if (error.parsed && error.parsed.type === "layer") {
const { parsed } = error;
const layerId = this.props.mapStyle?.layers[parsed.data.index].id;
content = (
<>
{t("Layer")} <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
{selectedLayerIndex !== parsed.data.index &&
<>
&nbsp;&mdash;&nbsp;
<button
className="maputnik-message-panel__switch-button"
onClick={() => this.props.onLayerSelect!(parsed.data.index)}
>
{t("switch to layer")}
</button>
</>
}
</>
);
}
else {
content = error.message;
}
return <p key={"error-" + idx} className="maputnik-message-panel-error">
{content}
</p>;
});
return <div className="maputnik-message-panel"> const infos = this.props.infos?.map((m, i) => {
{errors} return <p key={"info-" + i}>{m}</p>;
{infos} });
</div>;
}; return <div className="maputnik-message-panel">
{errors}
{infos}
</div>;
}
}
export const AppMessagePanel = withTranslation()(AppMessagePanelInternal); export const AppMessagePanel = withTranslation()(AppMessagePanelInternal);
+211 -191
View File
@@ -31,9 +31,11 @@ type IconTextProps = {
}; };
const IconText: React.FC<IconTextProps> = (props) => { class IconText extends React.Component<IconTextProps> {
return <span className="maputnik-icon-text">{props.children}</span>; render() {
}; return <span className="maputnik-icon-text">{this.props.children}</span>;
}
}
type ToolbarLinkProps = { type ToolbarLinkProps = {
className?: string className?: string
@@ -41,31 +43,35 @@ type ToolbarLinkProps = {
href?: string href?: string
}; };
const ToolbarLink: React.FC<ToolbarLinkProps> = (props) => { class ToolbarLink extends React.Component<ToolbarLinkProps> {
return <a render() {
className={classnames("maputnik-toolbar-link", props.className)} return <a
href={props.href} className={classnames("maputnik-toolbar-link", this.props.className)}
rel="noopener noreferrer" href={this.props.href}
target="_blank" rel="noopener noreferrer"
data-wd-key="toolbar:link" target="_blank"
> data-wd-key="toolbar:link"
{props.children} >
</a>; {this.props.children}
}; </a>;
}
}
type ToolbarSelectProps = { type ToolbarSelectProps = {
children?: React.ReactNode children?: React.ReactNode
wdKey?: string wdKey?: string
}; };
const ToolbarSelect: React.FC<ToolbarSelectProps> = (props) => { class ToolbarSelect extends React.Component<ToolbarSelectProps> {
return <div render() {
className='maputnik-toolbar-select' return <div
data-wd-key={props.wdKey} className='maputnik-toolbar-select'
> data-wd-key={this.props.wdKey}
{props.children} >
</div>; {this.props.children}
}; </div>;
}
}
type ToolbarActionProps = { type ToolbarActionProps = {
children?: React.ReactNode children?: React.ReactNode
@@ -73,15 +79,17 @@ type ToolbarActionProps = {
wdKey?: string wdKey?: string
}; };
const ToolbarAction: React.FC<ToolbarActionProps> = (props) => { class ToolbarAction extends React.Component<ToolbarActionProps> {
return <button render() {
className='maputnik-toolbar-action' return <button
data-wd-key={props.wdKey} className='maputnik-toolbar-action'
onClick={props.onClick} data-wd-key={this.props.wdKey}
> onClick={this.props.onClick}
{props.children} >
</button>; {this.props.children}
}; </button>;
}
}
export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deuteranopia" | "filter-protanopia" | "filter-tritanopia"; export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deuteranopia" | "filter-protanopia" | "filter-tritanopia";
@@ -100,16 +108,26 @@ type AppToolbarInternalProps = {
renderer?: string renderer?: string
} & WithTranslation; } & WithTranslation;
const AppToolbarInternal: React.FC<AppToolbarInternalProps> = (props) => { class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
function handleSelection(val: MapState) { state = {
props.onSetMapState(val); isOpen: {
settings: false,
sources: false,
open: false,
add: false,
export: false,
}
};
handleSelection(val: MapState) {
this.props.onSetMapState(val);
} }
function handleLanguageChange(val: string) { handleLanguageChange(val: string) {
props.i18n.changeLanguage(val); this.props.i18n.changeLanguage(val);
} }
const onSkip = (target: string) => { onSkip = (target: string) => {
if (target === "map") { if (target === "map") {
(document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus(); (document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus();
} }
@@ -119,171 +137,173 @@ const AppToolbarInternal: React.FC<AppToolbarInternalProps> = (props) => {
} }
}; };
const t = props.t; render() {
const views = [ const t = this.props.t;
{ const views = [
id: "map", {
group: "general", id: "map",
title: t("Map"), group: "general",
}, title: t("Map"),
{ },
id: "inspect", {
group: "general", id: "inspect",
title: t("Inspect"), group: "general",
disabled: props.renderer === "ol", title: t("Inspect"),
}, disabled: this.props.renderer === "ol",
{ },
id: "filter-deuteranopia", {
group: "color-accessibility", id: "filter-deuteranopia",
title: t("Deuteranopia filter"), group: "color-accessibility",
disabled: !colorAccessibilityFiltersEnabled, title: t("Deuteranopia filter"),
}, disabled: !colorAccessibilityFiltersEnabled,
{ },
id: "filter-protanopia", {
group: "color-accessibility", id: "filter-protanopia",
title: t("Protanopia filter"), group: "color-accessibility",
disabled: !colorAccessibilityFiltersEnabled, title: t("Protanopia filter"),
}, disabled: !colorAccessibilityFiltersEnabled,
{ },
id: "filter-tritanopia", {
group: "color-accessibility", id: "filter-tritanopia",
title: t("Tritanopia filter"), group: "color-accessibility",
disabled: !colorAccessibilityFiltersEnabled, title: t("Tritanopia filter"),
}, disabled: !colorAccessibilityFiltersEnabled,
{ },
id: "filter-achromatopsia", {
group: "color-accessibility", id: "filter-achromatopsia",
title: t("Achromatopsia filter"), group: "color-accessibility",
disabled: !colorAccessibilityFiltersEnabled, title: t("Achromatopsia filter"),
}, disabled: !colorAccessibilityFiltersEnabled,
]; },
];
const currentView = views.find((view) => { const currentView = views.find((view) => {
return view.id === props.mapState; return view.id === this.props.mapState;
}); });
return <nav className='maputnik-toolbar'> return <nav className='maputnik-toolbar'>
<div className="maputnik-toolbar__inner"> <div className="maputnik-toolbar__inner">
<div <div
className="maputnik-toolbar-logo-container" className="maputnik-toolbar-logo-container"
>
{/* Keyboard accessible quick links */}
<button
data-wd-key="root:skip:layer-list"
className="maputnik-toolbar-skip"
onClick={_e => onSkip("layer-list")}
> >
{t("Layers list")} {/* Keyboard accessible quick links */}
</button> <button
<button data-wd-key="root:skip:layer-list"
data-wd-key="root:skip:layer-editor" className="maputnik-toolbar-skip"
className="maputnik-toolbar-skip" onClick={_e => this.onSkip("layer-list")}
onClick={_e => onSkip("layer-editor")} >
> {t("Layers list")}
{t("Layer editor")} </button>
</button> <button
<button data-wd-key="root:skip:layer-editor"
data-wd-key="root:skip:map-view" className="maputnik-toolbar-skip"
className="maputnik-toolbar-skip" onClick={_e => this.onSkip("layer-editor")}
onClick={_e => onSkip("map")} >
> {t("Layer editor")}
{t("Map view")} </button>
</button> <button
<a data-wd-key="root:skip:map-view"
className="maputnik-toolbar-logo" className="maputnik-toolbar-skip"
target="blank" onClick={_e => this.onSkip("map")}
rel="noreferrer noopener" >
href="https://github.com/maplibre/maputnik" {t("Map view")}
> </button>
<img src={maputnikLogo} alt={t("Maputnik on GitHub")} /> <a
<h1> className="maputnik-toolbar-logo"
<span className="maputnik-toolbar-name">{pkgJson.name}</span> target="blank"
<span className="maputnik-toolbar-version">v{pkgJson.version}</span> rel="noreferrer noopener"
</h1> href="https://github.com/maplibre/maputnik"
</a> >
</div> <img src={maputnikLogo} alt={t("Maputnik on GitHub")} />
<div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar"> <h1>
<ToolbarAction wdKey="nav:open" onClick={() => props.onToggleModal("open")}> <span className="maputnik-toolbar-name">{pkgJson.name}</span>
<MdOpenInBrowser /> <span className="maputnik-toolbar-version">v{pkgJson.version}</span>
<IconText>{t("Open")}</IconText> </h1>
</ToolbarAction> </a>
<ToolbarAction wdKey="nav:export" onClick={() => props.onToggleModal("export")}> </div>
<MdSave /> <div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar">
<IconText>{t("Save")}</IconText> <ToolbarAction wdKey="nav:open" onClick={() => this.props.onToggleModal("open")}>
</ToolbarAction> <MdOpenInBrowser />
<ToolbarAction wdKey="nav:code-editor" onClick={() => props.onToggleModal("codeEditor")}> <IconText>{t("Open")}</IconText>
<MdCode /> </ToolbarAction>
<IconText>{t("Code Editor")}</IconText> <ToolbarAction wdKey="nav:export" onClick={() => this.props.onToggleModal("export")}>
</ToolbarAction> <MdSave />
<ToolbarAction wdKey="nav:sources" onClick={() => props.onToggleModal("sources")}> <IconText>{t("Save")}</IconText>
<MdLayers /> </ToolbarAction>
<IconText>{t("Data Sources")}</IconText> <ToolbarAction wdKey="nav:code-editor" onClick={() => this.props.onToggleModal("codeEditor")}>
</ToolbarAction> <MdCode />
<ToolbarAction wdKey="nav:settings" onClick={() => props.onToggleModal("settings")}> <IconText>{t("Code Editor")}</IconText>
<MdSettings /> </ToolbarAction>
<IconText>{t("Style Settings")}</IconText> <ToolbarAction wdKey="nav:sources" onClick={() => this.props.onToggleModal("sources")}>
</ToolbarAction> <MdLayers />
<ToolbarAction wdKey="nav:global-state" onClick={() => props.onToggleModal("globalState")}> <IconText>{t("Data Sources")}</IconText>
<MdPublic /> </ToolbarAction>
<IconText>{t("Global State")}</IconText> <ToolbarAction wdKey="nav:settings" onClick={() => this.props.onToggleModal("settings")}>
</ToolbarAction> <MdSettings />
<IconText>{t("Style Settings")}</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:global-state" onClick={() => this.props.onToggleModal("globalState")}>
<MdPublic />
<IconText>{t("Global State")}</IconText>
</ToolbarAction>
<ToolbarSelect wdKey="nav:inspect"> <ToolbarSelect wdKey="nav:inspect">
<MdFindInPage /> <MdFindInPage />
<IconText>{t("View")} <IconText>{t("View")}
<select <select
className="maputnik-select" className="maputnik-select"
data-wd-key="maputnik-select" data-wd-key="maputnik-select"
onChange={(e) => handleSelection(e.target.value as MapState)} onChange={(e) => this.handleSelection(e.target.value as MapState)}
value={currentView?.id} value={currentView?.id}
> >
{views.filter(v => v.group === "general").map((item) => { {views.filter(v => v.group === "general").map((item) => {
return (
<option key={item.id} value={item.id} disabled={item.disabled} data-wd-key={item.id}>
{item.title}
</option>
);
})}
<optgroup label={t("Color accessibility")}>
{views.filter(v => v.group === "color-accessibility").map((item) => {
return ( return (
<option key={item.id} value={item.id} disabled={item.disabled}> <option key={item.id} value={item.id} disabled={item.disabled} data-wd-key={item.id}>
{item.title} {item.title}
</option> </option>
); );
})} })}
</optgroup> <optgroup label={t("Color accessibility")}>
</select> {views.filter(v => v.group === "color-accessibility").map((item) => {
</IconText> return (
</ToolbarSelect> <option key={item.id} value={item.id} disabled={item.disabled}>
{item.title}
</option>
);
})}
</optgroup>
</select>
</IconText>
</ToolbarSelect>
<ToolbarSelect wdKey="nav:language"> <ToolbarSelect wdKey="nav:language">
<MdLanguage /> <MdLanguage />
<IconText>Language <IconText>Language
<select <select
className="maputnik-select" className="maputnik-select"
data-wd-key="maputnik-lang-select" data-wd-key="maputnik-lang-select"
onChange={(e) => handleLanguageChange(e.target.value)} onChange={(e) => this.handleLanguageChange(e.target.value)}
value={props.i18n.language} value={this.props.i18n.language}
> >
{Object.entries(supportedLanguages).map(([code, name]) => { {Object.entries(supportedLanguages).map(([code, name]) => {
return ( return (
<option key={code} value={code}> <option key={code} value={code}>
{name} {name}
</option> </option>
); );
})} })}
</select> </select>
</IconText> </IconText>
</ToolbarSelect> </ToolbarSelect>
<ToolbarLink href={"https://github.com/maplibre/maputnik/wiki"}> <ToolbarLink href={"https://github.com/maplibre/maputnik/wiki"}>
<MdHelpOutline /> <MdHelpOutline />
<IconText>{t("Help")}</IconText> <IconText>{t("Help")}</IconText>
</ToolbarLink> </ToolbarLink>
</div>
</div> </div>
</div> </nav>;
</nav>; }
}; }
export const AppToolbar = withTranslation()(AppToolbarInternal); export const AppToolbar = withTranslation()(AppToolbarInternal);
+68 -47
View File
@@ -1,4 +1,4 @@
import React, {type CSSProperties, type PropsWithChildren, type SyntheticEvent, useRef, useState} from "react"; import React, {type CSSProperties, 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";
@@ -14,13 +14,32 @@ export type BlockProps = PropsWithChildren & {
error?: {message: string} error?: {message: string}
}; };
/** Wrap a component with a label */ type BlockState = {
export const Block: React.FC<BlockProps> = (props) => { showDoc: boolean
const [showDoc, setShowDoc] = useState(false); };
const blockEl = useRef<HTMLDivElement | null>(null);
const onToggleDoc = (val: boolean) => { /** Wrap a component with a label */
setShowDoc(val); export class Block extends React.Component<BlockProps, BlockState> {
_blockEl: HTMLDivElement | null = null;
constructor (props: BlockProps) {
super(props);
this.state = {
showDoc: false,
};
}
onChange(e: React.BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) {
const value = e.target.value;
if (this.props.onChange) {
return this.props.onChange(value === "" ? undefined : value);
}
}
onToggleDoc = (val: boolean) => {
this.setState({
showDoc: val
});
}; };
/** /**
@@ -29,9 +48,9 @@ export const Block: React.FC<BlockProps> = (props) => {
* causing the picker to reopen. This causes a scenario where the picker can * causing the picker to reopen. This causes a scenario where the picker can
* never be closed once open. * never be closed once open.
*/ */
const onLabelClick = (event: SyntheticEvent<any, any>) => { onLabelClick = (event: SyntheticEvent<any, any>) => {
const el = event.nativeEvent.target; const el = event.nativeEvent.target;
const contains = blockEl.current?.contains(el); const contains = this._blockEl?.contains(el);
if (event.nativeEvent.target.nodeName !== "INPUT" && !contains) { if (event.nativeEvent.target.nodeName !== "INPUT" && !contains) {
event.stopPropagation(); event.stopPropagation();
@@ -41,43 +60,45 @@ export const Block: React.FC<BlockProps> = (props) => {
} }
}; };
return <label style={props.style} render() {
data-wd-key={props["data-wd-key"]} return <label style={this.props.style}
className={classnames({ data-wd-key={this.props["data-wd-key"]}
"maputnik-input-block": true, className={classnames({
"maputnik-input-block--wide": props.wideMode, "maputnik-input-block": true,
"maputnik-action-block": props.action, "maputnik-input-block--wide": this.props.wideMode,
"maputnik-input-block--error": props.error "maputnik-action-block": this.props.action,
})} "maputnik-input-block--error": this.props.error
onClick={onLabelClick} })}
> onClick={this.onLabelClick}
{props.fieldSpec && >
<div className="maputnik-input-block-label"> {this.props.fieldSpec &&
<FieldDocLabel <div className="maputnik-input-block-label">
label={props.label} <FieldDocLabel
onToggleDoc={onToggleDoc} label={this.props.label}
fieldSpec={props.fieldSpec} onToggleDoc={this.onToggleDoc}
/> fieldSpec={this.props.fieldSpec}
/>
</div>
}
{!this.props.fieldSpec &&
<div className="maputnik-input-block-label">
{this.props.label}
</div>
}
<div className="maputnik-input-block-action">
{this.props.action}
</div> </div>
} <div className="maputnik-input-block-content" ref={el => {this._blockEl = el;}}>
{!props.fieldSpec && {this.props.children}
<div className="maputnik-input-block-label">
{props.label}
</div> </div>
} {this.props.fieldSpec &&
<div className="maputnik-input-block-action"> <div
{props.action} className="maputnik-doc-inline"
</div> style={{display: this.state.showDoc ? "" : "none"}}
<div className="maputnik-input-block-content" ref={blockEl}> >
{props.children} <Doc fieldSpec={this.props.fieldSpec} />
</div> </div>
{props.fieldSpec && }
<div </label>;
className="maputnik-doc-inline" }
style={{display: showDoc ? "" : "none"}} }
>
<Doc fieldSpec={props.fieldSpec} />
</div>
}
</label>;
};
+21 -15
View File
@@ -9,19 +9,25 @@ type CollapseProps = {
}; };
export const Collapse: React.FC<CollapseProps> = ({isActive = true, children}) => { export class Collapse extends React.Component<CollapseProps> {
if (reducedMotionEnabled()) { static defaultProps = {
return ( isActive: true
<div style={{display: isActive ? "block" : "none"}}> };
{children}
</div> render() {
); if (reducedMotionEnabled()) {
return (
<div style={{display: this.props.isActive ? "block" : "none"}}>
{this.props.children}
</div>
);
}
else {
return (
<ReactCollapse isOpened={this.props.isActive}>
{this.props.children}
</ReactCollapse>
);
}
} }
else { }
return (
<ReactCollapse isOpened={isActive}>
{children}
</ReactCollapse>
);
}
};
+10 -8
View File
@@ -6,11 +6,13 @@ type CollapserProps = {
style?: object style?: object
}; };
export const Collapser: React.FC<CollapserProps> = (props) => { export class Collapser extends React.Component<CollapserProps> {
const iconStyle = { render() {
width: 20, const iconStyle = {
height: 20, width: 20,
...props.style, height: 20,
}; ...this.props.style,
return props.isCollapsed ? <MdArrowDropUp style={iconStyle}/> : <MdArrowDropDown style={iconStyle} />; };
}; return this.props.isCollapsed ? <MdArrowDropUp style={iconStyle}/> : <MdArrowDropDown style={iconStyle} />;
}
}
+173 -156
View File
@@ -1,4 +1,4 @@
import React, { useRef } from "react"; import React from "react";
import {PiListPlusBold} from "react-icons/pi"; import {PiListPlusBold} from "react-icons/pi";
import {TbMathFunction} from "react-icons/tb"; import {TbMathFunction} from "react-icons/tb";
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json"; import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
@@ -68,19 +68,32 @@ export type Stop = [{
value: number value: number
}, number]; }, number];
const DataPropertyInternal: React.FC<DataPropertyInternalProps> = (props) => { class DataPropertyInternal extends React.Component<DataPropertyInternalProps, DataPropertyState> {
// Kept in a ref rather than state: the original recomputed these on every state = {
// render via getDerivedStateFromProps, which as state would mean setting refs: {} as {[key: number]: string}
// state during render on every pass. };
const refs = useRef<{[key: number]: string}>({});
// setStopRefs returns undefined when no new stop needs a ref; as in the componentDidMount() {
// original, only replace the map when it actually produced one. const newRefs = setStopRefs(this.props, this.state);
const newStopRefs = setStopRefs(props, { refs: refs.current });
if (newStopRefs) { if(newRefs) {
refs.current = newStopRefs; this.setState({
refs: newRefs
});
}
} }
function getFieldFunctionType(fieldSpec: any) { static getDerivedStateFromProps(props: Readonly<DataPropertyInternalProps>, state: DataPropertyState) {
const newRefs = setStopRefs(props, state);
if(newRefs) {
return {
refs: newRefs
};
}
return null;
}
getFieldFunctionType(fieldSpec: any) {
if (fieldSpec.expression.interpolated) { if (fieldSpec.expression.interpolated) {
return "exponential"; return "exponential";
} }
@@ -90,7 +103,7 @@ const DataPropertyInternal: React.FC<DataPropertyInternalProps> = (props) => {
return "categorical"; return "categorical";
} }
function 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"];
} }
@@ -100,11 +113,11 @@ const DataPropertyInternal: React.FC<DataPropertyInternalProps> = (props) => {
} }
// Order the stops altering the refs to reflect their new position. // Order the stops altering the refs to reflect their new position.
function orderStopsByZoom(stops: Stop[]) { orderStopsByZoom(stops: Stop[]) {
const mappedWithRef = stops const mappedWithRef = stops
.map((stop, idx) => { .map((stop, idx) => {
return { return {
ref: refs.current[idx], ref: this.state.refs[idx],
data: stop data: stop
}; };
}) })
@@ -118,12 +131,14 @@ const DataPropertyInternal: React.FC<DataPropertyInternalProps> = (props) => {
newRefs[idx] = stop.ref; newRefs[idx] = stop.ref;
}); });
refs.current = newRefs; this.setState({
refs: newRefs
});
return mappedWithRef.map((item) => item.data); return mappedWithRef.map((item) => item.data);
} }
const onChange = (fieldName: string, value: any) => { onChange = (fieldName: string, value: any) => {
if (value.type === "identity") { if (value.type === "identity") {
value = { value = {
type: value.type, type: value.type,
@@ -137,17 +152,17 @@ const DataPropertyInternal: React.FC<DataPropertyInternalProps> = (props) => {
type: value.type, type: value.type,
// Default props if they don't already exist. // Default props if they don't already exist.
stops: [ stops: [
[{zoom: 6, value: stopValue}, findDefaultFromSpec(props.fieldSpec as any)], [{zoom: 6, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec as any)],
[{zoom: 10, value: stopValue}, findDefaultFromSpec(props.fieldSpec as any)] [{zoom: 10, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec as any)]
], ],
...value, ...value,
}; };
} }
props.onChange!(fieldName, value); this.props.onChange!(fieldName, value);
}; };
function changeStop(changeIdx: number, stopData: { zoom: number | undefined, value: number }, value: number) { changeStop(changeIdx: number, stopData: { zoom: number | undefined, value: number }, value: number) {
const stops = 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] = [
{ {
@@ -157,134 +172,135 @@ const DataPropertyInternal: React.FC<DataPropertyInternalProps> = (props) => {
value value
]; ];
const orderedStops = orderStopsByZoom(stops); const orderedStops = this.orderStopsByZoom(stops);
const changedValue = { const changedValue = {
...props.value, ...this.props.value,
stops: orderedStops, stops: orderedStops,
}; };
onChange(props.fieldName, changedValue); this.onChange(this.props.fieldName, changedValue);
} }
function changeBase(newValue: number | undefined) { changeBase(newValue: number | undefined) {
const changedValue = { const changedValue = {
...props.value, ...this.props.value,
base: newValue base: newValue
}; };
if (changedValue.base === undefined) { if (changedValue.base === undefined) {
delete changedValue["base"]; delete changedValue["base"];
} }
props.onChange!(props.fieldName, changedValue); this.props.onChange!(this.props.fieldName, changedValue);
} }
function changeDataType(propVal: string) { changeDataType(propVal: string) {
if (propVal === "interpolate" && props.onChangeToZoomFunction) { if (propVal === "interpolate" && this.props.onChangeToZoomFunction) {
props.onChangeToZoomFunction(); this.props.onChangeToZoomFunction();
} }
else { else {
onChange(props.fieldName, { this.onChange(this.props.fieldName, {
...props.value, ...this.props.value,
type: propVal, type: propVal,
}); });
} }
} }
function changeDataProperty(propName: "property" | "default", propVal: any) { changeDataProperty(propName: "property" | "default", propVal: any) {
if (propVal) { if (propVal) {
props.value![propName] = propVal; this.props.value![propName] = propVal;
} }
else { else {
delete props.value![propName]; delete this.props.value![propName];
} }
onChange(props.fieldName, props.value); this.onChange(this.props.fieldName, this.props.value);
} }
const t = props.t; render() {
const t = this.props.t;
if (typeof props.value?.type === "undefined") { if (typeof this.props.value?.type === "undefined") {
props.value!.type = getFieldFunctionType(props.fieldSpec); this.props.value!.type = this.getFieldFunctionType(this.props.fieldSpec);
} }
let dataFields; let dataFields;
if (props.value?.stops) { if (this.props.value?.stops) {
dataFields = 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 = refs.current[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={props.onDeleteStop?.bind(null, 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) => 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(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;
if(zoomLevel !== undefined) { if(zoomLevel !== undefined) {
zoomInput = <div> zoomInput = <div>
<InputNumber <InputNumber
aria-label="Zoom" aria-label="Zoom"
value={zoomLevel} value={zoomLevel}
onChange={newZoom => changeStop(idx, {zoom: newZoom, value: dataLevel}, value)} onChange={newZoom => this.changeStop(idx, {zoom: newZoom, value: dataLevel}, value)}
min={0} min={0}
max={22} max={22}
/>
</div>;
}
return <tr key={key}>
<td>
{zoomInput}
</td>
<td>
{dataInput}
</td>
<td>
<InputSpec
aria-label={t("Output value")}
fieldName={props.fieldName}
fieldSpec={props.fieldSpec}
value={value}
onChange={(_, newValue) => changeStop(idx, {zoom: zoomLevel, value: dataLevel}, newValue as number)}
/>
</td>
<td>
{deleteStopBtn}
</td>
</tr>;
});
}
return <div className="maputnik-data-spec-block">
<fieldset className="maputnik-data-spec-property">
<legend>{labelFromFieldName(props.fieldName)}</legend>
<div className="maputnik-data-fieldset-inner">
<Block
label={t("Function")}
key="function"
data-wd-key="function-type"
>
<div className="maputnik-data-spec-property-input">
<InputSelect
value={props.value!.type}
onChange={(propVal: string) => changeDataType(propVal)}
title={t("Select a type of data scale (default is 'categorical').")}
options={getDataFunctionTypes(props.fieldSpec)}
/> />
</div> </div>;
</Block> }
{props.value?.type !== "identity" &&
return <tr key={key}>
<td>
{zoomInput}
</td>
<td>
{dataInput}
</td>
<td>
<InputSpec
aria-label={t("Output value")}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={value}
onChange={(_, newValue) => this.changeStop(idx, {zoom: zoomLevel, value: dataLevel}, newValue as number)}
/>
</td>
<td>
{deleteStopBtn}
</td>
</tr>;
});
}
return <div className="maputnik-data-spec-block">
<fieldset className="maputnik-data-spec-property">
<legend>{labelFromFieldName(this.props.fieldName)}</legend>
<div className="maputnik-data-fieldset-inner">
<Block
label={t("Function")}
key="function"
data-wd-key="function-type"
>
<div className="maputnik-data-spec-property-input">
<InputSelect
value={this.props.value!.type}
onChange={(propVal: string) => this.changeDataType(propVal)}
title={t("Select a type of data scale (default is 'categorical').")}
options={this.getDataFunctionTypes(this.props.fieldSpec)}
/>
</div>
</Block>
{this.props.value?.type !== "identity" &&
<Block <Block
label={t("Base")} label={t("Base")}
key="base" key="base"
@@ -294,40 +310,40 @@ const DataPropertyInternal: React.FC<DataPropertyInternalProps> = (props) => {
<InputSpec <InputSpec
fieldName={"base"} fieldName={"base"}
fieldSpec={latest.function.base as typeof latest.function.base & { type: "number" }} fieldSpec={latest.function.base as typeof latest.function.base & { type: "number" }}
value={props.value?.base} value={this.props.value?.base}
onChange={(_, newValue) => changeBase(newValue as number)} onChange={(_, newValue) => this.changeBase(newValue as number)}
/> />
</div> </div>
</Block> </Block>
} }
<Block <Block
label={"Property"} label={"Property"}
key="property" key="property"
data-wd-key="function-property" data-wd-key="function-property"
> >
<div className="maputnik-data-spec-property-input"> <div className="maputnik-data-spec-property-input">
<InputString <InputString
value={props.value?.property} value={this.props.value?.property}
title={t("Input a data property to base styles off of.")} title={t("Input a data property to base styles off of.")}
onChange={propVal => changeDataProperty("property", propVal)} onChange={propVal => this.changeDataProperty("property", propVal)}
/> />
</div> </div>
</Block> </Block>
{dataFields && {dataFields &&
<Block <Block
label={t("Default")} label={t("Default")}
key="default" key="default"
data-wd-key="function-default" data-wd-key="function-default"
> >
<InputSpec <InputSpec
fieldName={props.fieldName} fieldName={this.props.fieldName}
fieldSpec={props.fieldSpec} fieldSpec={this.props.fieldSpec}
value={props.value?.default} value={this.props.value?.default}
onChange={(_, propVal) => changeDataProperty("default", propVal)} onChange={(_, propVal) => this.changeDataProperty("default", propVal)}
/> />
</Block> </Block>
} }
{dataFields && {dataFields &&
<div className="maputnik-function-stop"> <div className="maputnik-function-stop">
<table className="maputnik-function-stop-table"> <table className="maputnik-function-stop-table">
<caption>{t("Stops")}</caption> <caption>{t("Stops")}</caption>
@@ -343,29 +359,30 @@ const DataPropertyInternal: React.FC<DataPropertyInternalProps> = (props) => {
</tbody> </tbody>
</table> </table>
</div> </div>
} }
<div className="maputnik-toolbox"> <div className="maputnik-toolbox">
{dataFields && {dataFields &&
<InputButton <InputButton
className="maputnik-add-stop" className="maputnik-add-stop"
onClick={props.onAddStop?.bind(null)} onClick={this.props.onAddStop?.bind(this)}
> >
<PiListPlusBold style={{ verticalAlign: "text-bottom" }} /> <PiListPlusBold style={{ verticalAlign: "text-bottom" }} />
{t("Add stop")} {t("Add stop")}
</InputButton> </InputButton>
} }
<InputButton <InputButton
className="maputnik-add-stop" className="maputnik-add-stop"
data-wd-key="convert-to-expression" data-wd-key="convert-to-expression"
onClick={props.onExpressionClick?.bind(null)} onClick={this.props.onExpressionClick?.bind(this)}
> >
<TbMathFunction style={{ verticalAlign: "text-bottom" }} /> <TbMathFunction style={{ verticalAlign: "text-bottom" }} />
{t("Convert to expression")} {t("Convert to expression")}
</InputButton> </InputButton>
</div>
</div> </div>
</div> </fieldset>
</fieldset> </div>;
</div>; }
}; }
export const DataProperty = withTranslation()(DataPropertyInternal); export const DataProperty = withTranslation()(DataPropertyInternal);
+12 -10
View File
@@ -10,15 +10,17 @@ type DeleteStopButtonInternalProps = {
} & WithTranslation; } & WithTranslation;
const DeleteStopButtonInternal: React.FC<DeleteStopButtonInternalProps> = (props) => { class DeleteStopButtonInternal extends React.Component<DeleteStopButtonInternalProps> {
const t = props.t; render() {
return <InputButton const t = this.props.t;
className="maputnik-delete-stop" return <InputButton
onClick={props.onClick} className="maputnik-delete-stop"
title={t("Remove zoom level from stop")} onClick={this.props.onClick}
> title={t("Remove zoom level from stop")}
<MdDelete /> >
</InputButton>; <MdDelete />
}; </InputButton>;
}
}
export const DeleteStopButton = withTranslation()(DeleteStopButtonInternal); export const DeleteStopButton = withTranslation()(DeleteStopButtonInternal);
+80 -76
View File
@@ -23,84 +23,88 @@ type DocProps = {
} }
}; };
export const Doc: React.FC<DocProps> = ({fieldSpec}) => { export class Doc extends React.Component<DocProps> {
const {doc, values, docUrl, docUrlLinkText} = fieldSpec; render () {
const sdkSupport = fieldSpec["sdk-support"]; const {fieldSpec} = this.props;
const renderValues = ( const {doc, values, docUrl, docUrlLinkText} = fieldSpec;
!!values && const sdkSupport = fieldSpec["sdk-support"];
// HACK: Currently we merge additional values into the style spec, so this is required
// See <https://github.com/maplibre/maputnik/blob/main/src/components/PropertyGroup.jsx#L16>
!Array.isArray(values)
);
const sdkSupportToJsx = (value: string) => { const renderValues = (
const supportValue = value.toLowerCase(); !!values &&
if (supportValue.startsWith("https://")) { // HACK: Currently we merge additional values into the style spec, so this is required
return <a href={supportValue} target="_blank" rel="noreferrer">{"#" + supportValue.split("/").pop()}</a>; // See <https://github.com/maplibre/maputnik/blob/main/src/components/PropertyGroup.jsx#L16>
} !Array.isArray(values)
return value; );
};
return ( const sdkSupportToJsx = (value: string) => {
<> const supportValue = value.toLowerCase();
{doc && if (supportValue.startsWith("https://")) {
<div className="SpecDoc"> return <a href={supportValue} target="_blank" rel="noreferrer">{"#" + supportValue.split("/").pop()}</a>;
<div className="SpecDoc__doc" data-wd-key='spec-field-doc'>
<Markdown components={{
a: ({node: _node, href, children, ...props}) => <a href={href} target="_blank" {...props}>{children}</a>,
}}>{doc}</Markdown>
</div>
{renderValues &&
<ul className="SpecDoc__values">
{Object.entries(values).map(([key, value]) => {
return (
<li key={key}>
<code>{JSON.stringify(key)}</code>
<div>{value.doc}</div>
</li>
);
})}
</ul>
}
</div>
} }
{sdkSupport && return value;
<div className="SpecDoc__sdk-support"> };
<table className="SpecDoc__sdk-support__table">
<thead> return (
<tr> <>
<th></th> {doc &&
{Object.values(headers).map(header => { <div className="SpecDoc">
return <th key={header}>{header}</th>; <div className="SpecDoc__doc" data-wd-key='spec-field-doc'>
<Markdown components={{
a: ({node: _node, href, children, ...props}) => <a href={href} target="_blank" {...props}>{children}</a>,
}}>{doc}</Markdown>
</div>
{renderValues &&
<ul className="SpecDoc__values">
{Object.entries(values).map(([key, value]) => {
return (
<li key={key}>
<code>{JSON.stringify(key)}</code>
<div>{value.doc}</div>
</li>
);
})} })}
</tr> </ul>
</thead> }
<tbody> </div>
{Object.entries(sdkSupport).map(([key, supportObj]) => { }
return ( {sdkSupport &&
<tr key={key}> <div className="SpecDoc__sdk-support">
<td>{key}</td> <table className="SpecDoc__sdk-support__table">
{Object.keys(headers).map((k) => { <thead>
if (Object.prototype.hasOwnProperty.call(supportObj, k)) { <tr>
return <td key={k}>{sdkSupportToJsx(supportObj[k as keyof typeof headers])}</td>; <th></th>
} {Object.values(headers).map(header => {
else { return <th key={header}>{header}</th>;
return <td key={k}>no</td>; })}
} </tr>
})} </thead>
</tr> <tbody>
); {Object.entries(sdkSupport).map(([key, supportObj]) => {
})} return (
</tbody> <tr key={key}>
</table> <td>{key}</td>
</div> {Object.keys(headers).map((k) => {
} if (Object.prototype.hasOwnProperty.call(supportObj, k)) {
{docUrl && docUrlLinkText && return <td key={k}>{sdkSupportToJsx(supportObj[k as keyof typeof headers])}</td>;
<div className="SpecDoc__learn-more"> }
<a href={docUrl} target="_blank" rel="noreferrer">{docUrlLinkText}</a> else {
</div> return <td key={k}>no</td>;
} }
</> })}
); </tr>
}; );
})}
</tbody>
</table>
</div>
}
{docUrl && docUrlLinkText &&
<div className="SpecDoc__learn-more">
<a href={docUrl} target="_blank" rel="noreferrer">{docUrlLinkText}</a>
</div>
}
</>
);
}
}
+65 -55
View File
@@ -24,62 +24,72 @@ type ExpressionPropertyInternalProps = {
onBlur?(...args: unknown[]): unknown onBlur?(...args: unknown[]): unknown
} & WithTranslation; } & WithTranslation;
const ExpressionPropertyInternal: React.FC<ExpressionPropertyInternalProps> = ({ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInternalProps> {
errors = {}, static defaultProps = {
onFocus = () => {}, errors: {},
onBlur = () => {}, onFocus: () => {},
...props onBlur: () => {},
}) => { };
const {t, value, canUndo} = props;
const undoDisabled = canUndo ? !canUndo() : true;
const deleteStopBtn = ( constructor(props: ExpressionPropertyInternalProps) {
<> super(props);
{props.onUndo && this.state = {
<InputButton jsonError: false,
key="undo_action" };
onClick={props.onUndo}
disabled={undoDisabled}
className="maputnik-delete-stop"
data-wd-key="undo-expression"
title={t("Revert from expression")}
>
<MdUndo />
</InputButton>
}
<InputButton
key="delete_action"
onClick={props.onDelete}
className="maputnik-delete-stop"
data-wd-key="delete-expression"
title={t("Delete expression")}
>
<MdDelete />
</InputButton>
</>
);
let error = undefined;
if (errors) {
const fieldKey = props.fieldType ? props.fieldType + "." + props.fieldName : props.fieldName;
error = errors[fieldKey];
} }
return <Block
fieldSpec={props.fieldSpec} render() {
label={t(labelFromFieldName(props.fieldName))} const {t, value, canUndo} = this.props;
action={deleteStopBtn} const undoDisabled = canUndo ? !canUndo() : true;
wideMode={true}
error={error} const deleteStopBtn = (
> <>
<FieldJson {this.props.onUndo &&
lintType="expression" <InputButton
spec={props.fieldSpec} key="undo_action"
className="maputnik-expression-editor" onClick={this.props.onUndo}
onFocus={onFocus} disabled={undoDisabled}
onBlur={onBlur} className="maputnik-delete-stop"
value={value} data-wd-key="undo-expression"
onChange={props.onChange} title={t("Revert from expression")}
/> >
</Block>; <MdUndo />
}; </InputButton>
}
<InputButton
key="delete_action"
onClick={this.props.onDelete}
className="maputnik-delete-stop"
data-wd-key="delete-expression"
title={t("Delete expression")}
>
<MdDelete />
</InputButton>
</>
);
let error = undefined;
if (this.props.errors) {
const fieldKey = this.props.fieldType ? this.props.fieldType + "." + this.props.fieldName : this.props.fieldName;
error = this.props.errors[fieldKey];
}
return <Block
fieldSpec={this.props.fieldSpec}
label={t(labelFromFieldName(this.props.fieldName))}
action={deleteStopBtn}
wideMode={true}
error={error}
>
<FieldJson
lintType="expression"
spec={this.props.fieldSpec}
className="maputnik-expression-editor"
onFocus={this.props.onFocus}
onBlur={this.props.onBlur}
value={value}
onChange={this.props.onChange}
/>
</Block>;
}
}
export const ExpressionProperty = withTranslation()(ExpressionPropertyInternal); export const ExpressionProperty = withTranslation()(ExpressionPropertyInternal);
+4 -1
View File
@@ -2,7 +2,10 @@ import { InputDynamicArray, type InputDynamicArrayProps } from "./InputDynamicAr
import { Fieldset } from "./Fieldset"; import { Fieldset } from "./Fieldset";
type FieldDynamicArrayProps = InputDynamicArrayProps & { type FieldDynamicArrayProps = InputDynamicArrayProps & {
name?: string name?: string;
fieldSpec?: {
value?: string;
}
}; };
export const FieldDynamicArray: React.FC<FieldDynamicArrayProps> = (props) => { export const FieldDynamicArray: React.FC<FieldDynamicArrayProps> = (props) => {
+174 -142
View File
@@ -1,4 +1,4 @@
import React, { useState } from "react"; import React from "react";
import { TbMathFunction } from "react-icons/tb"; import { TbMathFunction } from "react-icons/tb";
import { PiListPlusBold } from "react-icons/pi"; import { PiListPlusBold } from "react-icons/pi";
import {isEqual} from "lodash"; import {isEqual} from "lodash";
@@ -100,190 +100,222 @@ type FilterEditorInternalProps = {
onChange(value: LegacyFilterSpecification | ExpressionSpecification): void onChange(value: LegacyFilterSpecification | ExpressionSpecification): void
} & WithTranslation; } & WithTranslation;
const FilterEditorInternal: React.FC<FilterEditorInternalProps> = ({ filter = ["all"], ...rest }) => { type FilterEditorState = {
const props = { filter, ...rest } as FilterEditorInternalProps; showDoc: boolean
displaySimpleFilter: boolean
valueIsSimpleFilter?: boolean
};
// Nothing ever toggles this: the Block below renders its own documentation class FilterEditorInternal extends React.Component<FilterEditorInternalProps, FilterEditorState> {
// toggle, so this component's inline doc panel stays hidden (as it did before). static defaultProps = {
const [showDoc] = useState(false); filter: ["all"],
const [displaySimpleFilter, setDisplaySimpleFilter] = useState(() => };
checkIfSimpleFilter(combiningFilter(props))
);
// Replaces getDerivedStateFromProps. "Upgrade but never downgrade": once the constructor (props: FilterEditorInternalProps) {
// filter stops being expressible in the simple editor, switch to the super(props);
// expression editor and stay there. this.state = {
const isSimpleFilter = checkIfSimpleFilter(combiningFilter(props)); showDoc: false,
if (!isSimpleFilter && displaySimpleFilter) { displaySimpleFilter: checkIfSimpleFilter(combiningFilter(props)),
setDisplaySimpleFilter(false); };
} }
// In the original this was state, but every branch derived it from these two
// values alone.
const valueIsSimpleFilter = isSimpleFilter && !displaySimpleFilter;
// Convert filter to combining filter // Convert filter to combining filter
function onFilterPartChanged(filterIdx: number, newPart: any[]) { onFilterPartChanged(filterIdx: number, newPart: any[]) {
const newFilter = combiningFilter(props).slice(0) as LegacyFilterSpecification | ExpressionSpecification; const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification;
newFilter[filterIdx] = newPart; newFilter[filterIdx] = newPart;
props.onChange(newFilter); this.props.onChange(newFilter);
} }
function deleteFilterItem(filterIdx: number) { deleteFilterItem(filterIdx: number) {
const newFilter = combiningFilter(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);
props.onChange(newFilter); this.props.onChange(newFilter);
} }
const addFilterItem = () => { addFilterItem = () => {
const newFilterItem = combiningFilter(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", ""]);
props.onChange(newFilterItem); this.props.onChange(newFilterItem);
}; };
const makeFilter = () => { onToggleDoc = (val: boolean) => {
setDisplaySimpleFilter(true); this.setState({
showDoc: val
});
}; };
const makeExpression = () => { makeFilter = () => {
const currentFilter = combiningFilter(props); this.setState({
props.onChange(migrateFilter(currentFilter)); displaySimpleFilter: true,
setDisplaySimpleFilter(false); });
}; };
makeExpression = () => {
const {errors, t} = props; const filter = combiningFilter(this.props);
const fieldSpec={ this.props.onChange(migrateFilter(filter));
doc: latest.layer.filter.doc + " Combine multiple filters together by using a compound filter." this.setState({
displaySimpleFilter: false,
});
}; };
const defaultFilter = ["all"] as LegacyFilterSpecification | ExpressionSpecification;
const isNestedCombiningFilter = displaySimpleFilter && hasNestedCombiningFilter(combiningFilter(props)); static getDerivedStateFromProps(props: Readonly<FilterEditorInternalProps>, state: FilterEditorState) {
const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props));
if (isNestedCombiningFilter) { // Upgrade but never downgrade
return <div className="maputnik-filter-editor-unsupported"> if (!displaySimpleFilter && state.displaySimpleFilter === true) {
<p> return {
{t("Nested filters are not supported.")} displaySimpleFilter: false,
</p> valueIsSimpleFilter: false,
<InputButton };
onClick={makeExpression} }
title={t("Convert to expression")} else if (displaySimpleFilter && state.displaySimpleFilter === false) {
> return {
<TbMathFunction /> valueIsSimpleFilter: true,
{t("Upgrade to expression")} };
</InputButton> }
</div>; else {
return {
valueIsSimpleFilter: false,
};
}
} }
else if (displaySimpleFilter) {
const filter = combiningFilter(props);
const combiningOp = filter[0];
const filters = filter.slice(1) as (LegacyFilterSpecification | ExpressionSpecification)[];
const actions = ( render() {
<div> const {errors, t} = this.props;
const {displaySimpleFilter} = this.state;
const fieldSpec={
doc: latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."
};
const defaultFilter = ["all"] as LegacyFilterSpecification | ExpressionSpecification;
const isNestedCombiningFilter = displaySimpleFilter && hasNestedCombiningFilter(combiningFilter(this.props));
if (isNestedCombiningFilter) {
return <div className="maputnik-filter-editor-unsupported">
<p>
{t("Nested filters are not supported.")}
</p>
<InputButton <InputButton
onClick={makeExpression} onClick={this.makeExpression}
title={t("Convert to expression")} title={t("Convert to expression")}
className="maputnik-make-zoom-function"
data-wd-key="filter-convert-to-expression"
> >
<TbMathFunction /> <TbMathFunction />
{t("Upgrade to expression")}
</InputButton> </InputButton>
</div> </div>;
); }
else if (displaySimpleFilter) {
const filter = combiningFilter(this.props);
const combiningOp = filter[0];
const filters = filter.slice(1) as (LegacyFilterSpecification | ExpressionSpecification)[];
const editorBlocks = filters.map((f, idx) => { const actions = (
const error = errors![`filter[${idx+1}]`]; <div>
return (
<div key={`block-${idx}`}>
<FilterEditorBlock key={idx} onDelete={deleteFilterItem.bind(null, idx)}>
<SingleFilterEditor
properties={props.properties}
filter={f}
onChange={onFilterPartChanged.bind(null, idx + 1)}
/>
</FilterEditorBlock>
{error &&
<div key="error" className="maputnik-inline-error">{error.message}</div>
}
</div>
);
});
return (
<>
<Block
key="top"
fieldSpec={fieldSpec}
label={t("Filter")}
action={actions}
data-wd-key="filter-combining-operator"
>
<InputSelect
value={combiningOp}
onChange={(v: [string, any]) => onFilterPartChanged(0, v)}
options={[
["all", t("every filter matches")],
["none", t("no filter matches")],
["any", t("any filter matches")]
]}
/>
</Block>
{editorBlocks}
<div
key="buttons"
className="maputnik-filter-editor-add-wrapper"
>
<InputButton <InputButton
data-wd-key="layer-filter-button" onClick={this.makeExpression}
className="maputnik-add-filter" title={t("Convert to expression")}
onClick={addFilterItem} className="maputnik-make-zoom-function"
data-wd-key="filter-convert-to-expression"
> >
<PiListPlusBold style={{ verticalAlign: "text-bottom" }} /> <TbMathFunction />
{t("Add filter")}
</InputButton> </InputButton>
</div> </div>
<div );
key="doc"
className="maputnik-doc-inline"
style={{display: showDoc ? "" : "none"}}
>
<Doc fieldSpec={fieldSpec} />
</div>
</>
);
}
else {
const {filter} = props;
return ( const editorBlocks = filters.map((f, idx) => {
<> const error = errors![`filter[${idx+1}]`];
<ExpressionProperty
onDelete={() => { return (
setDisplaySimpleFilter(true); <div key={`block-${idx}`}>
props.onChange(defaultFilter); <FilterEditorBlock key={idx} onDelete={this.deleteFilterItem.bind(this, idx)}>
}} <SingleFilterEditor
fieldName="filter" properties={this.props.properties}
value={filter} filter={f}
errors={errors} onChange={this.onFilterPartChanged.bind(this, idx + 1)}
onChange={props.onChange} />
/> </FilterEditorBlock>
{valueIsSimpleFilter && {error &&
<div key="error" className="maputnik-inline-error">{error.message}</div>
}
</div>
);
});
return (
<>
<Block
key="top"
fieldSpec={fieldSpec}
label={t("Filter")}
action={actions}
data-wd-key="filter-combining-operator"
>
<InputSelect
value={combiningOp}
onChange={(v: [string, any]) => this.onFilterPartChanged(0, v)}
options={[
["all", t("every filter matches")],
["none", t("no filter matches")],
["any", t("any filter matches")]
]}
/>
</Block>
{editorBlocks}
<div
key="buttons"
className="maputnik-filter-editor-add-wrapper"
>
<InputButton
data-wd-key="layer-filter-button"
className="maputnik-add-filter"
onClick={this.addFilterItem}
>
<PiListPlusBold style={{ verticalAlign: "text-bottom" }} />
{t("Add filter")}
</InputButton>
</div>
<div
key="doc"
className="maputnik-doc-inline"
style={{display: this.state.showDoc ? "" : "none"}}
>
<Doc fieldSpec={fieldSpec} />
</div>
</>
);
}
else {
const {filter} = this.props;
return (
<>
<ExpressionProperty
onDelete={() => {
this.setState({displaySimpleFilter: true});
this.props.onChange(defaultFilter);
}}
fieldName="filter"
value={filter}
errors={errors}
onChange={this.props.onChange}
/>
{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={makeFilter} onClick={this.makeFilter}
className="maputnik-expr-infobox__button" className="maputnik-expr-infobox__button"
> >
{t("Switch to filter editor.")} {t("Switch to filter editor.")}
</button> </button>
</div> </div>
} }
</> </>
); );
}
} }
}; }
export const FilterEditor = withTranslation()(FilterEditorInternal); export const FilterEditor = withTranslation()(FilterEditorInternal);
+19 -17
View File
@@ -7,22 +7,24 @@ type FilterEditorBlockInternalProps = PropsWithChildren & {
onDelete(...args: unknown[]): unknown onDelete(...args: unknown[]): unknown
} & WithTranslation; } & WithTranslation;
const FilterEditorBlockInternal: React.FC<FilterEditorBlockInternalProps> = (props) => { class FilterEditorBlockInternal extends React.Component<FilterEditorBlockInternalProps> {
const t = props.t; render() {
return <div className="maputnik-filter-editor-block"> const t = this.props.t;
<div className="maputnik-filter-editor-block-content"> return <div className="maputnik-filter-editor-block">
{props.children} <div className="maputnik-filter-editor-block-content">
</div> {this.props.children}
<div className="maputnik-filter-editor-block-action"> </div>
<InputButton <div className="maputnik-filter-editor-block-action">
className="maputnik-icon-button" <InputButton
onClick={props.onDelete} className="maputnik-icon-button"
title={t("Delete filter block")} onClick={this.props.onDelete}
> title={t("Delete filter block")}
<MdDelete /> >
</InputButton> <MdDelete />
</div> </InputButton>
</div>; </div>
}; </div>;
}
}
export const FilterEditorBlock = withTranslation()(FilterEditorBlockInternal); export const FilterEditorBlock = withTranslation()(FilterEditorBlockInternal);
+47 -45
View File
@@ -13,56 +13,58 @@ type FunctionInputButtonsInternalProps = {
onElevationClick?(): void onElevationClick?(): void
} & WithTranslation; } & WithTranslation;
const FunctionInputButtonsInternal: React.FC<FunctionInputButtonsInternalProps> = (props) => { class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsInternalProps> {
const t = props.t; render() {
const t = this.props.t;
if (props.fieldSpec.expression?.parameters.includes("zoom")) { if (this.props.fieldSpec.expression?.parameters.includes("zoom")) {
const expressionInputButton = ( const expressionInputButton = (
<InputButton <InputButton
className="maputnik-make-zoom-function"
onClick={this.props.onExpressionClick}
title={t("Convert to expression")}
>
<TbMathFunction />
</InputButton>
);
const makeZoomInputButton = <InputButton
className="maputnik-make-zoom-function" className="maputnik-make-zoom-function"
onClick={props.onExpressionClick} onClick={this.props.onZoomClick}
title={t("Convert to expression")} title={t("Convert property into a zoom function")}
> >
<TbMathFunction /> <MdFunctions />
</InputButton>
);
const makeZoomInputButton = <InputButton
className="maputnik-make-zoom-function"
onClick={props.onZoomClick}
title={t("Convert property into a zoom function")}
>
<MdFunctions />
</InputButton>;
let makeDataInputButton;
if (props.fieldSpec["property-type"] === "data-driven") {
makeDataInputButton = <InputButton
className="maputnik-make-data-function"
onClick={props.onDataClick}
title={t("Convert property to data function")}
>
<MdInsertChart />
</InputButton>; </InputButton>;
let makeDataInputButton;
if (this.props.fieldSpec["property-type"] === "data-driven") {
makeDataInputButton = <InputButton
className="maputnik-make-data-function"
onClick={this.props.onDataClick}
title={t("Convert property to data function")}
>
<MdInsertChart />
</InputButton>;
}
return <div>
{expressionInputButton}
{makeDataInputButton}
{makeZoomInputButton}
</div>;
} else if (this.props.fieldSpec.expression?.parameters.includes("elevation")) {
const inputElevationButton = <InputButton
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>;
} }
return <div>
{expressionInputButton}
{makeDataInputButton}
{makeZoomInputButton}
</div>;
} else if (props.fieldSpec.expression?.parameters.includes("elevation")) {
const inputElevationButton = <InputButton
className="maputnik-make-elevation-function"
onClick={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>;
} }
}; }
export const FunctionInputButtons = withTranslation()(FunctionInputButtonsInternal); export const FunctionInputButtons = withTranslation()(FunctionInputButtonsInternal);
+94 -60
View File
@@ -1,4 +1,4 @@
import React, { useState } from "react"; import React from "react";
import { InputString } from "./InputString"; import { InputString } from "./InputString";
import { InputNumber } from "./InputNumber"; import { InputNumber } from "./InputNumber";
@@ -12,72 +12,106 @@ export type InputArrayProps = {
label?: string label?: string
}; };
export const InputArray: React.FC<InputArrayProps> = ({ type InputArrayState = {
value: propsValue = [], value: (string | number | undefined)[]
default: propsDefault = [], initialPropsValue: unknown[]
...rest };
}) => {
const props = { value: propsValue, default: propsDefault, ...rest };
// The original seeded this from props and then never let props overwrite it export class InputArray extends React.Component<InputArrayProps, InputArrayState> {
// again (its getDerivedStateFromProps assigned the existing state back in static defaultProps = {
// both branches), so the value is owned by this component after mount. value: [],
const [value, setValue] = useState<(string | number | undefined)[]>(() => propsValue.slice(0)); default: [],
};
function isComplete(val: unknown[]) { constructor (props: InputArrayProps) {
return Array(props.length).fill(null).every((_, i) => { super(props);
const v = val[i]; this.state = {
return !(v === undefined || v === ""); value: this.props.value.slice(0),
// This is so we can compare changes in getDerivedStateFromProps
initialPropsValue: this.props.value.slice(0),
};
}
static getDerivedStateFromProps(props: Readonly<InputArrayProps>, state: InputArrayState) {
const value: any[] = [];
const initialPropsValue = state.initialPropsValue.slice(0);
Array(props.length).fill(null).map((_, i) => {
if (props.value[i] === state.initialPropsValue[i]) {
value[i] = state.value[i];
}
else {
value[i] = state.value[i];
initialPropsValue[i] = state.value[i];
}
});
return {
value,
initialPropsValue,
};
}
isComplete(value: unknown[]) {
return Array(this.props.length).fill(null).every((_, i) => {
const val = value[i];
return !(val === undefined || val === "");
}); });
} }
function changeValue(idx: number, newValue: string | number | undefined) { changeValue(idx: number, newValue: string | number | undefined) {
const nextValue = value.slice(0); const value = this.state.value.slice(0);
nextValue[idx] = newValue; value[idx] = newValue;
setValue(nextValue); this.setState({
value,
if (isComplete(nextValue) && props.onChange) { }, () => {
props.onChange(nextValue); if (this.isComplete(value) && this.props.onChange) {
} this.props.onChange(value);
else if (props.onChange) { }
// Unset until complete else if (this.props.onChange){
props.onChange(undefined); // Unset until complete
} this.props.onChange(undefined);
}
});
} }
const containsValues = ( render() {
value.length > 0 && const {value} = this.state;
!value.every(val => {
return (val === "" || val === undefined);
})
);
const inputs = Array(props.length).fill(null).map((_, i) => { const containsValues = (
if(props.type === "number") { value.length > 0 &&
return <InputNumber !value.every(val => {
key={i} return (val === "" || val === undefined);
default={containsValues || !props.default ? undefined : props.default[i] as number} })
value={value[i] as number} );
required={containsValues ? true : false}
onChange={(v) => changeValue(i, v)}
aria-label={props["aria-label"] || props.label}
/>;
} else {
return <InputString
key={i}
default={containsValues || !props.default ? undefined : props.default[i] as string}
value={value[i] as string}
required={containsValues ? true : false}
onChange={(v) => changeValue(i, v)}
aria-label={props["aria-label"] || props.label}
/>;
}
});
return ( const inputs = Array(this.props.length).fill(null).map((_, i) => {
<div className="maputnik-array"> if(this.props.type === "number") {
{inputs} return <InputNumber
</div> key={i}
); default={containsValues || !this.props.default ? undefined : this.props.default[i] as number}
}; value={value[i] as number}
required={containsValues ? true : false}
onChange={(v) => this.changeValue(i, v)}
aria-label={this.props["aria-label"] || this.props.label}
/>;
} else {
return <InputString
key={i}
default={containsValues || !this.props.default ? undefined : this.props.default[i] as string}
value={value[i] as string}
required={containsValues ? true : false}
onChange={this.changeValue.bind(this, i)}
aria-label={this.props["aria-label"] || this.props.label}
/>;
}
});
return (
<div className="maputnik-array">
{inputs}
</div>
);
}
}
+17 -15
View File
@@ -14,18 +14,20 @@ type InputButtonProps = {
title?: string title?: string
}; };
export const InputButton: React.FC<InputButtonProps> = (props) => { export class InputButton extends React.Component<InputButtonProps> {
return <button render() {
id={props.id} return <button
title={props.title} id={this.props.id}
type={props.type} title={this.props.title}
onClick={props.onClick} type={this.props.type}
disabled={props.disabled} onClick={this.props.onClick}
aria-label={props["aria-label"]} disabled={this.props.disabled}
className={classnames("maputnik-button", props.className)} aria-label={this.props["aria-label"]}
data-wd-key={props["data-wd-key"]} className={classnames("maputnik-button", this.props.className)}
style={props.style} data-wd-key={this.props["data-wd-key"]}
> style={this.props.style}
{props.children} >
</button>; {this.props.children}
}; </button>;
}
}
+27 -21
View File
@@ -6,26 +6,32 @@ export type InputCheckboxProps = {
onChange(...args: unknown[]): unknown onChange(...args: unknown[]): unknown
}; };
export const InputCheckbox: React.FC<InputCheckboxProps> = ({value = false, ...props}) => { export class InputCheckbox extends React.Component<InputCheckboxProps> {
const onChange = () => { static defaultProps = {
props.onChange(!value); value: false,
}; };
return <div className="maputnik-checkbox-wrapper"> onChange = () => {
<input this.props.onChange(!this.props.value);
className="maputnik-checkbox" };
type="checkbox"
style={props.style} render() {
onChange={onChange} return <div className="maputnik-checkbox-wrapper">
onClick={onChange} <input
checked={value} className="maputnik-checkbox"
/> type="checkbox"
<div className="maputnik-checkbox-box"> style={this.props.style}
<svg style={{ onChange={this.onChange}
display: value ? "inline" : "none" onClick={this.onChange}
}} className="maputnik-checkbox-icon" viewBox='0 0 32 32'> checked={this.props.value}
<path d='M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z' /> />
</svg> <div className="maputnik-checkbox-box">
</div> <svg style={{
</div>; display: this.props.value ? "inline" : "none"
}; }} className="maputnik-checkbox-icon" viewBox='0 0 32 32'>
<path d='M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z' />
</svg>
</div>
</div>;
}
}
+77 -76
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useRef, useState } 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 {type ColorResult} from "react-color"; import {type ColorResult} from "react-color";
@@ -20,27 +20,26 @@ export type InputColorProps = {
}; };
/*** Number fields with support for min, max and units and documentation*/ /*** Number fields with support for min, max and units and documentation*/
export const InputColor: React.FC<InputColorProps> = (props) => { export class InputColor extends React.Component<InputColorProps> {
const [pickerOpened, setPickerOpened] = useState(false); state = {
const colorInput = useRef<HTMLInputElement | null>(null); pickerOpened: false
};
colorInput: HTMLInputElement | null = null;
// Keep the latest `onChange` available to the throttled callback, which is constructor (props: InputColorProps) {
// created only once so that throttling actually takes effect. super(props);
const onChangeProp = useRef(props.onChange); this.onChangeNoCheck = lodash.throttle(this.onChangeNoCheck, 1000/30);
useEffect(() => { }
onChangeProp.current = props.onChange;
});
const onChangeNoCheck = useMemo( onChangeNoCheck(v: string) {
() => lodash.throttle((v: string) => onChangeProp.current(v), 1000/30), this.props.onChange(v);
[] }
);
//TODO: I much rather would do this with absolute positioning //TODO: I much rather would do this with absolute positioning
//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
const calcPickerOffset = () => { calcPickerOffset = () => {
const elem = colorInput.current; const elem = this.colorInput;
if(elem) { if(elem) {
const pos = elem.getBoundingClientRect(); const pos = elem.getBoundingClientRect();
return { return {
@@ -55,80 +54,82 @@ export const InputColor: React.FC<InputColorProps> = (props) => {
} }
}; };
const togglePicker = () => { togglePicker = () => {
setPickerOpened(opened => !opened); this.setState({ pickerOpened: !this.state.pickerOpened });
}; };
const getColor = () => { get color() {
// Catch invalid color. // Catch invalid color.
try { try {
return Color(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);
return Color("rgb(255,255,255)"); return Color("rgb(255,255,255)");
} }
}; }
const onChange = (v: string) => { onChange (v: string) {
props.onChange(v === "" ? undefined : v); this.props.onChange(v === "" ? undefined : v);
}; }
const offset = calcPickerOffset(); render() {
const currentColor = getColor().object(); const offset = this.calcPickerOffset();
const currentChromeColor = { const currentColor = this.color.object();
r: currentColor.r, const currentChromeColor = {
g: currentColor.g, r: currentColor.r,
b: currentColor.b, g: currentColor.g,
// Rename alpha -> a for ChromePicker b: currentColor.b,
a: currentColor.alpha! // Rename alpha -> a for ChromePicker
}; a: currentColor.alpha!
};
const picker = <div const picker = <div
className="maputnik-color-picker-offset"
style={{
position: "fixed",
zIndex: 1,
left: offset.left,
top: offset.top,
}}>
<ChromePicker
color={currentChromeColor}
onChange={c => onChangeNoCheck(formatColor(c))}
/>
<div
className="maputnik-color-picker-offset" className="maputnik-color-picker-offset"
onClick={togglePicker}
style={{ style={{
zIndex: -1,
position: "fixed", position: "fixed",
top: "0px", zIndex: 1,
right: "0px", left: offset.left,
bottom: "0px", top: offset.top,
left: "0px", }}>
}} <ChromePicker
/> color={currentChromeColor}
</div>; onChange={c => this.onChangeNoCheck(formatColor(c))}
/>
<div
className="maputnik-color-picker-offset"
onClick={this.togglePicker}
style={{
zIndex: -1,
position: "fixed",
top: "0px",
right: "0px",
bottom: "0px",
left: "0px",
}}
/>
</div>;
const swatchStyle = { const swatchStyle = {
backgroundColor: props.value backgroundColor: this.props.value
}; };
return <div className="maputnik-color-wrapper"> return <div className="maputnik-color-wrapper">
{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={props["aria-label"]} aria-label={this.props["aria-label"]}
spellCheck="false" spellCheck="false"
autoComplete="off" autoComplete="off"
className="maputnik-color" className="maputnik-color"
ref={colorInput} ref={(input) => {this.colorInput = input;}}
onClick={togglePicker} onClick={this.togglePicker}
style={props.style} style={this.props.style}
name={props.name} name={this.props.name}
placeholder={props.default} placeholder={this.props.default}
value={props.value ? props.value : ""} value={this.props.value ? this.props.value : ""}
onChange={(e) => onChange(e.target.value)} onChange={(e) => this.onChange(e.target.value)}
/> />
</div>; </div>;
}; }
}
+112 -106
View File
@@ -27,130 +27,136 @@ export type InputDynamicArrayProps = {
type InputDynamicArrayInternalProps = InputDynamicArrayProps & WithTranslation; type InputDynamicArrayInternalProps = InputDynamicArrayProps & WithTranslation;
const InputDynamicArrayInternal: React.FC<InputDynamicArrayInternalProps> = (props) => { class InputDynamicArrayInternal extends React.Component<InputDynamicArrayInternalProps> {
const values = props.value || props.default || []; changeValue(idx: number, newValue: string | number | undefined) {
const values = this.values.slice(0);
values[idx] = newValue;
if (this.props.onChange) this.props.onChange(values);
}
const changeValue = (idx: number, newValue: string | number | undefined) => { get values() {
const newValues = values.slice(0); return this.props.value || this.props.default || [];
newValues[idx] = newValue; }
if (props.onChange) props.onChange(newValues);
};
const addValue = () => { addValue = () => {
const newValues = values.slice(0); const values = this.values.slice(0);
if (props.type === "number") { if (this.props.type === "number") {
newValues.push(0); values.push(0);
} }
else if (props.type === "url") { else if (this.props.type === "url") {
newValues.push(""); values.push("");
} }
else if (props.type === "enum") { else if (this.props.type === "enum") {
const {fieldSpec} = props; const {fieldSpec} = this.props;
const defaultValue = Object.keys(fieldSpec!.values)[0]; const defaultValue = Object.keys(fieldSpec!.values)[0];
newValues.push(defaultValue); values.push(defaultValue);
} else if (props.type === "color") { } else if (this.props.type === "color") {
newValues.push("#000000"); values.push("#000000");
} else { } else {
newValues.push(""); values.push("");
} }
if (props.onChange) props.onChange(newValues); if (this.props.onChange) this.props.onChange(values);
}; };
const deleteValue = (valueIdx: number) => { deleteValue(valueIdx: number) {
const newValues = values.slice(0); const values = this.values.slice(0);
newValues.splice(valueIdx, 1); values.splice(valueIdx, 1);
if (props.onChange) props.onChange(newValues.length > 0 ? newValues : undefined); if (this.props.onChange) this.props.onChange(values.length > 0 ? values : undefined);
}; }
const t = props.t; render() {
const i18nProps = { t, i18n: props.i18n, tReady: props.tReady }; const t = this.props.t;
const inputs = values.map((v, i) => { const i18nProps = { t, i18n: this.props.i18n, tReady: this.props.tReady };
const deleteValueBtn= <DeleteValueInputButton const inputs = this.values.map((v, i) => {
onClick={deleteValue.bind(null, i)} const deleteValueBtn= <DeleteValueInputButton
{...i18nProps} onClick={this.deleteValue.bind(this, i)}
/>; {...i18nProps}
let input;
if(props.type === "url") {
input = <InputUrl
value={v as string}
onChange={changeValue.bind(null, i)}
aria-label={props["aria-label"] || props.label}
/>; />;
} let input;
else if (props.type === "number") { if(this.props.type === "url") {
input = <InputNumber input = <InputUrl
value={v as number} value={v as string}
onChange={changeValue.bind(null, i)} onChange={this.changeValue.bind(this, i)}
aria-label={props["aria-label"] || props.label} aria-label={this.props["aria-label"] || this.props.label}
/>; />;
} }
else if (props.type === "enum") { else if (this.props.type === "number") {
const options = Object.keys(props.fieldSpec?.values).map(v => [v, capitalize(v)]); input = <InputNumber
input = <InputEnum value={v as number}
options={options} onChange={this.changeValue.bind(this, i)}
value={v as string} aria-label={this.props["aria-label"] || this.props.label}
onChange={changeValue.bind(null, i)} />;
aria-label={props["aria-label"] || props.label} }
/>; else if (this.props.type === "enum") {
} const options = Object.keys(this.props.fieldSpec?.values).map(v => [v, capitalize(v)]);
else if (props.type === "color") { input = <InputEnum
input = <InputColor options={options}
value={v as string} value={v as string}
onChange={changeValue.bind(null, i)} onChange={this.changeValue.bind(this, i)}
aria-label={props["aria-label"] || props.label} aria-label={this.props["aria-label"] || this.props.label}
/>; />;
} }
else { else if (this.props.type === "color") {
input = <InputString input = <InputColor
value={v as string} value={v as string}
onChange={changeValue.bind(null, i)} onChange={this.changeValue.bind(this, i)}
aria-label={props["aria-label"] || props.label} aria-label={this.props["aria-label"] || this.props.label}
/>; />;
} }
else {
input = <InputString
value={v as string}
onChange={this.changeValue.bind(this, i)}
aria-label={this.props["aria-label"] || this.props.label}
/>;
}
return <div return <div
style={props.style} style={this.props.style}
key={i} key={i}
className="maputnik-array-block" className="maputnik-array-block"
>
<div className="maputnik-array-block-action">
{deleteValueBtn}
</div>
<div className="maputnik-array-block-content">
{input}
</div>
</div>;
});
return (
<div className="maputnik-array">
{inputs}
<InputButton
className="maputnik-array-add-value"
onClick={addValue}
> >
{t("Add value")} <div className="maputnik-array-block-action">
</InputButton> {deleteValueBtn}
</div> </div>
); <div className="maputnik-array-block-content">
}; {input}
</div>
</div>;
});
return (
<div className="maputnik-array">
{inputs}
<InputButton
className="maputnik-array-add-value"
onClick={this.addValue}
>
{t("Add value")}
</InputButton>
</div>
);
}
}
export const InputDynamicArray = withTranslation()(InputDynamicArrayInternal); export const InputDynamicArray = withTranslation()(InputDynamicArrayInternal);
type DeleteValueInputButtonProps = { type DeleteValueInputButtonProps = {
onClick?(...args: unknown[]): unknown onClick?(...args: unknown[]): unknown
} & WithTranslation; } & WithTranslation;
const DeleteValueInputButton: React.FC<DeleteValueInputButtonProps> = (props) => { class DeleteValueInputButton extends React.Component<DeleteValueInputButtonProps> {
const t = props.t; render() {
return <InputButton const t = this.props.t;
className="maputnik-delete-stop" return <InputButton
onClick={props.onClick} className="maputnik-delete-stop"
title={t("Remove array item")} onClick={this.props.onClick}
> title={t("Remove array item")}
<FieldDocLabel >
label={<MdDelete />} <FieldDocLabel
/> label={<MdDelete />}
</InputButton>; />
}; </InputButton>;
}
}
+20 -18
View File
@@ -25,23 +25,25 @@ export type InputEnumProps = {
}; };
export const InputEnum: React.FC<InputEnumProps> = (props) => { export class InputEnum extends React.Component<InputEnumProps> {
const {options, value, onChange, name, label} = props; render() {
const {options, value, onChange, name, label} = this.props;
if(options.length <= 3 && optionsLabelLength(options) <= 20) { if(options.length <= 3 && optionsLabelLength(options) <= 20) {
return <InputMultiInput return <InputMultiInput
name={name} name={name}
options={options} options={options}
value={(value || props.default)!} value={(value || this.props.default)!}
onChange={onChange} onChange={onChange}
aria-label={props["aria-label"] || label} aria-label={this.props["aria-label"] || label}
/>; />;
} else { } else {
return <InputSelect return <InputSelect
options={options} options={options}
value={(value || props.default)!} value={(value || this.props.default)!}
onChange={onChange} onChange={onChange}
aria-label={props["aria-label"] || label} aria-label={this.props["aria-label"] || label}
/>; />;
}
} }
}; }
+32 -28
View File
@@ -11,9 +11,13 @@ export type InputFontProps = {
"aria-label"?: string "aria-label"?: string
}; };
export const InputFont: React.FC<InputFontProps> = ({fonts = [], ...props}) => { export class InputFont extends React.Component<InputFontProps> {
const getValues = () => { static defaultProps = {
const out = props.value || props.default || []; fonts: []
};
get values() {
const out = this.props.value || this.props.default || [];
// Always put a "" in the last field to you can keep adding entries // Always put a "" in the last field to you can keep adding entries
if (out[out.length-1] !== ""){ if (out[out.length-1] !== ""){
@@ -22,36 +26,36 @@ export const InputFont: React.FC<InputFontProps> = ({fonts = [], ...props}) => {
else { else {
return out; return out;
} }
}; }
const values = getValues(); changeFont(idx: number, newValue: string) {
const changedValues = this.values.slice(0);
const changeFont = (idx: number, newValue: string) => {
const changedValues = 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 !== "");
props.onChange(filteredValues); this.props.onChange(filteredValues);
}; }
const inputs = values.map((value, i) => { render() {
return <li const inputs = this.values.map((value, i) => {
key={i} return <li
> key={i}
<InputAutocomplete >
aria-label={props["aria-label"] || props.name} <InputAutocomplete
value={value} aria-label={this.props["aria-label"] || this.props.name}
options={fonts.map(f => [f, f])} value={value}
onChange={changeFont.bind(null, i)} options={this.props.fonts?.map(f => [f, f])}
/> onChange={this.changeFont.bind(this, i)}
</li>; />
}); </li>;
});
return ( return (
<ul className="maputnik-font"> <ul className="maputnik-font">
{inputs} {inputs}
</ul> </ul>
); );
}; }
}
+87 -88
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useRef } from "react"; import React from "react";
import classnames from "classnames"; import classnames from "classnames";
import { type WithTranslation, withTranslation } from "react-i18next"; import { type WithTranslation, withTranslation } from "react-i18next";
@@ -25,49 +25,88 @@ export type InputJsonProps = {
}; };
type InputJsonInternalProps = InputJsonProps & WithTranslation; type InputJsonInternalProps = InputJsonProps & WithTranslation;
function getPrettyJson(data: any) { type InputJsonState = {
return stringifyPretty(data, {indent: 2, maxLength: 40}); isEditing: boolean
} prevValue: string
};
const InputJsonInternal: React.FC<InputJsonInternalProps> = ({ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJsonState> {
value, static defaultProps = {
className, onFocus: () => {},
onChange, onBlur: () => {},
onFocus = () => {}, withScroll: false
onBlur = () => {}, };
lintType, _view: EditorView | undefined;
spec, _el: HTMLDivElement | null = null;
withScroll = false, _cancelNextChange: boolean = false;
}) => {
const el = useRef<HTMLDivElement | null>(null);
const view = useRef<EditorView | undefined>(undefined);
const cancelNextChange = useRef<boolean>(false);
// `isEditing` and `prevValue` are never rendered, they are only read from the
// CodeMirror callbacks, so refs keep them up to date without re-rendering.
const isEditing = useRef<boolean>(false);
const prevValue = useRef<string>(getPrettyJson(value));
// Mirrors `prevProps.value` from the previous `componentDidUpdate`.
const prevValueProp = useRef<object>(value);
const handleFocus = () => { constructor(props: InputJsonInternalProps) {
if (onFocus) onFocus(); super(props);
isEditing.current = true; this.state = {
isEditing: false,
prevValue: this.getPrettyJson(this.props.value),
};
}
getPrettyJson(data: any) {
return stringifyPretty(data, {indent: 2, maxLength: 40});
}
componentDidMount () {
this._view = createEditor({
parent: this._el!,
value: this.getPrettyJson(this.props.value),
lintType: this.props.lintType || "layer",
onChange: (value:string) => this.onChange(value),
onFocus: () => this.onFocus(),
onBlur: () => this.onBlur(),
spec: this.props.spec
});
}
onFocus = () => {
if (this.props.onFocus) this.props.onFocus();
this.setState({
isEditing: true,
});
}; };
const handleBlur = () => { onBlur = () => {
if (onBlur) onBlur(); if (this.props.onBlur) this.props.onBlur();
isEditing.current = false; this.setState({
isEditing: false,
});
}; };
const handleChange = () => { componentDidUpdate(prevProps: InputJsonProps) {
if (cancelNextChange.current) { if (!this.state.isEditing && prevProps.value !== this.props.value) {
cancelNextChange.current = false; this._cancelNextChange = true;
prevValue.current = view.current!.state.doc.toString(); const transactionSpec: TransactionSpec = {
changes: {
from: 0,
to: this._view!.state.doc.length,
insert: this.getPrettyJson(this.props.value)
}
};
if (this.props.withScroll) {
transactionSpec.selection = this._view!.state.selection;
transactionSpec.scrollIntoView = true;
}
this._view!.dispatch(transactionSpec);
}
}
onChange = (_e: unknown) => {
if (this._cancelNextChange) {
this._cancelNextChange = false;
this.setState({
prevValue: this._view!.state.doc.toString(),
});
return; return;
} }
const newCode = view.current!.state.doc.toString(); const newCode = this._view!.state.doc.toString();
if (prevValue.current !== newCode) { if (this.state.prevValue !== newCode) {
let parsedLayer, err; let parsedLayer, err;
try { try {
parsedLayer = JSON.parse(newCode); parsedLayer = JSON.parse(newCode);
@@ -77,63 +116,23 @@ const InputJsonInternal: React.FC<InputJsonInternalProps> = ({
} }
if (!err) { if (!err) {
if (onChange) onChange(parsedLayer); if (this.props.onChange) this.props.onChange(parsedLayer);
} }
} }
prevValue.current = newCode; this.setState({
prevValue: newCode,
});
}; };
// The editor is created once on mount, so its callbacks have to go through a render() {
// ref to always see the latest props. return <div className="json-editor" data-wd-key="json-editor" aria-hidden="true" style={{cursor: "text"}}>
const handlers = useRef({handleChange, handleFocus, handleBlur}); <div
useEffect(() => { className={classnames("codemirror-container", this.props.className)}
handlers.current = {handleChange, handleFocus, handleBlur}; ref={(el) => {this._el = el;}}
}); />
</div>;
useEffect(() => { }
view.current = createEditor({ }
parent: el.current!,
value: getPrettyJson(value),
lintType: lintType || "layer",
onChange: () => handlers.current.handleChange(),
onFocus: () => handlers.current.handleFocus(),
onBlur: () => handlers.current.handleBlur(),
spec: spec
});
// Runs once on mount, mirroring the previous componentDidMount.
// eslint-disable-next-line react-hooks/exhaustive-deps -- mount-only: adding deps would rebuild the editor on every change
}, []);
useEffect(() => {
// Only react to an actual change of the `value` prop, which also skips the
// initial mount (the editor is created with the value already).
if (prevValueProp.current === value) return;
prevValueProp.current = value;
if (isEditing.current) return;
cancelNextChange.current = true;
const transactionSpec: TransactionSpec = {
changes: {
from: 0,
to: view.current!.state.doc.length,
insert: getPrettyJson(value)
}
};
if (withScroll) {
transactionSpec.selection = view.current!.state.selection;
transactionSpec.scrollIntoView = true;
}
view.current!.dispatch(transactionSpec);
}, [value, withScroll]);
return <div className="json-editor" data-wd-key="json-editor" aria-hidden="true" style={{cursor: "text"}}>
<div
className={classnames("codemirror-container", className)}
ref={el}
/>
</div>;
};
export const InputJson = withTranslation()(InputJsonInternal); export const InputJson = withTranslation()(InputJsonInternal);
+27 -25
View File
@@ -9,29 +9,31 @@ export type InputMultiInputProps = {
"aria-label"?: string "aria-label"?: string
}; };
export const InputMultiInput: React.FC<InputMultiInputProps> = (props) => { export class InputMultiInput extends React.Component<InputMultiInputProps> {
let options = props.options; render() {
if(options.length > 0 && !Array.isArray(options[0])) { let options = this.props.options;
options = options.map(v => [v, v]); if(options.length > 0 && !Array.isArray(options[0])) {
options = options.map(v => [v, v]);
}
const selectedValue = this.props.value || options[0][0];
const radios = options.map(([val, label])=> {
return <label
key={val}
className={classnames("maputnik-button", "maputnik-radio-as-button", {"maputnik-button-selected": val === selectedValue})}
>
<input type="radio"
name={this.props.name}
onChange={_e => this.props.onChange(val)}
value={val}
checked={val === selectedValue}
/>
{label}
</label>;
});
return <fieldset className="maputnik-multibutton" aria-label={this.props["aria-label"]}>
{radios}
</fieldset>;
} }
}
const selectedValue = props.value || options[0][0];
const radios = options.map(([val, label])=> {
return <label
key={val}
className={classnames("maputnik-button", "maputnik-radio-as-button", {"maputnik-button-selected": val === selectedValue})}
>
<input type="radio"
name={props.name}
onChange={_e => props.onChange(val)}
value={val}
checked={val === selectedValue}
/>
{label}
</label>;
});
return <fieldset className="maputnik-multibutton" aria-label={props["aria-label"]}>
{radios}
</fieldset>;
};
+169 -141
View File
@@ -1,4 +1,5 @@
import React, { type BaseSyntheticEvent, useRef, useState } from "react"; import React, { type BaseSyntheticEvent } from "react";
import { generateUniqueId } from "../libs/document-uid";
export type InputNumberProps = { export type InputNumberProps = {
value?: number value?: number
@@ -13,206 +14,233 @@ export type InputNumberProps = {
"aria-label"?: string "aria-label"?: string
}; };
export const InputNumber: React.FC<InputNumberProps> = (props) => { type InputNumberState = {
const { rangeStep = 1 } = props; uuid: number
editing: boolean
editingRange?: boolean
value?: number
/**
* This is the value that is currently being edited. It can be an invalid value.
*/
dirtyValue?: number | string | undefined
};
const [editing, setEditing] = useState(false); export class InputNumber extends React.Component<InputNumberProps, InputNumberState> {
const [editingRange, setEditingRange] = useState(false); static defaultProps = {
const [value, setValue] = useState<number | undefined>(props.value); rangeStep: 1
/** The value currently being edited. It can be an invalid value. */ };
const [dirtyValue, setDirtyValue] = useState<number | string | undefined>(props.value); _keyboardEvent: boolean = false;
const keyboardEvent = useRef(false); constructor(props: InputNumberProps) {
super(props);
// Replaces getDerivedStateFromProps: while not editing, track the prop. this.state = {
if (!editing && props.value !== value) { uuid: +generateUniqueId(),
setValue(props.value); editing: false,
setDirtyValue(props.value); value: props.value,
dirtyValue: props.value,
};
} }
function isValid(v: number | string | undefined) { static getDerivedStateFromProps(props: Readonly<InputNumberProps>, state: InputNumberState) {
if (!state.editing && props.value !== state.value) {
return {
value: props.value,
dirtyValue: props.value,
};
}
return null;
}
changeValue(newValue: number | string | undefined) {
const value = (newValue === "" || newValue === undefined) ?
undefined : +newValue;
const hasChanged = this.props.value !== value;
if(this.isValid(value) && hasChanged) {
if (this.props.onChange) this.props.onChange(value);
this.setState({
value: value,
});
}
else if (!this.isValid(value) && hasChanged) {
this.setState({
value: undefined,
});
}
this.setState({
dirtyValue: newValue === "" ? undefined : newValue,
});
}
isValid(v: number | string | undefined) {
if (v === undefined) { if (v === undefined) {
return true; return true;
} }
const val = +v; const value = +v;
if(isNaN(val)) { if(isNaN(value)) {
return false; return false;
} }
if(!isNaN(props.min!) && val < props.min!) { if(!isNaN(this.props.min!) && value < this.props.min!) {
return false; return false;
} }
if(!isNaN(props.max!) && val > props.max!) { if(!isNaN(this.props.max!) && value > this.props.max!) {
return false; return false;
} }
return true; return true;
} }
function changeValue(newValue: number | string | undefined) { resetValue = () => {
const val = (newValue === "" || newValue === undefined) ? this.setState({editing: false});
undefined : +newValue;
const hasChanged = props.value !== val;
if(isValid(val) && hasChanged) {
if (props.onChange) props.onChange(val);
setValue(val);
}
else if (!isValid(val) && hasChanged) {
setValue(undefined);
}
setDirtyValue(newValue === "" ? undefined : newValue);
}
const resetValue = () => {
setEditing(false);
// Reset explicitly to default value if value has been cleared // Reset explicitly to default value if value has been cleared
if(!value) { if(!this.state.value) {
return; return;
} }
// 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 (!isValid(value)) { if (!this.isValid(this.state.value)) {
if(isValid(props.value)) { if(this.isValid(this.props.value)) {
changeValue(props.value); this.changeValue(this.props.value);
setDirtyValue(props.value); this.setState({dirtyValue: this.props.value});
} else { } else {
changeValue(undefined); this.changeValue(undefined);
setDirtyValue(undefined); this.setState({dirtyValue: undefined});
} }
} }
}; };
const onChangeRange = (e: BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) => { onChangeRange = (e: BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) => {
let newValue = parseFloat(e.target.value); let value = parseFloat(e.target.value);
const step = rangeStep; const step = this.props.rangeStep;
let newDirtyValue = newValue; let dirtyValue = value;
if(step) { if(step) {
// Can't do this with the <input/> range step attribute else we won't be able to set a high precision value via the text input. // Can't do this with the <input/> range step attribute else we won't be able to set a high precision value via the text input.
const snap = newValue % step; const snap = value % step;
// Round up/down to step // Round up/down to step
if (keyboardEvent.current) { if (this._keyboardEvent) {
// If it's keyboard event we might get a low positive/negative value, // If it's keyboard event we might get a low positive/negative value,
// for example we might go from 13 to 13.23, however because we know // for example we might go from 13 to 13.23, however because we know
// that came from a keyboard event we always want to increase by a // that came from a keyboard event we always want to increase by a
// single step value. // single step value.
if (newValue < +dirtyValue!) { if (value < +this.state.dirtyValue!) {
newValue = value! - step; value = this.state.value! - step;
} }
else { else {
newValue = value! + step; value = this.state.value! + step;
} }
newDirtyValue = newValue; dirtyValue = value;
} }
else { else {
if (snap < step/2) { if (snap < step/2) {
newValue = newValue - snap; value = value - snap;
} }
else { else {
newValue = newValue + (step - snap); value = value + (step - snap);
} }
} }
} }
keyboardEvent.current = false; this._keyboardEvent = false;
// Clamp between min/max // Clamp between min/max
newValue = Math.max(props.min!, Math.min(props.max!, newValue)); value = Math.max(this.props.min!, Math.min(this.props.max!, value));
setValue(newValue); this.setState({value, dirtyValue});
setDirtyValue(newDirtyValue); if (this.props.onChange) this.props.onChange(value);
if (props.onChange) props.onChange(newValue);
}; };
if( render() {
Object.prototype.hasOwnProperty.call(props, "min") && if(
Object.prototype.hasOwnProperty.call(props, "max") && Object.prototype.hasOwnProperty.call(this.props, "min") &&
props.min !== undefined && props.max !== undefined && Object.prototype.hasOwnProperty.call(this.props, "max") &&
props.allowRange this.props.min !== undefined && this.props.max !== undefined &&
) { this.props.allowRange
const currentValue = editing ? dirtyValue : value; ) {
const defaultValue = props.default === undefined ? "" : props.default; const value = this.state.editing ? this.state.dirtyValue : this.state.value;
let inputValue; const defaultValue = this.props.default === undefined ? "" : this.props.default;
if (editingRange) { let inputValue;
inputValue = value; if (this.state.editingRange) {
inputValue = this.state.value;
}
else {
inputValue = value;
}
return <div className="maputnik-number-container">
<input
className="maputnik-number-range"
key="range"
type="range"
max={this.props.max}
min={this.props.min}
step="any"
spellCheck="false"
value={value === undefined ? defaultValue : value}
onChange={this.onChangeRange}
onKeyDown={() => {
this._keyboardEvent = true;
}}
onPointerDown={() => {
this.setState({editing: true, editingRange: true});
}}
onPointerUp={() => {
// Safari doesn't get onBlur event
this.setState({editing: false, editingRange: false});
}}
onBlur={() => {
this.setState({
editing: false,
editingRange: false,
dirtyValue: this.state.value,
});
}}
data-wd-key={this.props["data-wd-key"] + "-range"}
/>
<input
key="text"
type="text"
spellCheck="false"
className="maputnik-number"
placeholder={this.props.default?.toString()}
value={inputValue === undefined ? "" : inputValue}
onFocus={_e => {
this.setState({editing: true});
}}
onChange={e => {
this.changeValue(e.target.value);
}}
onBlur={_e => {
this.setState({editing: false});
this.resetValue();
}}
data-wd-key={this.props["data-wd-key"] + "-text"}
/>
</div>;
} }
else { else {
inputValue = currentValue; const value = this.state.editing ? this.state.dirtyValue : this.state.value;
}
return <div className="maputnik-number-container"> return <input
<input aria-label={this.props["aria-label"]}
className="maputnik-number-range"
key="range"
type="range"
max={props.max}
min={props.min}
step="any"
spellCheck="false"
value={currentValue === undefined ? defaultValue : currentValue}
onChange={onChangeRange}
onKeyDown={() => {
keyboardEvent.current = true;
}}
onPointerDown={() => {
setEditing(true);
setEditingRange(true);
}}
onPointerUp={() => {
// Safari doesn't get onBlur event
setEditing(false);
setEditingRange(false);
}}
onBlur={() => {
setEditing(false);
setEditingRange(false);
setDirtyValue(value);
}}
data-wd-key={props["data-wd-key"] + "-range"}
/>
<input
key="text"
type="text"
spellCheck="false" spellCheck="false"
className="maputnik-number" className="maputnik-number"
placeholder={props.default?.toString()} placeholder={this.props.default?.toString()}
value={inputValue === undefined ? "" : inputValue} value={value === undefined ? "" : value}
onFocus={_e => { onChange={e => this.changeValue(e.target.value)}
setEditing(true); onFocus={() => {
this.setState({editing: true});
}} }}
onChange={e => { onBlur={this.resetValue}
changeValue(e.target.value); required={this.props.required}
}} data-wd-key={this.props["data-wd-key"]}
onBlur={_e => { />;
setEditing(false); }
resetValue();
}}
data-wd-key={props["data-wd-key"] + "-text"}
/>
</div>;
} }
else { }
const currentValue = editing ? dirtyValue : value;
return <input
aria-label={props["aria-label"]}
spellCheck="false"
className="maputnik-number"
placeholder={props.default?.toString()}
value={currentValue === undefined ? "" : currentValue}
onChange={e => changeValue(e.target.value)}
onFocus={() => {
setEditing(true);
}}
onBlur={resetValue}
required={props.required}
data-wd-key={props["data-wd-key"]}
/>;
}
};
+19 -17
View File
@@ -10,21 +10,23 @@ export type InputSelectProps = {
"aria-label"?: string "aria-label"?: string
}; };
export const InputSelect: React.FC<InputSelectProps> = (props) => { export class InputSelect extends React.Component<InputSelectProps> {
let options = props.options; render() {
if(options.length > 0 && !Array.isArray(options[0])) { let options = this.props.options;
options = options.map((v) => [v, v]) as [string, any][]; if(options.length > 0 && !Array.isArray(options[0])) {
} options = options.map((v) => [v, v]) as [string, any][];
}
return <select return <select
className="maputnik-select" className="maputnik-select"
data-wd-key={props["data-wd-key"]} data-wd-key={this.props["data-wd-key"]}
style={props.style} style={this.props.style}
title={props.title} title={this.props.title}
value={props.value} value={this.props.value}
onChange={e => props.onChange(e.target.value)} onChange={e => this.props.onChange(e.target.value)}
aria-label={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>;
}; }
}
+40 -38
View File
@@ -38,31 +38,31 @@ export type InputSpecProps = {
/** 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 const InputSpec: React.FC<InputSpecProps> = (props) => { export class InputSpec extends React.Component<InputSpecProps> {
const childNodes = () => { childNodes() {
const commonProps = { const commonProps = {
fieldSpec: props.fieldSpec, fieldSpec: this.props.fieldSpec,
label: props.label, label: this.props.label,
action: props.action, action: this.props.action,
style: props.style, style: this.props.style,
value: props.value, value: this.props.value,
default: props.fieldSpec?.default, default: this.props.fieldSpec?.default,
name: props.fieldName, name: this.props.fieldName,
"data-wd-key": "spec-field-input:" + props.fieldName, "data-wd-key": "spec-field-input:" + this.props.fieldName,
onChange: (newValue: number | undefined | (string | number | undefined)[]) => props.onChange!(props.fieldName, newValue), onChange: (newValue: number | undefined | (string | number | undefined)[]) => this.props.onChange!(this.props.fieldName, newValue),
"aria-label": props["aria-label"], "aria-label": this.props["aria-label"],
}; };
switch(props.fieldSpec?.type) { switch(this.props.fieldSpec?.type) {
case "number": return ( case "number": return (
<InputNumber <InputNumber
{...commonProps as InputNumberProps} {...commonProps as InputNumberProps}
min={props.fieldSpec.minimum} min={this.props.fieldSpec.minimum}
max={props.fieldSpec.maximum} max={this.props.fieldSpec.maximum}
/> />
); );
case "enum": { case "enum": {
const options = Object.keys(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">}
@@ -72,8 +72,8 @@ export const InputSpec: React.FC<InputSpecProps> = (props) => {
case "resolvedImage": case "resolvedImage":
case "formatted": case "formatted":
case "string": case "string":
if (iconProperties.indexOf(props.fieldName!) >= 0) { if (iconProperties.indexOf(this.props.fieldName!) >= 0) {
const options = props.fieldSpec.values || []; const options = this.props.fieldSpec.values || [];
return <InputAutocomplete return <InputAutocomplete
{...commonProps as Omit<InputAutocompleteProps, "options">} {...commonProps as Omit<InputAutocompleteProps, "options">}
options={options.map(f => [f, f])} options={options.map(f => [f, f])}
@@ -94,59 +94,61 @@ export const InputSpec: React.FC<InputSpecProps> = (props) => {
/> />
); );
case "array": case "array":
if(props.fieldName === "text-font") { if(this.props.fieldName === "text-font") {
return <InputFont return <InputFont
{...commonProps as InputFontProps} {...commonProps as InputFontProps}
fonts={props.fieldSpec.values} fonts={this.props.fieldSpec.values}
/>; />;
} else { } else {
if (props.fieldSpec.length) { if (this.props.fieldSpec.length) {
return <InputArray return <InputArray
{...commonProps as InputArrayProps} {...commonProps as InputArrayProps}
type={props.fieldSpec.value} type={this.props.fieldSpec.value}
length={props.fieldSpec.length} length={this.props.fieldSpec.length}
/>; />;
} else { } else {
return <InputDynamicArray return <InputDynamicArray
{...commonProps as InputDynamicArrayProps} {...commonProps as InputDynamicArrayProps}
fieldSpec={props.fieldSpec} fieldSpec={this.props.fieldSpec}
type={props.fieldSpec.value as InputDynamicArrayProps["type"]} type={this.props.fieldSpec.value as InputDynamicArrayProps["type"]}
/>; />;
} }
} }
case "numberArray": return ( case "numberArray": return (
<InputDynamicArray <InputDynamicArray
{...commonProps as InputDynamicArrayProps} {...commonProps as InputDynamicArrayProps}
fieldSpec={props.fieldSpec} fieldSpec={this.props.fieldSpec}
type="number" type="number"
value={(Array.isArray(props.value) ? props.value : [props.value]) as (string | number | undefined)[]} value={(Array.isArray(this.props.value) ? this.props.value : [this.props.value]) as (string | number | undefined)[]}
/> />
); );
case "colorArray": return ( case "colorArray": return (
<InputDynamicArray <InputDynamicArray
{...commonProps as InputDynamicArrayProps} {...commonProps as InputDynamicArrayProps}
fieldSpec={props.fieldSpec} fieldSpec={this.props.fieldSpec}
type="color" type="color"
value={(Array.isArray(props.value) ? props.value : [props.value]) as (string | number | undefined)[]} value={(Array.isArray(this.props.value) ? this.props.value : [this.props.value]) as (string | number | undefined)[]}
/> />
); );
case "padding": return ( case "padding": return (
<InputArray <InputArray
{...commonProps as InputArrayProps} {...commonProps as InputArrayProps}
type="number" type="number"
value={(Array.isArray(props.value) ? props.value : [props.value]) as (string | number | undefined)[]} value={(Array.isArray(this.props.value) ? this.props.value : [this.props.value]) as (string | number | undefined)[]}
length={4} length={4}
/> />
); );
default: default:
console.warn(`No proper field input for ${props.fieldName} type: ${props.fieldSpec?.type}`); console.warn(`No proper field input for ${this.props.fieldName} type: ${this.props.fieldSpec?.type}`);
return null; return null;
} }
}; }
return ( render() {
<div data-wd-key={"spec-field:"+props.fieldName}> return (
{childNodes()} <div data-wd-key={"spec-field:"+this.props.fieldName}>
</div> {this.childNodes()}
); </div>
}; );
}
}
+82 -63
View File
@@ -1,4 +1,4 @@
import React, { useState } from "react"; import React from "react";
export type InputStringProps = { export type InputStringProps = {
"data-wd-key"?: string "data-wd-key"?: string
@@ -15,66 +15,85 @@ export type InputStringProps = {
title?: string title?: string
}; };
export const InputString: React.FC<InputStringProps> = (props) => { type InputStringState = {
const { onInput = () => {} } = props; editing: boolean
const [editing, setEditing] = useState(false); value?: string
const [value, setValue] = useState<string | undefined>(props.value);
// Replaces getDerivedStateFromProps: while the field is not being edited its
// value tracks the prop, so an external change to the style is picked up.
if (!editing && value !== props.value) {
setValue(props.value);
}
let tag;
let classes;
if(props.multi) {
tag = "textarea";
classes = [
"maputnik-string",
"maputnik-string--multi"
];
}
else {
tag = "input";
classes = [
"maputnik-string"
];
}
if(props.disabled) {
classes.push("maputnik-string--disabled");
}
return React.createElement(tag, {
"aria-label": props["aria-label"],
"data-wd-key": props["data-wd-key"],
spellCheck: Object.prototype.hasOwnProperty.call(props, "spellCheck") ? props.spellCheck : !(tag === "input"),
disabled: props.disabled,
className: classes.join(" "),
style: props.style,
value: value === undefined ? "" : value,
placeholder: props.default,
title: props.title,
onChange: (e: React.BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) => {
setEditing(true);
setValue(e.target.value);
onInput(e.target.value);
},
onBlur: () => {
// Note: editing is only cleared when the value actually changed; this
// mirrors the original and keeps a no-op blur from resyncing the value.
if(value !== props.value) {
setEditing(false);
if (props.onChange) props.onChange(value);
}
},
onKeyDown: (e: React.KeyboardEvent) => {
if (e.keyCode === 13 && props.onChange) {
props.onChange(value);
}
},
required: props.required,
});
}; };
export class InputString extends React.Component<InputStringProps, InputStringState> {
static defaultProps = {
onInput: () => {},
};
constructor(props: InputStringProps) {
super(props);
this.state = {
editing: false,
value: props.value || ""
};
}
static getDerivedStateFromProps(props: Readonly<InputStringProps>, state: InputStringState) {
if (!state.editing) {
return {
value: props.value
};
}
return {};
}
render() {
let tag;
let classes;
if(this.props.multi) {
tag = "textarea";
classes = [
"maputnik-string",
"maputnik-string--multi"
];
}
else {
tag = "input";
classes = [
"maputnik-string"
];
}
if(this.props.disabled) {
classes.push("maputnik-string--disabled");
}
return React.createElement(tag, {
"aria-label": this.props["aria-label"],
"data-wd-key": this.props["data-wd-key"],
spellCheck: Object.prototype.hasOwnProperty.call(this.props, "spellCheck") ? this.props.spellCheck : !(tag === "input"),
disabled: this.props.disabled,
className: classes.join(" "),
style: this.props.style,
value: this.state.value === undefined ? "" : this.state.value,
placeholder: this.props.default,
title: this.props.title,
onChange: (e: React.BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) => {
this.setState({
editing: true,
value: e.target.value
}, () => {
if (this.props.onInput) this.props.onInput(this.state.value);
});
},
onBlur: () => {
if(this.state.value!==this.props.value) {
this.setState({editing: false});
if (this.props.onChange) this.props.onChange(this.state.value);
}
},
onKeyDown: (e) => {
if (e.keyCode === 13 && this.props.onChange) {
this.props.onChange(this.state.value);
}
},
required: this.props.required,
});
}
}
+44 -25
View File
@@ -1,4 +1,4 @@
import React, { type JSX, useState } 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, type WithTranslation, withTranslation } from "react-i18next"; import { Trans, type WithTranslation, withTranslation } from "react-i18next";
@@ -46,30 +46,49 @@ export type FieldUrlProps = {
type InputUrlInternalProps = FieldUrlProps & WithTranslation; type InputUrlInternalProps = FieldUrlProps & WithTranslation;
const InputUrlInternal: React.FC<InputUrlInternalProps> = ({onInput = () => {}, ...props}) => { type InputUrlState = {
const [error, setError] = useState<ErrorType | undefined>(() => validate(props.value)); error?: ErrorType
const handleInput = (url: string) => {
setError(validate(url));
onInput(url);
};
const handleChange = (url: string) => {
setError(validate(url));
props.onChange(url);
};
return (
<div>
<InputString
{...props}
onInput={handleInput}
onChange={handleChange}
aria-label={props["aria-label"]}
/>
{errorTypeToJsx(error, props.t)}
</div>
);
}; };
class InputUrlInternal extends React.Component<InputUrlInternalProps, InputUrlState> {
static defaultProps = {
onInput: () => {},
};
constructor (props: InputUrlInternalProps) {
super(props);
this.state = {
error: validate(props.value),
};
}
onInput = (url: string) => {
this.setState({
error: validate(url),
});
if (this.props.onInput) this.props.onInput(url);
};
onChange = (url: string) => {
this.setState({
error: validate(url),
});
this.props.onChange(url);
};
render () {
return (
<div>
<InputString
{...this.props}
onInput={this.onInput}
onChange={this.onChange}
aria-label={this.props["aria-label"]}
/>
{errorTypeToJsx(this.state.error, this.props.t)}
</div>
);
}
}
export const InputUrl = withTranslation()(InputUrlInternal); export const InputUrl = withTranslation()(InputUrlInternal);
+201 -190
View File
@@ -1,4 +1,4 @@
import React, { type JSX, useState } 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";
@@ -17,9 +17,7 @@ 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";
// Aliased: the component defines its own changeProperty, which would otherwise import { changeType, changeProperty } from "../libs/layer";
// shadow this import (as a class method there was no collision).
import { changeType, changeProperty as changeLayerProperty } from "../libs/layer";
import { formatLayerId } from "../libs/format"; import { formatLayerId } from "../libs/format";
import { type WithTranslation, withTranslation } from "react-i18next"; import { type WithTranslation, withTranslation } from "react-i18next";
import { type TFunction } from "i18next"; import { type TFunction } from "i18next";
@@ -76,20 +74,20 @@ function getLayoutForType(type: LayerSpecification["type"], t: TFunction): Maput
return getLayoutForSymbolType(t); return getLayoutForSymbolType(t);
} }
const groups: MaputnikLayoutGroup[] = []; const groups: MaputnikLayoutGroup[] = [];
if (Object.keys(v8["paint_" + type]).length > 0) { if (Object.keys(v8["paint_" + type as keyof typeof v8]).length > 0) {
groups.push({ groups.push({
title: t("Paint properties"), title: t("Paint properties"),
id: "Paint_properties", id: "Paint_properties",
type: "properties", type: "properties",
fields: Object.keys(v8["paint_" + type]), fields: Object.keys(v8["paint_" + type as keyof typeof v8]),
}); });
} }
if (Object.keys(v8["layout_" + type]).length > 0) { if (Object.keys(v8["layout_" + type as keyof typeof v8]).length > 0) {
groups.push({ groups.push({
title: t("Layout properties"), title: t("Layout properties"),
id: "Layout_properties", id: "Layout_properties",
type: "properties", type: "properties",
fields: Object.keys(v8["layout_" + type]) fields: Object.keys(v8["layout_" + type as keyof typeof v8])
}); });
} }
return groups; return groups;
@@ -133,56 +131,67 @@ type LayerEditorInternalProps = {
errors?: MappedError[] errors?: MappedError[]
} & WithTranslation; } & WithTranslation;
type LayerEditorState = {
editorGroups: { [keys: string]: boolean }
};
/** Layer editor supporting multiple types of layers. */ /** Layer editor supporting multiple types of layers. */
const LayerEditorInternal: React.FC<LayerEditorInternalProps> = ({ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, LayerEditorState> {
onLayerChanged = () => { }, static defaultProps = {
onLayerIdChange = () => { }, onLayerChanged: () => { },
...rest onLayerIdChange: () => { },
}) => { onLayerDestroyed: () => { },
const props = { onLayerChanged, onLayerIdChange, ...rest } as LayerEditorInternalProps; };
const [editorGroups, setEditorGroups] = useState<{ [keys: string]: boolean }>(() => { constructor(props: LayerEditorInternalProps) {
const groups: { [keys: string]: boolean } = {}; super(props);
for (const group of layoutGroups(props.layer.type, props.t)) {
groups[group.title] = true;
}
return groups;
});
// Replaces getDerivedStateFromProps: groups that appear after mount (because const editorGroups: { [keys: string]: boolean } = {};
// the layer type changed) start out expanded. Guarded so it only sets state for (const group of layoutGroups(this.props.layer.type, props.t)) {
// when a group is genuinely new, otherwise this would loop every render. editorGroups[group.title] = true;
const newGroups = getLayoutForType(props.layer.type, props.t)
.filter(group => !(group.title in editorGroups));
if (newGroups.length > 0) {
const additionalGroups = { ...editorGroups };
for (const group of newGroups) {
additionalGroups[group.title] = true;
} }
setEditorGroups(additionalGroups);
this.state = { editorGroups };
} }
function changeProperty(group: keyof LayerSpecification | null, property: string, newValue: any) { static getDerivedStateFromProps(props: Readonly<LayerEditorInternalProps>, state: LayerEditorState) {
props.onLayerChanged( const additionalGroups = { ...state.editorGroups };
props.layerIndex,
changeLayerProperty(props.layer, group, property, newValue) for (const group of getLayoutForType(props.layer.type, props.t)) {
if (!(group.title in additionalGroups)) {
additionalGroups[group.title] = true;
}
}
return {
editorGroups: additionalGroups
};
}
changeProperty(group: keyof LayerSpecification | null, property: string, newValue: any) {
this.props.onLayerChanged(
this.props.layerIndex,
changeProperty(this.props.layer, group, property, newValue)
); );
} }
function onGroupToggle(groupTitle: string, active: boolean) { onGroupToggle(groupTitle: string, active: boolean) {
const changedActiveGroups = { const changedActiveGroups = {
...editorGroups, ...this.state.editorGroups,
[groupTitle]: active, [groupTitle]: active,
}; };
setEditorGroups(changedActiveGroups); this.setState({
editorGroups: changedActiveGroups
});
} }
function renderGroupType(type: string, fields?: string[]): JSX.Element { renderGroupType(type: string, fields?: string[]): JSX.Element {
let comment = ""; let comment = "";
if (props.layer.metadata) { if (this.props.layer.metadata) {
comment = (props.layer.metadata as any)["maputnik:comment"]; comment = (this.props.layer.metadata as any)["maputnik:comment"];
} }
const { errors, layerIndex } = props; const { errors, layerIndex } = this.props;
const errorData: MappedLayerErrors = {}; const errorData: MappedLayerErrors = {};
errors!.forEach(error => { errors!.forEach(error => {
@@ -198,85 +207,85 @@ const LayerEditorInternal: React.FC<LayerEditorInternalProps> = ({
}); });
let sourceLayerIds; let sourceLayerIds;
const layer = props.layer as Exclude<LayerSpecification, BackgroundLayerSpecification>; const layer = this.props.layer as Exclude<LayerSpecification, BackgroundLayerSpecification>;
if (Object.prototype.hasOwnProperty.call(props.sources, layer.source)) { if (Object.prototype.hasOwnProperty.call(this.props.sources, layer.source)) {
sourceLayerIds = props.sources[layer.source].layers; sourceLayerIds = this.props.sources[layer.source].layers;
} }
switch (type) { switch (type) {
case "layer": return <div> case "layer": return <div>
<FieldId <FieldId
value={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 => props.onLayerIdChange(props.layerIndex, props.layer.id, newId)} onChange={newId => this.props.onLayerIdChange(this.props.layerIndex, this.props.layer.id, newId)}
/> />
<FieldType <FieldType
disabled={true} disabled={true}
error={errorData.type} error={errorData.type}
value={props.layer.type} value={this.props.layer.type}
onChange={newType => props.onLayerChanged( onChange={newType => this.props.onLayerChanged(
props.layerIndex, this.props.layerIndex,
changeType(props.layer, newType) changeType(this.props.layer, newType)
)} )}
/> />
{props.layer.type !== "background" && <FieldSource {this.props.layer.type !== "background" && <FieldSource
wdKey="layer-editor.layer-source" wdKey="layer-editor.layer-source"
error={errorData.source} error={errorData.source}
sourceIds={Object.keys(props.sources!)} sourceIds={Object.keys(this.props.sources!)}
value={props.layer.source} value={this.props.layer.source}
onChange={v => changeProperty(null, "source", v)} onChange={v => this.changeProperty(null, "source", v)}
/> />
} }
{!NON_SOURCE_LAYERS.includes(props.layer.type) && {!NON_SOURCE_LAYERS.includes(this.props.layer.type) &&
<FieldSourceLayer <FieldSourceLayer
error={errorData["source-layer"]} error={errorData["source-layer"]}
sourceLayerIds={sourceLayerIds} sourceLayerIds={sourceLayerIds}
value={(props.layer as any)["source-layer"]} value={(this.props.layer as any)["source-layer"]}
onChange={v => changeProperty(null, "source-layer", v)} onChange={v => this.changeProperty(null, "source-layer", v)}
/> />
} }
<FieldMinZoom <FieldMinZoom
error={errorData.minzoom} error={errorData.minzoom}
value={props.layer.minzoom} value={this.props.layer.minzoom}
onChange={v => changeProperty(null, "minzoom", v)} onChange={v => this.changeProperty(null, "minzoom", v)}
/> />
<FieldMaxZoom <FieldMaxZoom
error={errorData.maxzoom} error={errorData.maxzoom}
value={props.layer.maxzoom} value={this.props.layer.maxzoom}
onChange={v => changeProperty(null, "maxzoom", v)} onChange={v => this.changeProperty(null, "maxzoom", v)}
/> />
<FieldComment <FieldComment
error={errorData.comment} error={errorData.comment}
value={comment} value={comment}
onChange={v => changeProperty("metadata", "maputnik:comment", v == "" ? undefined : v)} onChange={v => this.changeProperty("metadata", "maputnik:comment", v == "" ? undefined : v)}
/> />
</div>; </div>;
case "filter": return <div> case "filter": return <div>
<div className="maputnik-filter-editor-wrapper"> <div className="maputnik-filter-editor-wrapper">
<FilterEditor <FilterEditor
errors={errorData} errors={errorData}
filter={(props.layer as any).filter} filter={(this.props.layer as any).filter}
properties={props.vectorLayers[(props.layer as any)["source-layer"]]} properties={this.props.vectorLayers[(this.props.layer as any)["source-layer"]]}
onChange={f => changeProperty(null, "filter", f)} onChange={f => this.changeProperty(null, "filter", f)}
/> />
</div> </div>
</div>; </div>;
case "properties": case "properties":
return <PropertyGroup return <PropertyGroup
errors={errorData} errors={errorData}
layer={props.layer} layer={this.props.layer}
groupFields={fields!} groupFields={fields!}
spec={props.spec} spec={this.props.spec}
onChange={changeProperty.bind(null)} onChange={this.changeProperty.bind(this)}
/>; />;
case "jsoneditor": case "jsoneditor":
return <FieldJson return <FieldJson
lintType="layer" lintType="layer"
value={props.layer} value={this.props.layer}
onChange={(layer: LayerSpecification) => { onChange={(layer: LayerSpecification) => {
props.onLayerChanged( this.props.onLayerChanged(
props.layerIndex, this.props.layerIndex,
layer layer
); );
}} }}
@@ -285,128 +294,130 @@ const LayerEditorInternal: React.FC<LayerEditorInternalProps> = ({
} }
} }
function moveLayer(offset: number) { moveLayer(offset: number) {
props.onMoveLayer({ this.props.onMoveLayer({
oldIndex: props.layerIndex, oldIndex: this.props.layerIndex,
newIndex: props.layerIndex + offset newIndex: this.props.layerIndex + offset
}); });
} }
const t = props.t; render() {
const t = this.props.t;
const groupIds: string[] = []; const groupIds: string[] = [];
const layerType = 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);
return <LayerEditorGroup return <LayerEditorGroup
data-wd-key={group.title} data-wd-key={group.title}
id={groupId} id={groupId}
key={groupId} key={groupId}
title={group.title} title={group.title}
isActive={editorGroups[group.title]} isActive={this.state.editorGroups[group.title]}
onActiveToggle={onGroupToggle.bind(null, group.title)} onActiveToggle={this.onGroupToggle.bind(this, group.title)}
>
{renderGroupType(group.type, group.fields)}
</LayerEditorGroup>;
});
const layout = props.layer.layout || {};
const items: {
[key: string]: {
text: string,
handler: () => void,
disabled?: boolean,
wdKey?: string
}
} = {
delete: {
text: t("Delete"),
handler: () => props.onLayerDestroy(props.layerIndex),
wdKey: "menu-delete-layer"
},
duplicate: {
text: t("Duplicate"),
handler: () => props.onLayerCopy(props.layerIndex),
wdKey: "menu-duplicate-layer"
},
hide: {
text: (layout.visibility === "none") ? t("Show") : t("Hide"),
handler: () => props.onLayerVisibilityToggle(props.layerIndex),
wdKey: "menu-hide-layer"
},
moveLayerUp: {
text: t("Move layer up"),
disabled: props.isFirstLayer,
handler: () => moveLayer(-1),
wdKey: "menu-move-layer-up"
},
moveLayerDown: {
text: t("Move layer down"),
disabled: props.isLastLayer,
handler: () => moveLayer(+1),
wdKey: "menu-move-layer-down"
}
};
function handleSelection(id: string, event: React.SyntheticEvent) {
event.stopPropagation();
items[id].handler();
}
return <IconContext.Provider value={{ size: "14px", color: "#8e8e8e" }}>
<section className="maputnik-layer-editor"
role="main"
aria-label={t("Layer editor")}
data-wd-key="layer-editor"
>
<header data-wd-key="layer-editor.header">
<div className="layer-header">
<h2 className="layer-header__title">
{t("Layer")}: {formatLayerId(props.layer.id)}
</h2>
<div className="layer-header__info">
<Wrapper
className='more-menu'
onSelection={(id, event) => handleSelection(id as string, event)}
closeOnSelection={false}
>
<Button
id="skip-target-layer-editor"
data-wd-key="skip-target-layer-editor"
className='more-menu__button'
title={"Layer options"}>
<MdMoreVert className="more-menu__button__svg" />
</Button>
<Menu>
<ul className="more-menu__menu">
{Object.keys(items).map((id) => {
const item = items[id];
return <li key={id}>
<MenuItem value={id} className='more-menu__menu__item' data-wd-key={item.wdKey}>
{item.text}
</MenuItem>
</li>;
})}
</ul>
</Menu>
</Wrapper>
</div>
</div>
</header>
<Accordion
allowMultipleExpanded={true}
allowZeroExpanded={true}
preExpanded={groupIds}
> >
{groups} {this.renderGroupType(group.type, group.fields)}
</Accordion> </LayerEditorGroup>;
</section> });
</IconContext.Provider>;
}; const layout = this.props.layer.layout || {};
const items: {
[key: string]: {
text: string,
handler: () => void,
disabled?: boolean,
wdKey?: string
}
} = {
delete: {
text: t("Delete"),
handler: () => this.props.onLayerDestroy(this.props.layerIndex),
wdKey: "menu-delete-layer"
},
duplicate: {
text: t("Duplicate"),
handler: () => this.props.onLayerCopy(this.props.layerIndex),
wdKey: "menu-duplicate-layer"
},
hide: {
text: (layout.visibility === "none") ? t("Show") : t("Hide"),
handler: () => this.props.onLayerVisibilityToggle(this.props.layerIndex),
wdKey: "menu-hide-layer"
},
moveLayerUp: {
text: t("Move layer up"),
disabled: this.props.isFirstLayer,
handler: () => this.moveLayer(-1),
wdKey: "menu-move-layer-up"
},
moveLayerDown: {
text: t("Move layer down"),
disabled: this.props.isLastLayer,
handler: () => this.moveLayer(+1),
wdKey: "menu-move-layer-down"
}
};
function handleSelection(id: string, event: React.SyntheticEvent) {
event.stopPropagation();
items[id].handler();
}
return <IconContext.Provider value={{ size: "14px", color: "#8e8e8e" }}>
<section className="maputnik-layer-editor"
role="main"
aria-label={t("Layer editor")}
data-wd-key="layer-editor"
>
<header data-wd-key="layer-editor.header">
<div className="layer-header">
<h2 className="layer-header__title">
{t("Layer")}: {formatLayerId(this.props.layer.id)}
</h2>
<div className="layer-header__info">
<Wrapper
className='more-menu'
onSelection={(id, event) => handleSelection(id as string, event)}
closeOnSelection={false}
>
<Button
id="skip-target-layer-editor"
data-wd-key="skip-target-layer-editor"
className='more-menu__button'
title={"Layer options"}>
<MdMoreVert className="more-menu__button__svg" />
</Button>
<Menu>
<ul className="more-menu__menu">
{Object.keys(items).map((id) => {
const item = items[id];
return <li key={id}>
<MenuItem value={id} className='more-menu__menu__item' data-wd-key={item.wdKey}>
{item.text}
</MenuItem>
</li>;
})}
</ul>
</Menu>
</Wrapper>
</div>
</div>
</header>
<Accordion
allowMultipleExpanded={true}
allowZeroExpanded={true}
preExpanded={groupIds}
>
{groups}
</Accordion>
</section>
</IconContext.Provider>;
}
}
export const LayerEditor = withTranslation()(LayerEditorInternal); export const LayerEditor = withTranslation()(LayerEditorInternal);
+19 -17
View File
@@ -18,20 +18,22 @@ type LayerEditorGroupProps = {
}; };
export const LayerEditorGroup: React.FC<LayerEditorGroupProps> = (props) => { export class LayerEditorGroup extends React.Component<LayerEditorGroupProps> {
return <AccordionItem uuid={props.id}> render() {
<AccordionItemHeading className="maputnik-layer-editor-group" return <AccordionItem uuid={this.props.id}>
data-wd-key={"layer-editor-group:"+props["data-wd-key"]} <AccordionItemHeading className="maputnik-layer-editor-group"
onClick={_e => props.onActiveToggle(!props.isActive)} data-wd-key={"layer-editor-group:"+this.props["data-wd-key"]}
> onClick={_e => this.props.onActiveToggle(!this.props.isActive)}
<AccordionItemButton className="maputnik-layer-editor-group__button"> >
<span style={{flexGrow: 1, alignContent: "center"}}>{props.title}</span> <AccordionItemButton className="maputnik-layer-editor-group__button">
<MdArrowDropUp size={"2em"} className="maputnik-layer-editor-group__button__icon maputnik-layer-editor-group__button__icon--up"></MdArrowDropUp> <span style={{flexGrow: 1, alignContent: "center"}}>{this.props.title}</span>
<MdArrowDropDown size={"2em"} className="maputnik-layer-editor-group__button__icon maputnik-layer-editor-group__button__icon--down"></MdArrowDropDown> <MdArrowDropUp size={"2em"} className="maputnik-layer-editor-group__button__icon maputnik-layer-editor-group__button__icon--up"></MdArrowDropUp>
</AccordionItemButton> <MdArrowDropDown size={"2em"} className="maputnik-layer-editor-group__button__icon maputnik-layer-editor-group__button__icon--down"></MdArrowDropDown>
</AccordionItemHeading> </AccordionItemButton>
<AccordionItemPanel> </AccordionItemHeading>
{props.children} <AccordionItemPanel>
</AccordionItemPanel> {this.props.children}
</AccordionItem>; </AccordionItemPanel>
}; </AccordionItem>;
}
}
+229 -218
View File
@@ -1,4 +1,4 @@
import React, {type JSX, useEffect, useRef, useState} 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 {
@@ -37,98 +37,62 @@ type LayerListContainerProps = {
}; };
type LayerListContainerInternalProps = LayerListContainerProps & WithTranslation; type LayerListContainerInternalProps = LayerListContainerProps & WithTranslation;
const noopLayerSelect = () => {}; type LayerListContainerState = {
collapsedGroups: {[ket: string]: boolean}
// Replaces the previous `shouldComponentUpdate`. Note the inversion: this areAllGroupsExpanded: boolean
// returns true when the props are EQUAL (i.e. no re-render is needed), whereas keys: {[key: string]: number}
// `shouldComponentUpdate` returned true when a re-render WAS needed. isOpen: {[key: string]: boolean}
function arePropsEqual(prevProps: LayerListContainerInternalProps, nextProps: LayerListContainerInternalProps) { };
// This component tree only requires id and visibility from the layers
// objects
function getRequiredProps(layer: LayerSpecification) {
const out: {id: string, layout?: { visibility: any}} = {
id: layer.id,
};
if (layer.layout) {
out.layout = {
visibility: layer.layout.visibility
};
}
return out;
}
const layersEqual = lodash.isEqual(
nextProps.layers.map(getRequiredProps),
prevProps.layers.map(getRequiredProps),
);
function withoutLayers(props: LayerListContainerInternalProps) {
const out = {
...props
} as LayerListContainerInternalProps & { layers?: any };
delete out["layers"];
return out;
}
// Compare the props without layers because we've already compared them
// efficiently above.
const propsEqual = lodash.isEqual(
withoutLayers(prevProps),
withoutLayers(nextProps)
);
return layersEqual && propsEqual;
}
// List of collapsible layer editors // List of collapsible layer editors
function LayerListContainerInternal({ class LayerListContainerInternal extends React.Component<LayerListContainerInternalProps, LayerListContainerState> {
layers: propsLayers, static defaultProps = {
selectedLayerIndex, onLayerSelect: () => {},
onLayersChange, };
onLayerSelect = noopLayerSelect, selectedItemRef: React.RefObject<any>;
onLayerDestroy, scrollContainerRef: React.RefObject<HTMLElement | null>;
onLayerCopy,
onLayerVisibilityToggle,
sources,
errors,
t,
}: LayerListContainerInternalProps) {
const selectedItemRef = useRef<any>(null);
const scrollContainerRef = useRef<HTMLElement | null>(null);
const hasMountedRef = useRef(false);
const [collapsedGroups, setCollapsedGroups] = useState<{[key: string]: boolean}>({}); constructor(props: LayerListContainerInternalProps) {
const [areAllGroupsExpanded, setAreAllGroupsExpanded] = useState(false); super(props);
const [keys, setKeys] = useState<{[key: string]: number}>(() => ({ this.selectedItemRef = React.createRef();
add: +generateUniqueId(), this.scrollContainerRef = React.createRef();
})); this.state = {
const [isOpen, setIsOpen] = useState<{[key: string]: boolean}>({ collapsedGroups: {},
add: false, areAllGroupsExpanded: false,
}); keys: {
add: +generateUniqueId(),
function toggleModal(modalName: string) { },
setKeys(prevKeys => ({ isOpen: {
...prevKeys, add: false,
[modalName]: +generateUniqueId(), }
})); };
setIsOpen(prevIsOpen => ({
...prevIsOpen,
[modalName]: !prevIsOpen[modalName]
}));
} }
const toggleLayers = () => { toggleModal(modalName: string) {
this.setState({
keys: {
...this.state.keys,
[modalName]: +generateUniqueId(),
},
isOpen: {
...this.state.isOpen,
[modalName]: !this.state.isOpen[modalName]
}
});
}
toggleLayers = () => {
let idx = 0; let idx = 0;
const newGroups: {[key:string]: boolean} = {}; const newGroups: {[key:string]: boolean} = {};
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] = areAllGroupsExpanded; newGroups[lookupKey] = this.state.areAllGroupsExpanded;
} }
layers.forEach((_layer) => { layers.forEach((_layer) => {
@@ -136,17 +100,19 @@ function LayerListContainerInternal({
}); });
}); });
setCollapsedGroups(newGroups); this.setState({
setAreAllGroupsExpanded(!areAllGroupsExpanded); collapsedGroups: newGroups,
areAllGroupsExpanded: !this.state.areAllGroupsExpanded
});
}; };
function 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 < propsLayers.length; i++) { for (let i = 0; i < this.props.layers.length; i++) {
const origLayer = propsLayers[i]; const origLayer = this.props.layers[i];
const previousLayer = propsLayers[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
); );
@@ -164,162 +130,207 @@ function LayerListContainerInternal({
return groups; return groups;
} }
function toggleLayerGroup(groupPrefix: string, idx: number) { toggleLayerGroup(groupPrefix: string, idx: number) {
const lookupKey = [groupPrefix, idx].join("-"); const lookupKey = [groupPrefix, idx].join("-");
setCollapsedGroups(prevCollapsedGroups => { const newGroups = { ...this.state.collapsedGroups };
const newGroups = { ...prevCollapsedGroups }; if(lookupKey in this.state.collapsedGroups) {
if(lookupKey in prevCollapsedGroups) { newGroups[lookupKey] = !this.state.collapsedGroups[lookupKey];
newGroups[lookupKey] = !prevCollapsedGroups[lookupKey]; } else {
} else { newGroups[lookupKey] = false;
newGroups[lookupKey] = false; }
} this.setState({
return newGroups; collapsedGroups: newGroups
}); });
} }
function isCollapsed(groupPrefix: string, idx: number) { isCollapsed(groupPrefix: string, idx: number) {
const collapsed = collapsedGroups[[groupPrefix, idx].join("-")]; const collapsed = this.state.collapsedGroups[[groupPrefix, idx].join("-")];
return collapsed === undefined ? true : collapsed; return collapsed === undefined ? true : collapsed;
} }
useEffect(() => { shouldComponentUpdate (nextProps: LayerListContainerProps, nextState: LayerListContainerState) {
// `componentDidUpdate` did not run on mount, so skip the first run here too. // Always update on state change
if (!hasMountedRef.current) { if (this.state !== nextState) {
hasMountedRef.current = true; return true;
return;
} }
const selectedItemNode = selectedItemRef.current;
if (selectedItemNode && selectedItemNode.node) { // This component tree only requires id and visibility from the layers
const target = selectedItemNode.node; // objects
const options = { function getRequiredProps(layer: LayerSpecification) {
root: scrollContainerRef.current, const out: {id: string, layout?: { visibility: any}} = {
threshold: 1.0 id: layer.id,
}; };
const observer = new IntersectionObserver(entries => {
observer.unobserve(target);
if (entries.length > 0 && entries[0].intersectionRatio < 1) {
target.scrollIntoView();
}
}, options);
observer.observe(target); if (layer.layout) {
out.layout = {
visibility: layer.layout.visibility
};
}
return out;
} }
}, [selectedLayerIndex]); const layersEqual = lodash.isEqual(
nextProps.layers.map(getRequiredProps),
this.props.layers.map(getRequiredProps),
);
const listItems: JSX.Element[] = []; function withoutLayers(props: LayerListContainerProps) {
let idx = 0; const out = {
const layersByGroup = groupedLayers(); ...props
layersByGroup.forEach(layers => { } as LayerListContainerProps & { layers?: any };
const groupPrefix = layerPrefix(layers[0].id); delete out["layers"];
if(layers.length > 1) { return out;
const currentIdx = idx;
const grp = <LayerListGroup
data-wd-key={[groupPrefix, idx].join("-")}
aria-controls={layers.map(l => l.key).join(" ")}
key={`group-${groupPrefix}-${idx}`}
title={groupPrefix}
isActive={!isCollapsed(groupPrefix, idx) || idx === selectedLayerIndex}
onActiveToggle={() => toggleLayerGroup(groupPrefix, currentIdx)}
/>;
listItems.push(grp);
} }
layers.forEach((layer, idxInGroup) => { // Compare the props without layers because we've already compared them
const groupIdx = findClosestCommonPrefix(propsLayers, idx); // efficiently above.
const propsEqual = lodash.isEqual(
withoutLayers(this.props),
withoutLayers(nextProps)
);
const layerError = errors.find(error => { const propsChanged = !(layersEqual && propsEqual);
return ( return propsChanged;
error.parsed && }
error.parsed.type === "layer" &&
error.parsed.data.index == idx
);
});
const additionalProps: {ref?: React.RefObject<any>} = {}; componentDidUpdate (prevProps: LayerListContainerProps) {
if (idx === selectedLayerIndex) { if (prevProps.selectedLayerIndex !== this.props.selectedLayerIndex) {
additionalProps.ref = selectedItemRef; const selectedItemNode = this.selectedItemRef.current;
if (selectedItemNode && selectedItemNode.node) {
const target = selectedItemNode.node;
const options = {
root: this.scrollContainerRef.current,
threshold: 1.0
};
const observer = new IntersectionObserver(entries => {
observer.unobserve(target);
if (entries.length > 0 && entries[0].intersectionRatio < 1) {
target.scrollIntoView();
}
}, options);
observer.observe(target);
}
}
}
render() {
const listItems: JSX.Element[] = [];
let idx = 0;
const layersByGroup = this.groupedLayers();
layersByGroup.forEach(layers => {
const groupPrefix = layerPrefix(layers[0].id);
if(layers.length > 1) {
const grp = <LayerListGroup
data-wd-key={[groupPrefix, idx].join("-")}
aria-controls={layers.map(l => l.key).join(" ")}
key={`group-${groupPrefix}-${idx}`}
title={groupPrefix}
isActive={!this.isCollapsed(groupPrefix, idx) || idx === this.props.selectedLayerIndex}
onActiveToggle={this.toggleLayerGroup.bind(this, groupPrefix, idx)}
/>;
listItems.push(grp);
} }
const listItem = <LayerListItem layers.forEach((layer, idxInGroup) => {
className={classnames({ const groupIdx = findClosestCommonPrefix(this.props.layers, idx);
"maputnik-layer-list-item-collapsed": layers.length > 1 && isCollapsed(groupPrefix, groupIdx) && idx !== selectedLayerIndex,
"maputnik-layer-list-item-group-last": idxInGroup == layers.length - 1 && layers.length > 1,
"maputnik-layer-list-item--error": !!layerError
})}
key={layer.key}
id={layer.key}
layerId={layer.id}
layerIndex={idx}
layerType={layer.type}
visibility={(layer.layout || {}).visibility}
isSelected={idx === selectedLayerIndex}
onLayerSelect={onLayerSelect}
onLayerDestroy={onLayerDestroy}
onLayerCopy={onLayerCopy}
onLayerVisibilityToggle={onLayerVisibilityToggle}
{...additionalProps}
/>;
listItems.push(listItem);
idx += 1;
});
});
return <section const layerError = this.props.errors.find(error => {
className="maputnik-layer-list" return (
data-wd-key="layer-list" error.parsed &&
role="complementary" error.parsed.type === "layer" &&
aria-label={t("Layers list")} error.parsed.data.index == idx
ref={scrollContainerRef} );
> });
<ModalAdd
key={keys.add} const additionalProps: {ref?: React.RefObject<any>} = {};
layers={propsLayers} if (idx === this.props.selectedLayerIndex) {
sources={sources} additionalProps.ref = this.selectedItemRef;
isOpen={isOpen.add} }
onOpenToggle={() => toggleModal("add")}
onLayersChange={onLayersChange} const listItem = <LayerListItem
/> className={classnames({
<header className="maputnik-layer-list-header" data-wd-key="layer-list.header"> "maputnik-layer-list-item-collapsed": layers.length > 1 && this.isCollapsed(groupPrefix, groupIdx) && idx !== this.props.selectedLayerIndex,
<span className="maputnik-layer-list-header-title">{t("Layers")}</span> "maputnik-layer-list-item-group-last": idxInGroup == layers.length - 1 && layers.length > 1,
<span className="maputnik-space" /> "maputnik-layer-list-item--error": !!layerError
<div className="maputnik-default-property"> })}
<div className="maputnik-multibutton"> key={layer.key}
<button id={layer.key}
id="skip-target-layer-list" layerId={layer.id}
data-wd-key="skip-target-layer-list" layerIndex={idx}
onClick={toggleLayers} layerType={layer.type}
className="maputnik-button"> visibility={(layer.layout || {}).visibility}
{areAllGroupsExpanded === true ? isSelected={idx === this.props.selectedLayerIndex}
t("Collapse") onLayerSelect={this.props.onLayerSelect}
: onLayerDestroy={this.props.onLayerDestroy?.bind(this)}
t("Expand") onLayerCopy={this.props.onLayerCopy.bind(this)}
} onLayerVisibilityToggle={this.props.onLayerVisibilityToggle.bind(this)}
</button> {...additionalProps}
</div> />;
</div> listItems.push(listItem);
<div className="maputnik-default-property"> idx += 1;
<div className="maputnik-multibutton"> });
<button });
onClick={() => toggleModal("add")}
data-wd-key="layer-list:add-layer" const t = this.props.t;
className="maputnik-button maputnik-button-selected">
{t("Add Layer")} return <section
</button> className="maputnik-layer-list"
</div> data-wd-key="layer-list"
</div> role="complementary"
</header>
<div
role="navigation"
aria-label={t("Layers list")} aria-label={t("Layers list")}
ref={this.scrollContainerRef}
> >
<ul className="maputnik-layer-list-container"> <ModalAdd
{listItems} key={this.state.keys.add}
</ul> layers={this.props.layers}
</div> sources={this.props.sources}
</section>; isOpen={this.state.isOpen.add}
onOpenToggle={this.toggleModal.bind(this, "add")}
onLayersChange={this.props.onLayersChange}
/>
<header className="maputnik-layer-list-header" data-wd-key="layer-list.header">
<span className="maputnik-layer-list-header-title">{t("Layers")}</span>
<span className="maputnik-space" />
<div className="maputnik-default-property">
<div className="maputnik-multibutton">
<button
id="skip-target-layer-list"
data-wd-key="skip-target-layer-list"
onClick={this.toggleLayers}
className="maputnik-button">
{this.state.areAllGroupsExpanded === true ?
t("Collapse")
:
t("Expand")
}
</button>
</div>
</div>
<div className="maputnik-default-property">
<div className="maputnik-multibutton">
<button
onClick={this.toggleModal.bind(this, "add")}
data-wd-key="layer-list:add-layer"
className="maputnik-button maputnik-button-selected">
{t("Add Layer")}
</button>
</div>
</div>
</header>
<div
role="navigation"
aria-label={t("Layers list")}
>
<ul className="maputnik-layer-list-container">
{listItems}
</ul>
</div>
</section>;
}
} }
const LayerListContainer = withTranslation()(React.memo(LayerListContainerInternal, arePropsEqual)); const LayerListContainer = withTranslation()(LayerListContainerInternal);
type LayerListProps = LayerListContainerProps & { type LayerListProps = LayerListContainerProps & {
onMoveLayer: OnMoveLayerCallback onMoveLayer: OnMoveLayerCallback
+22 -20
View File
@@ -9,24 +9,26 @@ type LayerListGroupProps = {
"aria-controls"?: string "aria-controls"?: string
}; };
export const LayerListGroup: React.FC<LayerListGroupProps> = (props) => { export class LayerListGroup extends React.Component<LayerListGroupProps> {
return <li className="maputnik-layer-list-group"> render() {
<div className="maputnik-layer-list-group-header" return <li className="maputnik-layer-list-group">
data-wd-key={"layer-list-group:"+props["data-wd-key"]} <div className="maputnik-layer-list-group-header"
onClick={_e => props.onActiveToggle(!props.isActive)} data-wd-key={"layer-list-group:"+this.props["data-wd-key"]}
> onClick={_e => this.props.onActiveToggle(!this.props.isActive)}
<button
className="maputnik-layer-list-group-title"
aria-controls={props["aria-controls"]}
aria-expanded={props.isActive}
> >
{props.title} <button
</button> className="maputnik-layer-list-group-title"
<span className="maputnik-space" /> aria-controls={this.props["aria-controls"]}
<Collapser aria-expanded={this.props.isActive}
style={{ height: 14, width: 14 }} >
isCollapsed={props.isActive} {this.props.title}
/> </button>
</div> <span className="maputnik-space" />
</li>; <Collapser
}; style={{ height: 14, width: 14 }}
isCollapsed={this.props.isActive}
/>
</div>
</li>;
}
}
+23 -21
View File
@@ -38,9 +38,9 @@ type IconActionProps = {
classBlockModifier?: string classBlockModifier?: string
}; };
const IconAction: React.FC<IconActionProps> = (props) => { class IconAction extends React.Component<IconActionProps> {
function renderIcon() { renderIcon() {
switch (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 />;
@@ -48,28 +48,30 @@ const IconAction: React.FC<IconActionProps> = (props) => {
} }
} }
const { classBlockName, classBlockModifier } = props; render() {
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}`;
if (classBlockModifier) { if (classBlockModifier) {
classAdditions += ` maputnik-layer-list-icon-action__${classBlockName}--${classBlockModifier}`; classAdditions += ` maputnik-layer-list-icon-action__${classBlockName}--${classBlockModifier}`;
}
} }
}
return <button return <button
tabIndex={-1} tabIndex={-1}
title={props.action} title={this.props.action}
className={`maputnik-layer-list-icon-action ${classAdditions}`} className={`maputnik-layer-list-icon-action ${classAdditions}`}
data-wd-key={props.wdKey} data-wd-key={this.props.wdKey}
onClick={props.onClick} onClick={this.props.onClick}
aria-hidden="true" aria-hidden="true"
> >
{renderIcon()} {this.renderIcon()}
</button>; </button>;
}; }
}
type LayerListItemProps = { type LayerListItemProps = {
id?: string id?: string
+205 -235
View File
@@ -1,4 +1,4 @@
import React, {useCallback, useEffect, useReducer, useRef} from "react"; import React from "react";
import {createRoot} from "react-dom/client"; import {createRoot} from "react-dom/client";
import MapLibreGl, {type LayerSpecification, type LngLat, type Map, type MapOptions, type SourceSpecification, type 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";
@@ -71,65 +71,199 @@ type MapMaplibreGlInternalProps = {
onChange(value: {center: LngLat, zoom: number, _from: "map" | "app"}): unknown onChange(value: {center: LngLat, zoom: number, _from: "map" | "app"}): unknown
} & WithTranslation; } & WithTranslation;
/** type MapMaplibreGlState = {
* Replacement for the previous `shouldComponentUpdate`. `React.memo` expects the map: Map | null;
* inverse: `true` means "props are equal, skip the render", whereas inspect: MaplibreInspect | null;
* `shouldComponentUpdate` returned `true` when it *should* re-render. geocoder: MaplibreGeocoder | null;
* As before, if the props cannot be serialized we treat them as equal and skip zoomControl: ZoomControl | null;
* the render ("no biggie, carry on"). zoom?: number;
*/ };
function arePropsEqual(prevProps: MapMaplibreGlInternalProps, nextProps: MapMaplibreGlInternalProps) {
let should = false; class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps, MapMaplibreGlState> {
try { static defaultProps = {
should = JSON.stringify(prevProps) !== JSON.stringify(nextProps); onMapLoaded: () => {},
} catch(_e) { onDataChange: () => {},
// no biggie, carry on onLayerSelect: () => {},
onChange: () => {},
options: {} as MapOptions,
};
container: HTMLDivElement | null = null;
constructor(props: MapMaplibreGlInternalProps) {
super(props);
this.state = {
map: null,
inspect: null,
geocoder: null,
zoomControl: null,
};
i18next.on("languageChanged", () => {
this.forceUpdate();
});
} }
return !should;
}
const MapMaplibreGlInternal = ({
onDataChange = () => {},
onLayerSelect = () => {},
onChange = () => {},
options = {},
mapStyle,
mapView,
inspectModeEnabled,
highlightedLayer,
replaceAccessTokens,
t,
}: MapMaplibreGlInternalProps) => {
const container = useRef<HTMLDivElement | null>(null);
// These used to live in `this.state`, but were never able to trigger a shouldComponentUpdate(nextProps: MapMaplibreGlInternalProps, nextState: MapMaplibreGlState) {
// re-render: `shouldComponentUpdate` stringified the state, which throws for let should = false;
// the (circular) maplibre `Map`, so the comparison was swallowed and the try {
// component only ever re-rendered on prop changes. They are imperative should = JSON.stringify(this.props) !== JSON.stringify(nextProps) || JSON.stringify(this.state) !== JSON.stringify(nextState);
// handles, so they are refs here and mutating them does not re-render. } catch(_e) {
const map = useRef<Map | null>(null); // no biggie, carry on
const inspect = useRef<MaplibreInspect | null>(null); }
const geocoder = useRef<MaplibreGeocoder | null>(null); return should;
const zoomControl = useRef<ZoomControl | null>(null); }
const zoom = useRef<number | undefined>(undefined);
// `componentDidUpdate` did not run on mount, so neither may the effect below. componentDidUpdate() {
const hasMounted = useRef(false); const map = this.state.map;
const [, forceUpdate] = useReducer((tick: number) => tick + 1, 0); const styleWithTokens = this.props.replaceAccessTokens(this.props.mapStyle);
if (map) {
// Maplibre GL now does diffing natively so we don't need to calculate
// the necessary operations ourselves!
// We also need to update the style for inspect to work properly
map.setStyle(styleWithTokens, {diff: true});
map.showTileBoundaries = this.props.options?.showTileBoundaries!;
map.showCollisionBoxes = this.props.options?.showCollisionBoxes!;
map.showOverdrawInspector = this.props.options?.showOverdrawInspector!;
// The maplibre event handlers/callbacks below are registered once (on mount) // set the map view when the prop was updated from outside
// but read `this.props` at call time in the class version, so they must not if (this.props.mapView._from === "app") {
// close over the props of the first render. map.jumpTo(this.props.mapView);
const latestProps = useRef({onDataChange, onLayerSelect, onChange, mapStyle, inspectModeEnabled, highlightedLayer, t}); }
latestProps.current = {onDataChange, onLayerSelect, onChange, mapStyle, inspectModeEnabled, highlightedLayer, t}; }
const onLayerSelectById = useCallback((id: string) => { if(this.state.inspect && this.props.inspectModeEnabled !== this.state.inspect._showInspectMap) {
const index = latestProps.current.mapStyle.layers.findIndex(layer => layer.id === id); this.state.inspect.toggleInspector();
latestProps.current.onLayerSelect(index); }
}, []); if (this.state.inspect && this.props.inspectModeEnabled) {
this.state.inspect.setOriginalStyle(styleWithTokens);
// In case the sources are the same, there's a need to refresh the style
setTimeout(() => {
this.state.inspect!.render();
}, 500);
}
const initGeocoder = useCallback((mapInstance: Map) => { }
componentDidMount() {
const mapOpts = {
...this.props.options,
container: this.container!,
style: this.props.mapStyle,
hash: true,
maxZoom: 24,
// make root relative urls in stylefiles work as maplibre gl js does
// not support this for everything:
// https://github.com/maplibre/maplibre-gl-js/issues/6818
transformRequest: (url) => {
if (url.startsWith("/")) {
url = `${window.location.origin}${url}`;
}
return { url };
},
// setting to always load glyphs of CJK fonts from server
// https://maplibre.org/maplibre-gl-js/docs/examples/local-ideographs/
localIdeographFontFamily: false
} satisfies MapOptions;
const protocol = new Protocol({metadata: true});
MapLibreGl.addProtocol("pmtiles",protocol.tile);
const map = new MapLibreGl.Map(mapOpts);
const mapViewChange = () => {
const center = map.getCenter();
const zoom = map.getZoom();
this.props.onChange({center, zoom, _from: "map"});
};
mapViewChange();
map.showTileBoundaries = mapOpts.showTileBoundaries!;
map.showCollisionBoxes = mapOpts.showCollisionBoxes!;
map.showOverdrawInspector = mapOpts.showOverdrawInspector!;
const geocoder = this.initGeocoder(map);
const zoomControl = new ZoomControl();
map.addControl(zoomControl, "top-right");
const nav = new MapLibreGl.NavigationControl({visualizePitch:true});
map.addControl(nav, "top-right");
const tmpNode = document.createElement("div");
const root = createRoot(tmpNode);
const inspectPopup = new MapLibreGl.Popup({
closeOnClick: false
});
const inspect = new MaplibreInspect({
popup: inspectPopup,
showMapPopup: true,
showMapPopupOnHover: false,
showInspectMapPopupOnHover: true,
showInspectButton: false,
blockHoverPopupOnClick: true,
assignLayerColor: (layerId: string, alpha: number) => {
return Color(colors.brightColor(layerId, alpha)).desaturate(0.5).string();
},
buildInspectStyle: (originalMapStyle: StyleSpecification, coloredLayers: HighlightedLayer[]) => buildInspectStyle(originalMapStyle, coloredLayers, this.props.highlightedLayer),
renderPopup: (features: InspectFeature[]) => {
if(this.props.inspectModeEnabled) {
inspectPopup.once("open", () => {
root.render(<MapMaplibreGlFeaturePropertyPopup features={features} />);
});
return tmpNode;
} else {
inspectPopup.once("open", () => {
root.render(<MapMaplibreGlLayerPopup
features={features}
onLayerSelect={this.onLayerSelectById}
zoom={this.state.zoom}
/>,);
});
return tmpNode;
}
}
});
map.addControl(inspect);
map.on("style.load", () => {
this.setState({
map,
inspect,
geocoder,
zoomControl,
zoom: map.getZoom()
});
});
map.on("data", e => {
if(e.dataType !== "tile") return;
this.props.onDataChange!({
map: this.state.map
});
});
map.on("error", e => {
console.log("ERROR", e);
});
map.on("zoom", _e => {
this.setState({
zoom: map.getZoom()
});
});
map.on("dragend", mapViewChange);
map.on("zoomend", mapViewChange);
}
onLayerSelectById = (id: string) => {
const index = this.props.mapStyle.layers.findIndex(layer => layer.id === id);
this.props.onLayerSelect(index);
};
initGeocoder(map: Map) {
const geocoderConfig = { const geocoderConfig = {
forwardGeocode: async (config: MaplibreGeocoderApiConfig) => { forwardGeocode: async (config: MaplibreGeocoderApiConfig) => {
const features = []; const features = [];
@@ -166,190 +300,26 @@ const MapMaplibreGlInternal = ({
}; };
}, },
} as unknown as MaplibreGeocoderApi; } as unknown as MaplibreGeocoderApi;
const geocoderInstance = new MaplibreGeocoder(geocoderConfig, { const geocoder = new MaplibreGeocoder(geocoderConfig, {
placeholder: latestProps.current.t("Search"), placeholder: this.props.t("Search"),
maplibregl: MapLibreGl, maplibregl: MapLibreGl,
}); });
mapInstance.addControl(geocoderInstance, "top-left"); map.addControl(geocoder, "top-left");
return geocoderInstance; return geocoder;
}, []); }
// Was the `i18next.on("languageChanged", () => this.forceUpdate())` in the render() {
// constructor. `forceUpdate` bypassed `shouldComponentUpdate`; a state update const t = this.props.t;
// likewise bypasses `React.memo`. this.state.geocoder?.setPlaceholder(t("Search"));
useEffect(() => { this.state.zoomControl?.setLabel(t("Zoom:"));
const onLanguageChanged = () => { return <div
forceUpdate(); className="maputnik-map__map"
}; role="region"
i18next.on("languageChanged", onLanguageChanged); aria-label={t("Map view")}
return () => { ref={x => {this.container = x;}}
i18next.off("languageChanged", onLanguageChanged); data-wd-key="maplibre:map"
}; ></div>;
}, []); }
}
// componentDidMount export const MapMaplibreGl = withTranslation()(MapMaplibreGlInternal);
useEffect(() => {
const mapOpts = {
...options,
container: container.current!,
style: mapStyle,
hash: true,
maxZoom: 24,
// make root relative urls in stylefiles work as maplibre gl js does
// not support this for everything:
// https://github.com/maplibre/maplibre-gl-js/issues/6818
transformRequest: (url) => {
if (url.startsWith("/")) {
url = `${window.location.origin}${url}`;
}
return { url };
},
// setting to always load glyphs of CJK fonts from server
// https://maplibre.org/maplibre-gl-js/docs/examples/local-ideographs/
localIdeographFontFamily: false
} satisfies MapOptions;
const protocol = new Protocol({metadata: true});
MapLibreGl.addProtocol("pmtiles",protocol.tile);
const mapInstance = new MapLibreGl.Map(mapOpts);
const mapViewChange = () => {
const center = mapInstance.getCenter();
const currentZoom = mapInstance.getZoom();
latestProps.current.onChange({center, zoom: currentZoom, _from: "map"});
};
mapViewChange();
mapInstance.showTileBoundaries = mapOpts.showTileBoundaries!;
mapInstance.showCollisionBoxes = mapOpts.showCollisionBoxes!;
mapInstance.showOverdrawInspector = mapOpts.showOverdrawInspector!;
const geocoderInstance = initGeocoder(mapInstance);
const zoomControlInstance = new ZoomControl();
mapInstance.addControl(zoomControlInstance, "top-right");
const nav = new MapLibreGl.NavigationControl({visualizePitch:true});
mapInstance.addControl(nav, "top-right");
const tmpNode = document.createElement("div");
const root = createRoot(tmpNode);
const inspectPopup = new MapLibreGl.Popup({
closeOnClick: false
});
const inspectInstance = new MaplibreInspect({
popup: inspectPopup,
showMapPopup: true,
showMapPopupOnHover: false,
showInspectMapPopupOnHover: true,
showInspectButton: false,
blockHoverPopupOnClick: true,
assignLayerColor: (layerId: string, alpha: number) => {
return Color(colors.brightColor(layerId, alpha)).desaturate(0.5).string();
},
buildInspectStyle: (originalMapStyle: StyleSpecification, coloredLayers: HighlightedLayer[]) => buildInspectStyle(originalMapStyle, coloredLayers, latestProps.current.highlightedLayer),
renderPopup: (features: InspectFeature[]) => {
if(latestProps.current.inspectModeEnabled) {
inspectPopup.once("open", () => {
root.render(<MapMaplibreGlFeaturePropertyPopup features={features} />);
});
return tmpNode;
} else {
inspectPopup.once("open", () => {
root.render(<MapMaplibreGlLayerPopup
features={features}
onLayerSelect={onLayerSelectById}
zoom={zoom.current}
/>,);
});
return tmpNode;
}
}
});
mapInstance.addControl(inspectInstance);
mapInstance.on("style.load", () => {
map.current = mapInstance;
inspect.current = inspectInstance;
geocoder.current = geocoderInstance;
zoomControl.current = zoomControlInstance;
zoom.current = mapInstance.getZoom();
});
mapInstance.on("data", e => {
if(e.dataType !== "tile") return;
latestProps.current.onDataChange!({
map: map.current
});
});
mapInstance.on("error", e => {
console.log("ERROR", e);
});
mapInstance.on("zoom", _e => {
zoom.current = mapInstance.getZoom();
});
mapInstance.on("dragend", mapViewChange);
mapInstance.on("zoomend", mapViewChange);
// Mount only, exactly like componentDidMount: the map is created from the
// props of the first render and kept up to date imperatively below (the
// handlers read `latestProps`, so no prop belongs in the dependencies). The
// class had no componentWillUnmount, so there is no teardown here either.
// eslint-disable-next-line react-hooks/exhaustive-deps -- mount-only: adding mapStyle/options would re-create the map
}, [initGeocoder, onLayerSelectById]);
// componentDidUpdate. It had no `prevProps` guards, so it ran after *every*
// re-render; the equivalent is an effect without a dependency array. Since the
// component only re-renders when `arePropsEqual` reports a prop change (or on
// a language change, as before), this runs exactly as often as it used to.
useEffect(() => {
if (!hasMounted.current) {
// componentDidUpdate does not run on mount.
hasMounted.current = true;
return;
}
const styleWithTokens = replaceAccessTokens(mapStyle);
if (map.current) {
// Maplibre GL now does diffing natively so we don't need to calculate
// the necessary operations ourselves!
// We also need to update the style for inspect to work properly
map.current.setStyle(styleWithTokens, {diff: true});
map.current.showTileBoundaries = options?.showTileBoundaries!;
map.current.showCollisionBoxes = options?.showCollisionBoxes!;
map.current.showOverdrawInspector = options?.showOverdrawInspector!;
// set the map view when the prop was updated from outside
if (mapView._from === "app") {
map.current.jumpTo(mapView);
}
}
if(inspect.current && inspectModeEnabled !== inspect.current._showInspectMap) {
inspect.current.toggleInspector();
}
if (inspect.current && inspectModeEnabled) {
inspect.current.setOriginalStyle(styleWithTokens);
// In case the sources are the same, there's a need to refresh the style
setTimeout(() => {
inspect.current!.render();
}, 500);
}
});
geocoder.current?.setPlaceholder(t("Search"));
zoomControl.current?.setLabel(t("Zoom:"));
return <div
className="maputnik-map__map"
role="region"
aria-label={t("Map view")}
ref={container}
data-wd-key="maplibre:map"
></div>;
};
export const MapMaplibreGl = withTranslation()(React.memo(MapMaplibreGlInternal, arePropsEqual));
@@ -63,13 +63,15 @@ type FeaturePropertyPopupProps = {
features: InspectFeature[] features: InspectFeature[]
}; };
export const FeaturePropertyPopup: React.FC<FeaturePropertyPopupProps> = (props) => { export class FeaturePropertyPopup extends React.Component<FeaturePropertyPopupProps> {
const features = removeDuplicatedFeatures(props.features); render() {
return <div className="maputnik-feature-property-popup" dir="ltr" data-wd-key="feature-property-popup"> const features = removeDuplicatedFeatures(this.props.features);
<table className="maputnik-popup-table"> return <div className="maputnik-feature-property-popup" dir="ltr" data-wd-key="feature-property-popup">
<tbody> <table className="maputnik-popup-table">
{features.map(renderFeature)} <tbody>
</tbody> {features.map(renderFeature)}
</table> </tbody>
</div>; </table>
}; </div>;
}
}
+40 -38
View File
@@ -32,8 +32,8 @@ type FeatureLayerPopupProps = {
zoom?: number zoom?: number
}; };
export const FeatureLayerPopup: React.FC<FeatureLayerPopupProps> = (props) => { export class FeatureLayerPopup extends React.Component<FeatureLayerPopupProps> {
function _getFeatureColor(feature: InspectFeature, _zoom?: number) { _getFeatureColor(feature: InspectFeature, _zoom?: number) {
// Guard because openlayers won't have this // Guard because openlayers won't have this
if (!feature.layer.paint) { if (!feature.layer.paint) {
return; return;
@@ -65,45 +65,47 @@ export const FeatureLayerPopup: React.FC<FeatureLayerPopupProps> = (props) => {
} }
} }
const sources = groupFeaturesBySourceLayer(props.features); render() {
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) => {
const featureColor = _getFeatureColor(feature, props.zoom); const featureColor = this._getFeatureColor(feature, this.props.zoom);
return <div return <div
key={idx} key={idx}
className="maputnik-popup-layer" className="maputnik-popup-layer"
>
<div
className="maputnik-popup-layer__swatch"
style={{background: featureColor}}
></div>
<label
className="maputnik-popup-layer__label"
onClick={() => {
props.onLayerSelect(feature.layer.id);
}}
> >
{feature.layer.type && <div
<IconLayer type={feature.layer.type} style={{ className="maputnik-popup-layer__swatch"
width: 14, style={{background: featureColor}}
height: 14, ></div>
paddingRight: 3 <label
}}/> className="maputnik-popup-layer__label"
} onClick={() => {
{feature.layer.id} this.props.onLayerSelect(feature.layer.id);
{feature.counter && <span> × {feature.counter}</span>} }}
</label> >
{feature.layer.type &&
<IconLayer type={feature.layer.type} style={{
width: 14,
height: 14,
paddingRight: 3
}}/>
}
{feature.layer.id}
{feature.counter && <span> × {feature.counter}</span>}
</label>
</div>;
});
return <div key={vectorLayerId}>
<div className="maputnik-popup-layer-id">{vectorLayerId}</div>
{layers}
</div>; </div>;
}); });
return <div key={vectorLayerId}>
<div className="maputnik-popup-layer-id">{vectorLayerId}</div>
{layers}
</div>;
});
return <div className="maputnik-feature-layer-popup" data-wd-key="feature-layer-popup" dir="ltr"> return <div className="maputnik-feature-layer-popup" data-wd-key="feature-layer-popup" dir="ltr">
{items} {items}
</div>; </div>;
}; }
}
+128 -139
View File
@@ -1,4 +1,4 @@
import {useCallback, useEffect, useMemo, useRef, useState} from "react"; import React from "react";
import {throttle} from "lodash"; import {throttle} from "lodash";
import { type WithTranslation, withTranslation } from "react-i18next"; import { type WithTranslation, withTranslation } from "react-i18next";
@@ -35,52 +35,56 @@ type MapOpenLayersInternalProps = {
onChange(...args: unknown[]): unknown onChange(...args: unknown[]): unknown
} & WithTranslation; } & WithTranslation;
const MapOpenLayersInternal = ({ type MapOpenLayersState = {
onLayerSelect = () => {}, zoom: string
mapStyle, rotation: string
style, cursor: string[]
debugToolbox, center: string[]
replaceAccessTokens, selectedFeatures?: any[]
onChange, };
t,
}: MapOpenLayersInternalProps) => {
const [zoom, setZoom] = useState("0");
const [rotation, setRotation] = useState("0");
const [cursor, setCursor] = useState<string[]>([]);
const [center, setCenter] = useState<string[]>([]);
// Never assigned in the class version either, kept for the popup below.
const [selectedFeatures] = useState<any[] | undefined>(undefined);
// Imperative handles: they were plain instance fields, so they are refs and class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps, MapOpenLayersState> {
// mutating them must not re-render. static defaultProps = {
const map = useRef<Map | null>(null); onMapLoaded: () => {},
const container = useRef<HTMLDivElement | null>(null); onDataChange: () => {},
const overlay = useRef<Overlay | undefined>(undefined); onLayerSelect: () => {},
const popupContainer = useRef<HTMLDivElement | null>(null); };
updateStyle: any;
map: any;
container: HTMLDivElement | null = null;
overlay: Overlay | undefined;
popupContainer: HTMLElement | null = null;
// componentDidUpdate did not run on mount, and componentDidMount already constructor(props: MapOpenLayersInternalProps) {
// applies the initial style, so the style effect below must skip the mount. super(props);
const hasMounted = useRef(false); this.state = {
zoom: "0",
rotation: "0",
cursor: [] as string[],
center: [],
};
this.updateStyle = throttle(this._updateStyle.bind(this), 200);
}
// The openlayers event handlers are registered on mount but read `this.props` _updateStyle(newMapStyle: StyleSpecification) {
// at call time in the class version. if(!this.map) return;
const latestProps = useRef({mapStyle, replaceAccessTokens, onChange});
latestProps.current = {mapStyle, replaceAccessTokens, onChange};
// Was `this.updateStyle = throttle(this._updateStyle.bind(this), 200)` in the
// constructor: created once per instance, so it is memoized once here.
const updateStyle = useMemo(() => throttle((newMapStyle: StyleSpecification) => {
if(!map.current) return;
// See <https://github.com/openlayers/ol-mapbox-style/issues/215#issuecomment-493198815> // See <https://github.com/openlayers/ol-mapbox-style/issues/215#issuecomment-493198815>
map.current.getLayers().clear(); this.map.getLayers().clear();
apply(map.current, newMapStyle); apply(this.map, newMapStyle);
}, 200), []); }
// componentDidMount componentDidUpdate(prevProps: MapOpenLayersInternalProps) {
useEffect(() => { if (this.props.mapStyle !== prevProps.mapStyle) {
overlay.current = new Overlay({ this.updateStyle(
element: popupContainer.current!, this.props.replaceAccessTokens(this.props.mapStyle)
);
}
}
componentDidMount() {
this.overlay = new Overlay({
element: this.popupContainer!,
autoPan: { autoPan: {
animation: { animation: {
duration: 250 duration: 250
@@ -88,131 +92,116 @@ const MapOpenLayersInternal = ({
}, },
}); });
const mapInstance = new Map({ const map = new Map({
target: container.current!, target: this.container!,
overlays: [overlay.current], overlays: [this.overlay],
view: new View({ view: new View({
zoom: 1, zoom: 1,
center: [180, -90], center: [180, -90],
}) })
}); });
mapInstance.on("pointermove", (evt) => { map.on("pointermove", (evt) => {
const coords = toLonLat(evt.coordinate); const coords = toLonLat(evt.coordinate);
setCursor([ this.setState({
coords[0].toFixed(2), cursor: [
coords[1].toFixed(2) coords[0].toFixed(2),
]); coords[1].toFixed(2)
]
});
}); });
const onMoveEnd = () => { const onMoveEnd = () => {
const currentZoom = mapInstance.getView().getZoom(); const zoom = map.getView().getZoom();
const currentCenter = toLonLat(mapInstance.getView().getCenter()!); const center = toLonLat(map.getView().getCenter()!);
latestProps.current.onChange({ this.props.onChange({
zoom: currentZoom, zoom,
center: { center: {
lng: currentCenter[0], lng: center[0],
lat: currentCenter[1], lat: center[1],
}, },
}); });
}; };
onMoveEnd(); onMoveEnd();
mapInstance.on("moveend", onMoveEnd); map.on("moveend", onMoveEnd);
mapInstance.on("postrender", (_e) => { map.on("postrender", (_e) => {
const currentCenter = toLonLat(mapInstance.getView().getCenter()!); const center = toLonLat(map.getView().getCenter()!);
setCenter([ this.setState({
currentCenter[0].toFixed(2), center: [
currentCenter[1].toFixed(2), center[0].toFixed(2),
]); center[1].toFixed(2),
setRotation(mapInstance.getView().getRotation().toFixed(2)); ],
setZoom(mapInstance.getView().getZoom()!.toFixed(2)); rotation: map.getView().getRotation().toFixed(2),
zoom: map.getView().getZoom()!.toFixed(2)
});
}); });
map.current = mapInstance; this.map = map;
updateStyle( this.updateStyle(
latestProps.current.replaceAccessTokens(latestProps.current.mapStyle) as StyleSpecification this.props.replaceAccessTokens(this.props.mapStyle)
); );
// Mount only, like componentDidMount. The class had no componentWillUnmount, }
// so there is no teardown here either. The props read above are read from
// latestProps/the first render on purpose, so they are not dependencies.
}, [updateStyle]);
// componentDidUpdate: `if (this.props.mapStyle !== prevProps.mapStyle)`. closeOverlay = (e: any) => {
useEffect(() => {
if (!hasMounted.current) {
// componentDidUpdate does not run on mount; componentDidMount already
// applied the initial style, and applying it twice would push a second
// (throttled) clear + apply through ol-mapbox-style.
hasMounted.current = true;
return;
}
updateStyle(
replaceAccessTokens(mapStyle) as StyleSpecification
);
// `replaceAccessTokens` is deliberately *not* a dependency: the parent
// re-creates it on every render, so depending on it would re-apply the style
// on every render instead of only when `mapStyle` changes, which is exactly
// what the `this.props.mapStyle !== prevProps.mapStyle` guard checked.
// eslint-disable-next-line react-hooks/exhaustive-deps -- see above: replaceAccessTokens is a new identity each render
}, [mapStyle, updateStyle]);
const closeOverlay = useCallback((e: any) => {
e.target.blur(); e.target.blur();
overlay.current!.setPosition(undefined); this.overlay!.setPosition(undefined);
}, []); };
return <div className="maputnik-ol-container"> render() {
<div const t = this.props.t;
ref={popupContainer} return <div className="maputnik-ol-container">
style={{background: "black"}} <div
className="maputnik-popup" ref={x => {this.popupContainer = x;}}
> style={{background: "black"}}
<button className="maputnik-popup"
className="maplibregl-popup-close-button"
onClick={closeOverlay}
aria-label={t("Close popup")}
> >
× <button
</button> className="maplibregl-popup-close-button"
<MapMaplibreGlLayerPopup onClick={this.closeOverlay}
features={selectedFeatures || []} aria-label={t("Close popup")}
onLayerSelect={onLayerSelect} >
/> ×
</div> </button>
<div className="maputnik-ol-zoom"> <MapMaplibreGlLayerPopup
{t("Zoom:")} {zoom} features={this.state.selectedFeatures || []}
</div> onLayerSelect={this.props.onLayerSelect}
{debugToolbox && />
<div className="maputnik-ol-debug">
<div>
<label>{t("cursor:")} </label>
<span>{renderCoords(cursor)}</span>
</div>
<div>
<label>{t("center:")} </label>
<span>{renderCoords(center)}</span>
</div>
<div>
<label>{t("rotation:")} </label>
<span>{rotation}</span>
</div>
</div> </div>
} <div className="maputnik-ol-zoom">
<div {t("Zoom:")} {this.state.zoom}
className="maputnik-ol" </div>
ref={container} {this.props.debugToolbox &&
role="region" <div className="maputnik-ol-debug">
aria-label={t("Map view")} <div>
style={{ <label>{t("cursor:")} </label>
...style, <span>{renderCoords(this.state.cursor)}</span>
}}> </div>
</div> <div>
</div>; <label>{t("center:")} </label>
}; <span>{renderCoords(this.state.center)}</span>
</div>
<div>
<label>{t("rotation:")} </label>
<span>{this.state.rotation}</span>
</div>
</div>
}
<div
className="maputnik-ol"
ref={x => {this.container = x;}}
role="region"
aria-label={t("Map view")}
style={{
...this.props.style,
}}>
</div>
</div>;
}
}
export const MapOpenLayers = withTranslation()(MapOpenLayersInternal); export const MapOpenLayers = withTranslation()(MapOpenLayersInternal);
+29 -27
View File
@@ -40,35 +40,37 @@ type PropertyGroupProps = {
errors?: MappedLayerErrors errors?: MappedLayerErrors
}; };
export const PropertyGroup: React.FC<PropertyGroupProps> = (props) => { export class PropertyGroup extends React.Component<PropertyGroupProps> {
const onPropertyChange = (property: string, newValue: any) => { onPropertyChange = (property: string, newValue: any) => {
const group = getGroupName(props.spec, props.layer.type, property); const group = getGroupName(this.props.spec, this.props.layer.type, property);
props.onChange(group ,property, newValue); this.props.onChange(group ,property, newValue);
}; };
const {errors} = props; render() {
const fields = props.groupFields.map(fieldName => { const {errors} = this.props;
const fieldSpec = getFieldSpec(props.spec, props.layer.type, fieldName); const fields = this.props.groupFields.map(fieldName => {
const fieldSpec = getFieldSpec(this.props.spec, this.props.layer.type, fieldName);
const paint = props.layer.paint || {}; const paint = this.props.layer.paint || {};
const layout = 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}
onChange={onPropertyChange} onChange={this.onPropertyChange}
key={fieldName} key={fieldName}
fieldName={fieldName} fieldName={fieldName}
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>;
}; }
}
+7 -5
View File
@@ -4,8 +4,10 @@ type ScrollContainerProps = {
children?: React.ReactNode children?: React.ReactNode
}; };
export const ScrollContainer: React.FC<ScrollContainerProps> = (props) => { export class ScrollContainer extends React.Component<ScrollContainerProps> {
return <div className="maputnik-scroll-container"> render() {
{props.children} return <div className="maputnik-scroll-container">
</div>; {this.props.children}
}; </div>;
}
}
+41 -38
View File
@@ -40,50 +40,53 @@ type SingleFilterEditorProps = {
properties?: {[key: string]: string} properties?: {[key: string]: string}
}; };
export const SingleFilterEditor: React.FC<SingleFilterEditorProps> = ({ export class SingleFilterEditor extends React.Component<SingleFilterEditorProps> {
properties = {}, static defaultProps = {
...props properties: {},
}) => { };
function 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, ""];
} }
props.onChange(newFilter); this.props.onChange(newFilter);
} }
const f = props.filter; render() {
const filterOp = f[0]; const f = this.props.filter;
const propertyName = f[1]; const filterOp = f[0];
const filterArgs = f.slice(2); const propertyName = f[1];
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">
<InputAutocomplete <InputAutocomplete
aria-label="key" aria-label="key"
value={propertyName} value={propertyName}
options={Object.keys(properties).map(propName => [propName, propName])} options={Object.keys(this.props.properties!).map(propName => [propName, propName])}
onChange={(newPropertyName: string) => onFilterPartChanged(filterOp, newPropertyName, filterArgs)} onChange={(newPropertyName: string) => this.onFilterPartChanged(filterOp, newPropertyName, filterArgs)}
/> />
</div> </div>
<div className="maputnik-filter-editor-operator"> <div className="maputnik-filter-editor-operator">
<InputSelect <InputSelect
aria-label="function" aria-label="function"
value={filterOp} value={filterOp}
onChange={(newFilterOp: string) => onFilterPartChanged(newFilterOp, propertyName, filterArgs)} onChange={(newFilterOp: string) => this.onFilterPartChanged(newFilterOp, propertyName, filterArgs)}
options={otherFilterOps} options={otherFilterOps}
/> />
</div> </div>
{filterArgs.length > 0 && {filterArgs.length > 0 &&
<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) => onFilterPartChanged(filterOp, propertyName, v.split(","))} onChange={(v: string) => this.onFilterPartChanged(filterOp, propertyName, v.split(","))}
/> />
</div> </div>
} }
</div>; </div>;
}; }
}
+10 -8
View File
@@ -7,13 +7,15 @@ type SmallErrorInternalProps = {
children?: React.ReactNode children?: React.ReactNode
} & WithTranslation; } & WithTranslation;
const SmallErrorInternal: React.FC<SmallErrorInternalProps> = (props) => { class SmallErrorInternal extends React.Component<SmallErrorInternalProps> {
const t = props.t; render () {
return ( const t = this.props.t;
<div className="SmallError"> return (
{t("Error:")} {props.children} <div className="SmallError">
</div> {t("Error:")} {this.props.children}
); </div>
}; );
}
}
export const SmallError = withTranslation()(SmallErrorInternal); export const SmallError = withTranslation()(SmallErrorInternal);
+23 -19
View File
@@ -19,26 +19,30 @@ type SpecPropertyProps = FieldSpecProps & {
}; };
export const SpecProperty: React.FC<SpecPropertyProps> = (props) => { export class SpecProperty extends React.Component<SpecPropertyProps> {
const {errors = {}, fieldName, fieldType} = props; static defaultProps = {
errors: {},
};
const functionBtn = <FunctionButtons render() {
fieldSpec={props.fieldSpec} const {errors, fieldName, fieldType} = this.props;
onZoomClick={props.onZoomClick}
onDataClick={props.onDataClick}
onExpressionClick={props.onExpressionClick}
onElevationClick={props.onElevationClick}
/>;
const error = errors[fieldType+"."+fieldName as any] as any; const functionBtn = <FunctionButtons
fieldSpec={this.props.fieldSpec}
onZoomClick={this.props.onZoomClick}
onDataClick={this.props.onDataClick}
onExpressionClick={this.props.onExpressionClick}
onElevationClick={this.props.onElevationClick}
/>;
const propsWithDefaults = {...props, errors}; const error = errors![fieldType+"."+fieldName as any] as any;
return <FieldSpec return <FieldSpec
{...propsWithDefaults} {...this.props}
error={error} error={error}
fieldSpec={props.fieldSpec} fieldSpec={this.props.fieldSpec}
label={labelFromFieldName(props.fieldName || "")} label={labelFromFieldName(this.props.fieldName || "")}
action={functionBtn} action={functionBtn}
/>; />;
}; }
}
+146 -122
View File
@@ -1,4 +1,4 @@
import React, { useRef } from "react"; import React from "react";
import { PiListPlusBold } from "react-icons/pi"; import { PiListPlusBold } from "react-icons/pi";
import { TbMathFunction } from "react-icons/tb"; import { TbMathFunction } from "react-icons/tb";
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json"; import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
@@ -68,21 +68,41 @@ type ZoomPropertyState = {
refs: {[key: number]: string} refs: {[key: number]: string}
}; };
const ZoomPropertyInternal: React.FC<ZoomPropertyInternalProps> = ({ errors = {}, ...rest }) => { class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, ZoomPropertyState> {
const props = { errors, ...rest } as ZoomPropertyInternalProps; static defaultProps = {
errors: {},
};
// The stop refs never reach the rendered output (the row key is derived from state = {
// the stop itself), so they live in a ref rather than state: keeping them in refs: {} as {[key: number]: string}
// state would mean setting state during render on every pass. };
const refs = useRef<{[key: number]: string}>({});
refs.current = setStopRefs(props, { refs: refs.current }); componentDidMount() {
const newRefs = setStopRefs(this.props, this.state);
if(newRefs) {
this.setState({
refs: newRefs
});
}
}
static getDerivedStateFromProps(props: Readonly<ZoomPropertyInternalProps>, state: ZoomPropertyState) {
const newRefs = setStopRefs(props, state);
if(newRefs) {
return {
refs: newRefs
};
}
return null;
}
// Order the stops altering the refs to reflect their new position. // Order the stops altering the refs to reflect their new position.
function orderStopsByZoom(stops: ZoomWithStops["stops"]) { orderStopsByZoom(stops: ZoomWithStops["stops"]) {
const mappedWithRef = stops const mappedWithRef = stops
.map((stop, idx) => { .map((stop, idx) => {
return { return {
ref: refs.current[idx], ref: this.state.refs[idx],
data: stop data: stop
}; };
}) })
@@ -96,43 +116,145 @@ const ZoomPropertyInternal: React.FC<ZoomPropertyInternalProps> = ({ errors = {}
newRefs[idx] = stop.ref; newRefs[idx] = stop.ref;
}); });
refs.current = newRefs; this.setState({
refs: newRefs
});
return mappedWithRef.map((item) => item.data); return mappedWithRef.map((item) => item.data);
} }
function changeZoomStop(changeIdx: number, stopData: number | undefined, value: number) { changeZoomStop(changeIdx: number, stopData: number | undefined, value: number) {
const stops = (props.value as ZoomWithStops).stops.slice(0); const stops = (this.props.value as ZoomWithStops).stops.slice(0);
stops[changeIdx] = [stopData, value]; stops[changeIdx] = [stopData, value];
const orderedStops = orderStopsByZoom(stops); const orderedStops = this.orderStopsByZoom(stops);
const changedValue = { const changedValue = {
...props.value as ZoomWithStops, ...this.props.value as ZoomWithStops,
stops: orderedStops stops: orderedStops
}; };
props.onChange!(props.fieldName, changedValue); this.props.onChange!(this.props.fieldName, changedValue);
} }
function changeBase(newValue: number | undefined) { changeBase(newValue: number | undefined) {
const changedValue = { const changedValue = {
...props.value, ...this.props.value,
base: newValue base: newValue
}; };
if (changedValue.base === undefined) { if (changedValue.base === undefined) {
delete changedValue["base"]; delete changedValue["base"];
} }
props.onChange!(props.fieldName, changedValue); this.props.onChange!(this.props.fieldName, changedValue);
} }
const changeDataType = (type: string) => { changeDataType = (type: string) => {
if (type !== "interpolate" && props.onChangeToDataFunction) { if (type !== "interpolate" && this.props.onChangeToDataFunction) {
props.onChangeToDataFunction(type); this.props.onChangeToDataFunction(type);
} }
}; };
function getDataFunctionTypes(fieldSpec: { render() {
const t = this.props.t;
const zoomFields = this.props.value?.stops.map((stop, idx) => {
const zoomLevel = stop[0];
const value = stop[1];
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop?.bind(this, idx)} />;
return <tr
key={`${stop[0]}-${stop[1]}`}
>
<td>
<InputNumber
aria-label={t("Zoom")}
value={zoomLevel}
onChange={changedStop => this.changeZoomStop(idx, changedStop, value)}
min={0}
max={22}
/>
</td>
<td>
<InputSpec
aria-label={t("Output value")}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec as any}
value={value}
onChange={(_, newValue) => this.changeZoomStop(idx, zoomLevel, newValue as number)}
/>
</td>
<td>
{deleteStopBtn}
</td>
</tr>;
});
// return <div className="maputnik-zoom-spec-property">
return <div className="maputnik-data-spec-block">
<fieldset className="maputnik-data-spec-property">
<legend>{labelFromFieldName(this.props.fieldName)}</legend>
<div className="maputnik-data-fieldset-inner">
<Block
label={t("Function")}
data-wd-key="function-type"
>
<div className="maputnik-data-spec-property-input">
<InputSelect
value={"interpolate"}
onChange={(propVal: string) => this.changeDataType(propVal)}
title={t("Select a type of data scale (default is 'categorical').")}
options={this.getDataFunctionTypes(this.props.fieldSpec!)}
/>
</div>
</Block>
<Block
label={t("Base")}
data-wd-key="function-base"
>
<div className="maputnik-data-spec-property-input">
<InputSpec
fieldName={"base"}
fieldSpec={latest.function.base as typeof latest.function.base & { type: "number" }}
value={this.props.value?.base}
onChange={(_, newValue) => this.changeBase(newValue as number | undefined)}
/>
</div>
</Block>
<div className="maputnik-function-stop">
<table className="maputnik-function-stop-table maputnik-function-stop-table--zoom">
<caption>{t("Stops")}</caption>
<thead>
<tr>
<th>{t("Zoom")}</th>
<th rowSpan={2}>{t("Output value")}</th>
</tr>
</thead>
<tbody>
{zoomFields}
</tbody>
</table>
</div>
<div className="maputnik-toolbox">
<InputButton
className="maputnik-add-stop"
onClick={this.props.onAddStop?.bind(this)}
>
<PiListPlusBold style={{ verticalAlign: "text-bottom" }} />
{t("Add stop")}
</InputButton>
<InputButton
className="maputnik-add-stop"
data-wd-key="convert-to-expression"
onClick={this.props.onExpressionClick?.bind(this)}
>
<TbMathFunction style={{ verticalAlign: "text-bottom" }} />
{t("Convert to expression")}
</InputButton>
</div>
</div>
</fieldset>
</div>;
}
getDataFunctionTypes(fieldSpec: {
"property-type"?: string "property-type"?: string
"function-type"?: string "function-type"?: string
}) { }) {
@@ -143,104 +265,6 @@ const ZoomPropertyInternal: React.FC<ZoomPropertyInternalProps> = ({ errors = {}
return ["interpolate"]; return ["interpolate"];
} }
} }
}
const t = props.t;
const zoomFields = props.value?.stops.map((stop, idx) => {
const zoomLevel = stop[0];
const value = stop[1];
const deleteStopBtn = <DeleteStopButton onClick={props.onDeleteStop?.bind(null, idx)} />;
return <tr
key={`${stop[0]}-${stop[1]}`}
>
<td>
<InputNumber
aria-label={t("Zoom")}
value={zoomLevel}
onChange={changedStop => changeZoomStop(idx, changedStop, value)}
min={0}
max={22}
/>
</td>
<td>
<InputSpec
aria-label={t("Output value")}
fieldName={props.fieldName}
fieldSpec={props.fieldSpec as any}
value={value}
onChange={(_, newValue) => changeZoomStop(idx, zoomLevel, newValue as number)}
/>
</td>
<td>
{deleteStopBtn}
</td>
</tr>;
});
// return <div className="maputnik-zoom-spec-property">
return <div className="maputnik-data-spec-block">
<fieldset className="maputnik-data-spec-property">
<legend>{labelFromFieldName(props.fieldName)}</legend>
<div className="maputnik-data-fieldset-inner">
<Block
label={t("Function")}
data-wd-key="function-type"
>
<div className="maputnik-data-spec-property-input">
<InputSelect
value={"interpolate"}
onChange={(propVal: string) => changeDataType(propVal)}
title={t("Select a type of data scale (default is 'categorical').")}
options={getDataFunctionTypes(props.fieldSpec!)}
/>
</div>
</Block>
<Block
label={t("Base")}
data-wd-key="function-base"
>
<div className="maputnik-data-spec-property-input">
<InputSpec
fieldName={"base"}
fieldSpec={latest.function.base as typeof latest.function.base & { type: "number" }}
value={props.value?.base}
onChange={(_, newValue) => changeBase(newValue as number | undefined)}
/>
</div>
</Block>
<div className="maputnik-function-stop">
<table className="maputnik-function-stop-table maputnik-function-stop-table--zoom">
<caption>{t("Stops")}</caption>
<thead>
<tr>
<th>{t("Zoom")}</th>
<th rowSpan={2}>{t("Output value")}</th>
</tr>
</thead>
<tbody>
{zoomFields}
</tbody>
</table>
</div>
<div className="maputnik-toolbox">
<InputButton
className="maputnik-add-stop"
onClick={props.onAddStop?.bind(null)}
>
<PiListPlusBold style={{ verticalAlign: "text-bottom" }} />
{t("Add stop")}
</InputButton>
<InputButton
className="maputnik-add-stop"
data-wd-key="convert-to-expression"
onClick={props.onExpressionClick?.bind(null)}
>
<TbMathFunction style={{ verticalAlign: "text-bottom" }} />
{t("Convert to expression")}
</InputButton>
</div>
</div>
</fieldset>
</div>;
};
export const ZoomProperty = withTranslation()(ZoomPropertyInternal); export const ZoomProperty = withTranslation()(ZoomPropertyInternal);
+40 -34
View File
@@ -14,51 +14,57 @@ type ModalInternalProps = PropsWithChildren & {
} & WithTranslation; } & WithTranslation;
const ModalInternal: React.FC<ModalInternalProps> = ({underlayClickExits = true, ...props}) => { class ModalInternal extends React.Component<ModalInternalProps> {
static defaultProps = {
underlayClickExits: true
};
// See <https://github.com/maplibre/maputnik/issues/416> // See <https://github.com/maplibre/maputnik/issues/416>
const onClose = () => { onClose = () => {
if (document.activeElement) { if (document.activeElement) {
(document.activeElement as HTMLElement).blur(); (document.activeElement as HTMLElement).blur();
} }
setTimeout(() => { setTimeout(() => {
props.onOpenToggle(); this.props.onOpenToggle();
}, 0); }, 0);
}; };
const t = props.t; render() {
if(props.isOpen) { const t = this.props.t;
return <AriaModal if(this.props.isOpen) {
titleText={props.title} return <AriaModal
underlayClickExits={underlayClickExits} titleText={this.props.title}
data-wd-key={props["data-wd-key"]} underlayClickExits={this.props.underlayClickExits}
verticallyCenter={true} data-wd-key={this.props["data-wd-key"]}
onExit={onClose} verticallyCenter={true}
dialogClass='maputnik-modal-container' onExit={this.onClose}
> dialogClass='maputnik-modal-container'
<div className={classnames("maputnik-modal", props.className)}
data-wd-key={props["data-wd-key"]}
> >
<header className="maputnik-modal-header"> <div className={classnames("maputnik-modal", this.props.className)}
<h1 className="maputnik-modal-header-title">{props.title}</h1> data-wd-key={this.props["data-wd-key"]}
<span className="maputnik-space"></span> >
<button className="maputnik-modal-header-toggle" <header className="maputnik-modal-header">
title={t("Close modal")} <h1 className="maputnik-modal-header-title">{this.props.title}</h1>
onClick={onClose} <span className="maputnik-space"></span>
data-wd-key={props["data-wd-key"]+".close-modal"} <button className="maputnik-modal-header-toggle"
> title={t("Close modal")}
<MdClose /> onClick={this.onClose}
</button> data-wd-key={this.props["data-wd-key"]+".close-modal"}
</header> >
<div className="maputnik-modal-scroller"> <MdClose />
<div className="maputnik-modal-content">{props.children}</div> </button>
</header>
<div className="maputnik-modal-scroller">
<div className="maputnik-modal-content">{this.props.children}</div>
</div>
</div> </div>
</div> </AriaModal>;
</AriaModal>; }
else {
return false;
}
} }
else { }
return false;
}
};
export const Modal = withTranslation()(ModalInternal); export const Modal = withTranslation()(ModalInternal);
+144 -141
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from "react"; import React from "react";
import { type WithTranslation, withTranslation } from "react-i18next"; import { type WithTranslation, withTranslation } from "react-i18next";
import type {LayerSpecification, SourceSpecification} from "maplibre-gl"; import type {LayerSpecification, SourceSpecification} from "maplibre-gl";
@@ -27,166 +27,169 @@ type ModalAddState = {
error?: string | null error?: string | null
}; };
type ModalAddSources = ModalAddInternalProps["sources"]; class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddState> {
addLayer = () => {
function getLayersForSource(allSources: ModalAddSources, source: string) { if (this.props.layers.some(l => l.id === this.state.id)) {
const sourceObj = allSources[source] || {}; this.setState({ error: this.props.t("Layer ID already exists") });
return sourceObj.layers || [];
}
function getSources(allSources: ModalAddSources, type: LayerSpecification["type"]) {
switch(type) {
case "background":
return [];
case "hillshade":
case "color-relief":
return Object.entries(allSources).filter(([_, v]) => v.type === "raster-dem").map(([k, _]) => k);
case "raster":
return Object.entries(allSources).filter(([_, v]) => v.type === "raster").map(([k, _]) => k);
case "heatmap":
case "circle":
case "fill":
case "fill-extrusion":
case "line":
case "symbol":
return Object.entries(allSources).filter(([_, v]) => v.type === "vector" || v.type === "geojson").map(([k, _]) => k);
default:
return [];
}
}
const ModalAddInternal: React.FC<ModalAddInternalProps> = (props) => {
const [type, setType] = useState<LayerSpecification["type"]>("fill");
const [id, setId] = useState<string>("");
const [error, setError] = useState<string | null | undefined>(null);
const [source, setSource] = useState<string | undefined>(() => {
const sourceIds = Object.keys(props.sources);
return sourceIds.length > 0 ? sourceIds[0] : undefined;
});
const [sourceLayer, setSourceLayer] = useState<string | undefined>(() => {
const sourceIds = Object.keys(props.sources);
if (sourceIds.length === 0) return undefined;
const sourceLayers = props.sources[sourceIds[0]].layers || [];
return sourceLayers.length > 0 ? sourceLayers[0] : undefined;
});
// Mirrors `prevState.type` from the previous `componentDidUpdate`.
const prevType = useRef<LayerSpecification["type"]>(type);
useEffect(() => {
// Check if source is valid for new type
const oldType = prevType.current;
prevType.current = type;
// Only when the type has changed (also skips the initial mount)
if (oldType === type) return;
const availableSourcesOld = getSources(props.sources, oldType);
const availableSourcesNew = getSources(props.sources, type);
if(
source !== ""
// Was a valid source previously
&& availableSourcesOld.indexOf(source!) > -1
// And is not a valid source now
&& availableSourcesNew.indexOf(source!) < 0
) {
// Clear the source
setSource("");
}
}, [type, source, props.sources]);
const addLayer = () => {
if (props.layers.some(l => l.id === id)) {
setError(props.t("Layer ID already exists"));
return; return;
} }
const changedLayers = props.layers.slice(0); const changedLayers = this.props.layers.slice(0);
const layer: ModalAddState = { const layer: ModalAddState = {
id: id, id: this.state.id,
type: type, type: this.state.type,
}; };
if(type !== "background") { if(this.state.type !== "background") {
layer.source = source; layer.source = this.state.source;
if(!NON_SOURCE_LAYERS.includes(type) && sourceLayer) { if(!NON_SOURCE_LAYERS.includes(this.state.type) && this.state["source-layer"]) {
layer["source-layer"] = sourceLayer; layer["source-layer"] = this.state["source-layer"];
} }
} }
changedLayers.push(layer as LayerSpecification); changedLayers.push(layer as LayerSpecification);
setError(null); this.setState({ error: null }, () => {
props.onLayersChange(changedLayers); this.props.onLayersChange(changedLayers);
props.onOpenToggle(); this.props.onOpenToggle();
});
}; };
const t = props.t; constructor(props: ModalAddInternalProps) {
const sources = getSources(props.sources, type); super(props);
const layers = getLayersForSource(props.sources, source!); const state: ModalAddState = {
let errorElement; type: "fill",
if (error) { id: "",
errorElement = ( error: null,
<div className="maputnik-modal-error"> };
{error}
<a if(Object.keys(props.sources).length > 0) {
href="#" state.source = Object.keys(this.props.sources)[0];
onClick={() => setError(null)} const sourceLayers = this.props.sources[state.source].layers || [];
className="maputnik-modal-error-close" if (sourceLayers.length > 0) {
> state["source-layer"] = sourceLayers[0];
× }
</a> }
</div> this.state = state;
);
} }
return <Modal componentDidUpdate(_prevProps: ModalAddInternalProps, prevState: ModalAddState) {
isOpen={props.isOpen} // Check if source is valid for new type
onOpenToggle={props.onOpenToggle} const oldType = prevState.type;
title={t("Add Layer")} const newType = this.state.type;
data-wd-key="modal:add-layer"
className="maputnik-add-modal" const availableSourcesOld = this.getSources(oldType);
> const availableSourcesNew = this.getSources(newType);
{errorElement}
<div className="maputnik-add-layer"> if(
<FieldId // Type has changed
value={id} oldType !== newType
wdKey="add-layer.layer-id" && prevState.source !== ""
onChange={(v: string) => { // Was a valid source previously
setId(v); && availableSourcesOld.indexOf(prevState.source!) > -1
setError(null); // And is not a valid source now
}} && availableSourcesNew.indexOf(this.state.source!) < 0
/> ) {
<FieldType // Clear the source
value={type} this.setState({
wdKey="add-layer.layer-type" source: ""
onChange={(v: LayerSpecification["type"]) => setType(v)} });
/> }
{type !== "background" && }
getLayersForSource(source: string) {
const sourceObj = this.props.sources[source] || {};
return sourceObj.layers || [];
}
getSources(type: LayerSpecification["type"]) {
switch(type) {
case "background":
return [];
case "hillshade":
case "color-relief":
return Object.entries(this.props.sources).filter(([_, v]) => v.type === "raster-dem").map(([k, _]) => k);
case "raster":
return Object.entries(this.props.sources).filter(([_, v]) => v.type === "raster").map(([k, _]) => k);
case "heatmap":
case "circle":
case "fill":
case "fill-extrusion":
case "line":
case "symbol":
return Object.entries(this.props.sources).filter(([_, v]) => v.type === "vector" || v.type === "geojson").map(([k, _]) => k);
default:
return [];
}
}
render() {
const t = this.props.t;
const sources = this.getSources(this.state.type);
const layers = this.getLayersForSource(this.state.source!);
let errorElement;
if (this.state.error) {
errorElement = (
<div className="maputnik-modal-error">
{this.state.error}
<a
href="#"
onClick={() => this.setState({ error: null })}
className="maputnik-modal-error-close"
>
×
</a>
</div>
);
}
return <Modal
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={t("Add Layer")}
data-wd-key="modal:add-layer"
className="maputnik-add-modal"
>
{errorElement}
<div className="maputnik-add-layer">
<FieldId
value={this.state.id}
wdKey="add-layer.layer-id"
onChange={(v: string) => {
this.setState({ id: v, error: null });
}}
/>
<FieldType
value={this.state.type}
wdKey="add-layer.layer-type"
onChange={(v: LayerSpecification["type"]) => this.setState({ type: v })}
/>
{this.state.type !== "background" &&
<FieldSource <FieldSource
sourceIds={sources} sourceIds={sources}
wdKey="add-layer.layer-source-block" wdKey="add-layer.layer-source-block"
value={source} value={this.state.source}
onChange={(v: string) => setSource(v)} onChange={(v: string) => this.setState({ source: v })}
/> />
} }
{!NON_SOURCE_LAYERS.includes(type) && {!NON_SOURCE_LAYERS.includes(this.state.type) &&
<FieldSourceLayer <FieldSourceLayer
sourceLayerIds={layers} sourceLayerIds={layers}
value={sourceLayer} value={this.state["source-layer"]}
onChange={(v: string) => setSourceLayer(v)} onChange={(v: string) => this.setState({ "source-layer": v })}
/> />
} }
<InputButton <InputButton
className="maputnik-add-layer-button" className="maputnik-add-layer-button"
onClick={addLayer} onClick={this.addLayer}
data-wd-key="add-layer" data-wd-key="add-layer"
> >
{t("Add Layer")} {t("Add Layer")}
</InputButton> </InputButton>
</div> </div>
</Modal>; </Modal>;
}; }
}
export const ModalAdd = withTranslation()(ModalAddInternal); export const ModalAdd = withTranslation()(ModalAddInternal);
+54 -52
View File
@@ -22,59 +22,61 @@ type ModalDebugInternalProps = {
} & WithTranslation; } & WithTranslation;
const ModalDebugInternal: React.FC<ModalDebugInternalProps> = (props) => { class ModalDebugInternal extends React.Component<ModalDebugInternalProps> {
const {t, mapView} = props; render() {
const {t, mapView} = this.props;
const osmZoom = Math.round(mapView.zoom)+1; const osmZoom = Math.round(mapView.zoom)+1;
const osmLon = +(mapView.center.lng).toFixed(5); const osmLon = +(mapView.center.lng).toFixed(5);
const osmLat = +(mapView.center.lat).toFixed(5); const osmLat = +(mapView.center.lat).toFixed(5);
return <Modal return <Modal
data-wd-key="modal:debug" data-wd-key="modal:debug"
isOpen={props.isOpen} isOpen={this.props.isOpen}
onOpenToggle={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>
{props.renderer === "mlgljs" && {this.props.renderer === "mlgljs" &&
<ul> <ul>
{Object.entries(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) => 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>
} }
{props.renderer === "ol" && {this.props.renderer === "ol" &&
<ul> <ul>
{Object.entries(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) => 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>{t("Links")}</h1> <h1>{t("Links")}</h1>
<p> <p>
<Trans t={t}> <Trans t={t}>
<a <a
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
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>. Opens the current view on openstreetmap.org </a>. Opens the current view on openstreetmap.org
</Trans> </Trans>
</p> </p>
</section> </section>
</Modal>; </Modal>;
}; }
}
export const ModalDebug = withTranslation()(ModalDebugInternal); export const ModalDebug = withTranslation()(ModalDebugInternal);
+117 -114
View File
@@ -28,31 +28,31 @@ type ModalExportInternalProps = {
} & WithTranslation; } & WithTranslation;
const ModalExportInternal: React.FC<ModalExportInternalProps> = (props) => { class ModalExportInternal extends React.Component<ModalExportInternalProps> {
function tokenizedStyle() { tokenizedStyle() {
return format( return format(
stripAccessTokens( stripAccessTokens(
replaceAccessTokens(props.mapStyle) replaceAccessTokens(this.props.mapStyle)
) )
); );
} }
function exportName() { exportName() {
if (props.mapStyle.name) { if (this.props.mapStyle.name) {
return Slugify(props.mapStyle.name, { return Slugify(this.props.mapStyle.name, {
replacement: "_", replacement: "_",
remove: /[*\-+~.()'"!:]/g, remove: /[*\-+~.()'"!:]/g,
lower: true lower: true
}); });
} else { } else {
return props.mapStyle.id; return this.props.mapStyle.id;
} }
} }
function createHtml() { createHtml() {
const tokenStyle = tokenizedStyle(); const tokenStyle = this.tokenizedStyle();
const htmlTitle = props.mapStyle.name || props.t("Map"); const htmlTitle = this.props.mapStyle.name || this.props.t("Map");
const html = `<!DOCTYPE html> const html = `<!DOCTYPE html>
<html> <html>
<head> <head>
@@ -80,11 +80,49 @@ const ModalExportInternal: React.FC<ModalExportInternalProps> = (props) => {
`; `;
const blob = new Blob([html], {type: "text/html;charset=utf-8"}); const blob = new Blob([html], {type: "text/html;charset=utf-8"});
const name = exportName(); const exportName = this.exportName();
saveAs(blob, name + ".html"); saveAs(blob, exportName + ".html");
} }
async function createFileHandle(): Promise<FileSystemFileHandle | null> { async saveStyle() {
const tokenStyle = this.tokenizedStyle();
// 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.
if (!showSaveFilePickerAvailable) {
const blob = new Blob([tokenStyle], {type: "application/json;charset=utf-8"});
const exportName = this.exportName();
saveAs(blob, exportName + ".json");
return;
}
let fileHandle = this.props.fileHandle;
if (fileHandle == null) {
fileHandle = await this.createFileHandle();
this.props.onSetFileHandle(fileHandle);
if (fileHandle == null) return;
}
const writable = await fileHandle.createWritable();
await writable.write(tokenStyle);
await writable.close();
this.props.onOpenToggle();
}
async saveStyleAs() {
const tokenStyle = this.tokenizedStyle();
const fileHandle = await this.createFileHandle();
this.props.onSetFileHandle(fileHandle);
if (fileHandle == null) return;
const writable = await fileHandle.createWritable();
await writable.write(tokenStyle);
await writable.close();
this.props.onOpenToggle();
}
async createFileHandle(): Promise<FileSystemFileHandle | null> {
const pickerOpts: SaveFilePickerOptions = { const pickerOpts: SaveFilePickerOptions = {
types: [ types: [
{ {
@@ -92,126 +130,91 @@ const ModalExportInternal: React.FC<ModalExportInternalProps> = (props) => {
accept: {"application/json": [".json"]}, accept: {"application/json": [".json"]},
}, },
], ],
suggestedName: exportName(), suggestedName: this.exportName(),
}; };
const fileHandle = await window.showSaveFilePicker(pickerOpts) as FileSystemFileHandle; const fileHandle = await window.showSaveFilePicker(pickerOpts) as FileSystemFileHandle;
props.onSetFileHandle(fileHandle); this.props.onSetFileHandle(fileHandle);
return fileHandle; return fileHandle;
} }
async function saveStyle() { changeMetadataProperty(property: string, value: any) {
const tokenStyle = tokenizedStyle();
// 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.
if (!showSaveFilePickerAvailable) {
const blob = new Blob([tokenStyle], {type: "application/json;charset=utf-8"});
const name = exportName();
saveAs(blob, name + ".json");
return;
}
let fileHandle = props.fileHandle;
if (fileHandle == null) {
fileHandle = await createFileHandle();
props.onSetFileHandle(fileHandle);
if (fileHandle == null) return;
}
const writable = await fileHandle.createWritable();
await writable.write(tokenStyle);
await writable.close();
props.onOpenToggle();
}
async function saveStyleAs() {
const tokenStyle = tokenizedStyle();
const fileHandle = await createFileHandle();
props.onSetFileHandle(fileHandle);
if (fileHandle == null) return;
const writable = await fileHandle.createWritable();
await writable.write(tokenStyle);
await writable.close();
props.onOpenToggle();
}
function changeMetadataProperty(property: string, value: any) {
const changedStyle = { const changedStyle = {
...props.mapStyle, ...this.props.mapStyle,
metadata: { metadata: {
...props.mapStyle.metadata as any, ...this.props.mapStyle.metadata as any,
[property]: value [property]: value
} }
}; };
props.onStyleChanged(changedStyle); this.props.onStyleChanged(changedStyle);
} }
const t = props.t;
const fsa = fieldSpecAdditional(t);
return <Modal
data-wd-key="modal:export"
isOpen={props.isOpen}
onOpenToggle={props.onOpenToggle}
title={t("Save Style")}
className="maputnik-export-modal"
>
<section className="maputnik-modal-section"> render() {
<h1>{t("Save Style")}</h1> const t = this.props.t;
<p> const fsa = fieldSpecAdditional(t);
{t("Save the JSON style to your computer.")} return <Modal
</p> data-wd-key="modal:export"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={t("Save Style")}
className="maputnik-export-modal"
>
<div> <section className="maputnik-modal-section">
<FieldString <h1>{t("Save Style")}</h1>
label={fsa.maputnik.maptiler_access_token.label} <p>
fieldSpec={fsa.maputnik.maptiler_access_token} {t("Save the JSON style to your computer.")}
value={(props.mapStyle.metadata || {} as any)["maputnik:openmaptiles_access_token"]} </p>
onChange={changeMetadataProperty.bind(null, "maputnik:openmaptiles_access_token")}
/>
<FieldString
label={fsa.maputnik.thunderforest_access_token.label}
fieldSpec={fsa.maputnik.thunderforest_access_token}
value={(props.mapStyle.metadata || {} as any)["maputnik:thunderforest_access_token"]}
onChange={changeMetadataProperty.bind(null, "maputnik:thunderforest_access_token")}
/>
<FieldString
label={fsa.maputnik.stadia_access_token.label}
fieldSpec={fsa.maputnik.stadia_access_token}
value={(props.mapStyle.metadata || {} as any)["maputnik:stadia_access_token"]}
onChange={changeMetadataProperty.bind(null, "maputnik:stadia_access_token")}
/>
<FieldString
label={fsa.maputnik.locationiq_access_token.label}
fieldSpec={fsa.maputnik.locationiq_access_token}
value={(props.mapStyle.metadata || {} as any)["maputnik:locationiq_access_token"]}
onChange={changeMetadataProperty.bind(null, "maputnik:locationiq_access_token")}
/>
</div>
<div className="maputnik-modal-export-buttons"> <div>
<InputButton onClick={saveStyle}> <FieldString
<MdSave/> label={fsa.maputnik.maptiler_access_token.label}
{t("Save")} fieldSpec={fsa.maputnik.maptiler_access_token}
</InputButton> value={(this.props.mapStyle.metadata || {} as any)["maputnik:openmaptiles_access_token"]}
{showSaveFilePickerAvailable && ( onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
<InputButton onClick={saveStyleAs}> />
<FieldString
label={fsa.maputnik.thunderforest_access_token.label}
fieldSpec={fsa.maputnik.thunderforest_access_token}
value={(this.props.mapStyle.metadata || {} as any)["maputnik:thunderforest_access_token"]}
onChange={this.changeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
/>
<FieldString
label={fsa.maputnik.stadia_access_token.label}
fieldSpec={fsa.maputnik.stadia_access_token}
value={(this.props.mapStyle.metadata || {} as any)["maputnik:stadia_access_token"]}
onChange={this.changeMetadataProperty.bind(this, "maputnik:stadia_access_token")}
/>
<FieldString
label={fsa.maputnik.locationiq_access_token.label}
fieldSpec={fsa.maputnik.locationiq_access_token}
value={(this.props.mapStyle.metadata || {} as any)["maputnik:locationiq_access_token"]}
onChange={this.changeMetadataProperty.bind(this, "maputnik:locationiq_access_token")}
/>
</div>
<div className="maputnik-modal-export-buttons">
<InputButton onClick={this.saveStyle.bind(this)}>
<MdSave/> <MdSave/>
{t("Save as")} {t("Save")}
</InputButton> </InputButton>
)} {showSaveFilePickerAvailable && (
<InputButton onClick={this.saveStyleAs.bind(this)}>
<MdSave/>
{t("Save as")}
</InputButton>
)}
<InputButton onClick={createHtml}> <InputButton onClick={this.createHtml.bind(this)}>
<MdMap/> <MdMap/>
{t("Create HTML")} {t("Create HTML")}
</InputButton> </InputButton>
</div> </div>
</section> </section>
</Modal>; </Modal>;
}; }
}
export const ModalExport = withTranslation()(ModalExportInternal); export const ModalExport = withTranslation()(ModalExportInternal);
+21 -19
View File
@@ -13,24 +13,26 @@ type ModalLoadingInternalProps = {
} & WithTranslation; } & WithTranslation;
const ModalLoadingInternal: React.FC<ModalLoadingInternalProps> = (props) => { class ModalLoadingInternal extends React.Component<ModalLoadingInternalProps> {
const t = props.t; render() {
return <Modal const t = this.props.t;
data-wd-key="modal:loading" return <Modal
isOpen={props.isOpen} data-wd-key="modal:loading"
underlayClickExits={false} isOpen={this.props.isOpen}
title={props.title} underlayClickExits={false}
onOpenToggle={() => props.onCancel()} title={this.props.title}
> onOpenToggle={() => this.props.onCancel()}
<p> >
{props.message} <p>
</p> {this.props.message}
<p className="maputnik-dialog__buttons"> </p>
<InputButton onClick={(e) => props.onCancel(e)}> <p className="maputnik-dialog__buttons">
{t("Cancel")} <InputButton onClick={(e) => this.props.onCancel(e)}>
</InputButton> {t("Cancel")}
</p> </InputButton>
</Modal>; </p>
}; </Modal>;
}
}
export const ModalLoading = withTranslation()(ModalLoadingInternal); export const ModalLoading = withTranslation()(ModalLoadingInternal);
+225 -193
View File
@@ -1,4 +1,4 @@
import React, { type DragEvent, type FormEvent, useRef, useState } from "react"; import React, { type DragEvent, 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 { Trans, type WithTranslation, withTranslation } from "react-i18next"; import { Trans, type WithTranslation, withTranslation } from "react-i18next";
@@ -18,27 +18,29 @@ type PublicStyleProps = {
onSelect(...args: unknown[]): unknown onSelect(...args: unknown[]): unknown
}; };
const PublicStyle: React.FC<PublicStyleProps> = (props) => { class PublicStyle extends React.Component<PublicStyleProps> {
return <div className="maputnik-public-style"> render() {
<InputButton return <div className="maputnik-public-style">
className="maputnik-public-style-button" <InputButton
aria-label={props.title} className="maputnik-public-style-button"
onClick={() => props.onSelect(props.url)} aria-label={this.props.title}
> onClick={() => this.props.onSelect(this.props.url)}
<div className="maputnik-public-style-header"> >
<div>{props.title}</div> <div className="maputnik-public-style-header">
<span className="maputnik-space" /> <div>{this.props.title}</div>
<MdAddCircleOutline /> <span className="maputnik-space" />
</div> <MdAddCircleOutline />
<div </div>
className="maputnik-public-style-thumbnail" <div
style={{ className="maputnik-public-style-thumbnail"
backgroundImage: `url(${props.thumbnailUrl})` style={{
}} backgroundImage: `url(${this.props.thumbnailUrl})`
></div> }}
</InputButton> ></div>
</div>; </InputButton>
}; </div>;
}
}
type ModalOpenInternalProps = { type ModalOpenInternalProps = {
isOpen: boolean isOpen: boolean
@@ -47,32 +49,46 @@ type ModalOpenInternalProps = {
fileHandle: FileSystemFileHandle | null fileHandle: FileSystemFileHandle | null
} & WithTranslation; } & WithTranslation;
const ModalOpenInternal: React.FC<ModalOpenInternalProps> = (props) => { type ModalOpenState = {
const fileInputRef = useRef<HTMLInputElement>(null); styleUrl: string
isDragOver: boolean
error?: string | null
activeRequest?: any
activeRequestUrl?: string | null
};
const [styleUrl, setStyleUrl] = useState<string>(""); class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpenState> {
const [isDragOver, setIsDragOver] = useState<boolean>(false); private fileInputRef = React.createRef<HTMLInputElement>();
const [error, setError] = useState<string | null | undefined>(undefined);
const [activeRequest, setActiveRequest] = useState<any>(undefined);
const [activeRequestUrl, setActiveRequestUrl] = useState<string | null | undefined>(undefined);
const clearError = () => { constructor(props: ModalOpenInternalProps) {
setError(null); super(props);
}; this.state = {
styleUrl: "",
isDragOver: false,
};
}
const onCancelActiveRequest = (e: Event) => { clearError() {
this.setState({
error: null
});
}
onCancelActiveRequest(e: Event) {
// Else the click propagates to the underlying modal // Else the click propagates to the underlying modal
if (e) e.stopPropagation(); if (e) e.stopPropagation();
if (activeRequest) { if (this.state.activeRequest) {
activeRequest.abort(); this.state.activeRequest.abort();
setActiveRequest(null); this.setState({
setActiveRequestUrl(null); activeRequest: null,
activeRequestUrl: null
});
} }
}; }
const onStyleSelect = (styleUrl: string) => { onStyleSelect = (styleUrl: string) => {
clearError(); this.clearError();
let canceled: boolean = false; let canceled: boolean = false;
@@ -88,37 +104,43 @@ const ModalOpenInternal: React.FC<ModalOpenInternalProps> = (props) => {
return; return;
} }
setActiveRequest(null); this.setState({
setActiveRequestUrl(null); activeRequest: null,
activeRequestUrl: null
});
const mapStyle = ensureStyleValidity(body); const mapStyle = ensureStyleValidity(body);
console.log("Loaded style ", mapStyle.id); console.log("Loaded style ", mapStyle.id);
props.onStyleOpen(mapStyle); this.props.onStyleOpen(mapStyle);
onOpenToggle(); this.onOpenToggle();
}) })
.catch((err) => { .catch((err) => {
setError(`Failed to load: '${styleUrl}'`); this.setState({
setActiveRequest(null); error: `Failed to load: '${styleUrl}'`,
setActiveRequestUrl(null); activeRequest: 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);
}); });
setActiveRequest({ this.setState({
abort: function () { activeRequest: {
canceled = true; abort: function () {
} canceled = true;
}
},
activeRequestUrl: styleUrl
}); });
setActiveRequestUrl(styleUrl);
}; };
const onSubmitUrl = (e: FormEvent<HTMLFormElement>) => { onSubmitUrl = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
onStyleSelect(styleUrl); this.onStyleSelect(this.state.styleUrl);
}; };
const onOpenFile = async () => { onOpenFile = async () => {
clearError(); this.clearError();
const pickerOpts: OpenFilePickerOptions = { const pickerOpts: OpenFilePickerOptions = {
types: [ types: [
@@ -138,24 +160,26 @@ const ModalOpenInternal: React.FC<ModalOpenInternalProps> = (props) => {
try { try {
mapStyle = JSON.parse(content); mapStyle = JSON.parse(content);
} catch (err) { } catch (err) {
setError((err as Error).toString()); this.setState({
error: (err as Error).toString()
});
return; return;
} }
mapStyle = ensureStyleValidity(mapStyle); mapStyle = ensureStyleValidity(mapStyle);
props.onStyleOpen(mapStyle, fileHandle); this.props.onStyleOpen(mapStyle, fileHandle);
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.
const onFileChanged = (files: FileList | null) => { onFileChanged = (files: FileList | null) => {
if (!files) return; if (!files) return;
if (files.length === 0) return; if (files.length === 0) return;
const file = files[0]; const file = files[0];
const reader = new FileReader(); const reader = new FileReader();
clearError(); this.clearError();
reader.readAsText(file, "UTF-8"); reader.readAsText(file, "UTF-8");
reader.onload = e => { reader.onload = e => {
@@ -164,172 +188,180 @@ const ModalOpenInternal: React.FC<ModalOpenInternalProps> = (props) => {
mapStyle = JSON.parse(e.target?.result as string); mapStyle = JSON.parse(e.target?.result as string);
} }
catch (err) { catch (err) {
setError((err as Error).toString()); this.setState({
error: (err as Error).toString()
});
return; return;
} }
mapStyle = ensureStyleValidity(mapStyle); mapStyle = ensureStyleValidity(mapStyle);
props.onStyleOpen(mapStyle); this.props.onStyleOpen(mapStyle);
onOpenToggle(); this.onOpenToggle();
}; };
reader.onerror = e => console.log(e.target); reader.onerror = e => console.log(e.target);
}; };
const onOpenToggle = () => { onOpenToggle() {
setStyleUrl(""); this.setState({
setIsDragOver(false); styleUrl: "",
clearError(); isDragOver: false,
props.onOpenToggle(); });
}; this.clearError();
this.props.onOpenToggle();
}
const onBrowseClick = async () => { onBrowseClick = async () => {
if (typeof window.showOpenFilePicker === "function") { if (typeof window.showOpenFilePicker === "function") {
await onOpenFile(); await this.onOpenFile();
return; return;
} }
fileInputRef.current?.click(); this.fileInputRef.current?.click();
}; };
const onFileDragOver = (e: DragEvent<HTMLDivElement>) => { onFileDragOver = (e: DragEvent<HTMLDivElement>) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (!isDragOver) { if (!this.state.isDragOver) {
setIsDragOver(true); this.setState({ isDragOver: true });
} }
}; };
const onFileDragLeave = (e: DragEvent<HTMLDivElement>) => { onFileDragLeave = (e: DragEvent<HTMLDivElement>) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
setIsDragOver(false); this.setState({ isDragOver: false });
}; };
const onFileDrop = (e: DragEvent<HTMLDivElement>) => { onFileDrop = (e: DragEvent<HTMLDivElement>) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
setIsDragOver(false); this.setState({ isDragOver: false });
onFileChanged(e.dataTransfer.files); this.onFileChanged(e.dataTransfer.files);
}; };
const onChangeUrl = (url: string) => { onChangeUrl = (url: string) => {
setStyleUrl(url); this.setState({
styleUrl: url,
});
}; };
const t = props.t; render() {
const styleOptions = publicStyles.map(style => { const t = this.props.t;
return <PublicStyle const styleOptions = publicStyles.map(style => {
key={style.id} return <PublicStyle
url={style.url} key={style.id}
title={style.title} url={style.url}
thumbnailUrl={style.thumbnail} title={style.title}
onSelect={onStyleSelect} thumbnailUrl={style.thumbnail}
/>; onSelect={this.onStyleSelect}
}); />;
});
let errorElement; let errorElement;
if (error) { if (this.state.error) {
errorElement = ( errorElement = (
<div className="maputnik-modal-error"> <div className="maputnik-modal-error">
{error} {this.state.error}
<a href="#" onClick={() => clearError()} className="maputnik-modal-error-close">×</a> <a href="#" onClick={() => this.clearError()} className="maputnik-modal-error-close">×</a>
</div>
);
}
return (
<div>
<Modal
data-wd-key="modal:open"
isOpen={this.props.isOpen}
onOpenToggle={() => this.onOpenToggle()}
title={t("Open Style")}
>
{errorElement}
<section className="maputnik-modal-section">
<h1>{t("Open local Style")}</h1>
<p>{t("Open a local JSON style from your computer.")}</p>
<div
data-wd-key="modal:open.dropzone"
className={`maputnik-upload-dropzone${this.state.isDragOver ? " maputnik-upload-dropzone--active" : ""}`}
role="button"
tabIndex={0}
onDragOver={this.onFileDragOver}
onDragLeave={this.onFileDragLeave}
onDrop={this.onFileDrop}
onClick={() => void this.onBrowseClick()}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
void this.onBrowseClick();
}
}}
>
<div className="maputnik-upload-dropzone-content">
<MdFileUpload className="maputnik-upload-dropzone-icon" aria-hidden="true" />
<p className="maputnik-upload-dropzone-text">
{t("Drag and drop a style JSON file here or click to browse")}
</p>
</div>
<input
ref={this.fileInputRef}
data-wd-key="modal:open.file.input"
type="file"
style={{ display: "none" }}
onChange={(e) => this.onFileChanged(e.target.files)}
/>
</div>
</section>
<section className="maputnik-modal-section">
<form onSubmit={this.onSubmitUrl}>
<h1>{t("Load from URL")}</h1>
<p>
<Trans t={t}>
Load from a URL. Note that the URL must have <a href="https://enable-cors.org" target="_blank" rel="noopener noreferrer">CORS enabled</a>.
</Trans>
</p>
<InputUrl
aria-label={t("Style URL")}
data-wd-key="modal:open.url.input"
type="text"
className="maputnik-input"
default={t("Enter URL...")}
value={this.state.styleUrl}
onInput={this.onChangeUrl}
onChange={this.onChangeUrl}
/>
<div>
<InputButton
data-wd-key="modal:open.url.button"
type="submit"
className="maputnik-big-button"
disabled={this.state.styleUrl.length < 1}
>Load from URL</InputButton>
</div>
</form>
</section>
<section className="maputnik-modal-section maputnik-modal-section--shrink">
<h1>{t("Gallery Styles")}</h1>
<p>
{t("Open one of the publicly available styles to start from.")}
</p>
<div className="maputnik-style-gallery-container">
{styleOptions}
</div>
</section>
</Modal>
<ModalLoading
isOpen={!!this.state.activeRequest}
title={t("Loading style")}
onCancel={(e: Event) => this.onCancelActiveRequest(e)}
message={t("Loading") + ": " + this.state.activeRequestUrl}
/>
</div> </div>
); );
} }
}
return (
<div>
<Modal
data-wd-key="modal:open"
isOpen={props.isOpen}
onOpenToggle={() => onOpenToggle()}
title={t("Open Style")}
>
{errorElement}
<section className="maputnik-modal-section">
<h1>{t("Open local Style")}</h1>
<p>{t("Open a local JSON style from your computer.")}</p>
<div
data-wd-key="modal:open.dropzone"
className={`maputnik-upload-dropzone${isDragOver ? " maputnik-upload-dropzone--active" : ""}`}
role="button"
tabIndex={0}
onDragOver={onFileDragOver}
onDragLeave={onFileDragLeave}
onDrop={onFileDrop}
onClick={() => void onBrowseClick()}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
void onBrowseClick();
}
}}
>
<div className="maputnik-upload-dropzone-content">
<MdFileUpload className="maputnik-upload-dropzone-icon" aria-hidden="true" />
<p className="maputnik-upload-dropzone-text">
{t("Drag and drop a style JSON file here or click to browse")}
</p>
</div>
<input
ref={fileInputRef}
data-wd-key="modal:open.file.input"
type="file"
style={{ display: "none" }}
onChange={(e) => onFileChanged(e.target.files)}
/>
</div>
</section>
<section className="maputnik-modal-section">
<form onSubmit={onSubmitUrl}>
<h1>{t("Load from URL")}</h1>
<p>
<Trans t={t}>
Load from a URL. Note that the URL must have <a href="https://enable-cors.org" target="_blank" rel="noopener noreferrer">CORS enabled</a>.
</Trans>
</p>
<InputUrl
aria-label={t("Style URL")}
data-wd-key="modal:open.url.input"
type="text"
className="maputnik-input"
default={t("Enter URL...")}
value={styleUrl}
onInput={onChangeUrl}
onChange={onChangeUrl}
/>
<div>
<InputButton
data-wd-key="modal:open.url.button"
type="submit"
className="maputnik-big-button"
disabled={styleUrl.length < 1}
>Load from URL</InputButton>
</div>
</form>
</section>
<section className="maputnik-modal-section maputnik-modal-section--shrink">
<h1>{t("Gallery Styles")}</h1>
<p>
{t("Open one of the publicly available styles to start from.")}
</p>
<div className="maputnik-style-gallery-container">
{styleOptions}
</div>
</section>
</Modal>
<ModalLoading
isOpen={!!activeRequest}
title={t("Loading style")}
onCancel={(e: Event) => onCancelActiveRequest(e)}
message={t("Loading") + ": " + activeRequestUrl}
/>
</div>
);
};
export const ModalOpen = withTranslation()(ModalOpenInternal); export const ModalOpen = withTranslation()(ModalOpenInternal);
+214 -212
View File
@@ -24,10 +24,10 @@ type ModalSettingsInternalProps = {
onOpenToggle(): void onOpenToggle(): void
} & WithTranslation; } & WithTranslation;
const ModalSettingsInternal: React.FC<ModalSettingsInternalProps> = (props) => { class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps> {
function changeTransitionProperty(property: keyof TransitionSpecification, value: number | undefined) { changeTransitionProperty(property: keyof TransitionSpecification, value: number | undefined) {
const transition = { const transition = {
...props.mapStyle.transition, ...this.props.mapStyle.transition,
}; };
if (value === undefined) { if (value === undefined) {
@@ -37,15 +37,15 @@ const ModalSettingsInternal: React.FC<ModalSettingsInternalProps> = (props) => {
transition[property] = value; transition[property] = value;
} }
props.onStyleChanged({ this.props.onStyleChanged({
...props.mapStyle, ...this.props.mapStyle,
transition, transition,
}); });
} }
function changeLightProperty(property: keyof LightSpecification, value: any) { changeLightProperty(property: keyof LightSpecification, value: any) {
const light = { const light = {
...props.mapStyle.light, ...this.props.mapStyle.light,
}; };
if (value === undefined) { if (value === undefined) {
@@ -56,15 +56,15 @@ const ModalSettingsInternal: React.FC<ModalSettingsInternalProps> = (props) => {
light[property] = value; light[property] = value;
} }
props.onStyleChanged({ this.props.onStyleChanged({
...props.mapStyle, ...this.props.mapStyle,
light, light,
}); });
} }
function changeTerrainProperty(property: keyof TerrainSpecification, value: any) { changeTerrainProperty(property: keyof TerrainSpecification, value: any) {
const terrain = { const terrain = {
...props.mapStyle.terrain, ...this.props.mapStyle.terrain,
} as TerrainSpecification; } as TerrainSpecification;
if (value === undefined) { if (value === undefined) {
@@ -75,15 +75,15 @@ const ModalSettingsInternal: React.FC<ModalSettingsInternalProps> = (props) => {
terrain[property] = value; terrain[property] = value;
} }
props.onStyleChanged({ this.props.onStyleChanged({
...props.mapStyle, ...this.props.mapStyle,
terrain, terrain,
}); });
} }
function changeProjectionType(value: any) { changeProjectionType(value: any) {
const projection = { const projection = {
...props.mapStyle.projection, ...this.props.mapStyle.projection,
} as ProjectionSpecification; } as ProjectionSpecification;
if (value === undefined) { if (value === undefined) {
@@ -93,15 +93,15 @@ const ModalSettingsInternal: React.FC<ModalSettingsInternalProps> = (props) => {
projection.type = value; projection.type = value;
} }
props.onStyleChanged({ this.props.onStyleChanged({
...props.mapStyle, ...this.props.mapStyle,
projection, projection,
}); });
} }
function changeStyleProperty(property: keyof StyleSpecification | "owner", value: any) { changeStyleProperty(property: keyof StyleSpecification | "owner", value: any) {
const changedStyle = { const changedStyle = {
...props.mapStyle, ...this.props.mapStyle,
}; };
if (value === undefined) { if (value === undefined) {
@@ -112,222 +112,224 @@ const ModalSettingsInternal: React.FC<ModalSettingsInternalProps> = (props) => {
// @ts-ignore // @ts-ignore
changedStyle[property] = value; changedStyle[property] = value;
} }
props.onStyleChanged(changedStyle); this.props.onStyleChanged(changedStyle);
} }
const metadata = props.mapStyle.metadata || {} as any; render() {
const {t, onChangeMetadataProperty, mapStyle} = props; const metadata = this.props.mapStyle.metadata || {} as any;
const fsa = fieldSpecAdditional(t); const {t, onChangeMetadataProperty, mapStyle} = this.props;
const fsa = fieldSpecAdditional(t);
const light = props.mapStyle.light || {}; const light = this.props.mapStyle.light || {};
const transition = props.mapStyle.transition || {}; const transition = this.props.mapStyle.transition || {};
const terrain = props.mapStyle.terrain || {} as TerrainSpecification; const terrain = this.props.mapStyle.terrain || {} as TerrainSpecification;
const projection = props.mapStyle.projection || {} as ProjectionSpecification; const projection = this.props.mapStyle.projection || {} as ProjectionSpecification;
return <Modal return <Modal
data-wd-key="modal:settings" data-wd-key="modal:settings"
isOpen={props.isOpen} isOpen={this.props.isOpen}
onOpenToggle={props.onOpenToggle} onOpenToggle={this.props.onOpenToggle}
title={t("Style Settings")} title={t("Style Settings")}
> >
<div className="modal:settings"> <div className="modal:settings">
<FieldString <FieldString
label={t("Name")} label={t("Name")}
fieldSpec={latest.$root.name} fieldSpec={latest.$root.name}
data-wd-key="modal:settings.name" data-wd-key="modal:settings.name"
value={props.mapStyle.name} value={this.props.mapStyle.name}
onChange={(value) => changeStyleProperty("name", value)} onChange={(value) => this.changeStyleProperty("name", value)}
/>
<FieldString
label={t("Owner")}
fieldSpec={{doc: t("Owner ID of the style. Used by Mapbox or future style APIs.")}}
data-wd-key="modal:settings.owner"
value={(props.mapStyle as any).owner}
onChange={(value) => changeStyleProperty("owner", value)}
/>
<Block label={t("Sprite URL")} fieldSpec={latest.$root.sprite} data-wd-key="modal:settings.sprite">
<FieldJson
lintType="json"
value={props.mapStyle.sprite as any}
onChange={(value) => changeStyleProperty("sprite", value)}
/> />
</Block> <FieldString
label={t("Owner")}
fieldSpec={{doc: t("Owner ID of the style. Used by Mapbox or future style APIs.")}}
data-wd-key="modal:settings.owner"
value={(this.props.mapStyle as any).owner}
onChange={(value) => this.changeStyleProperty("owner", value)}
/>
<Block label={t("Sprite URL")} fieldSpec={latest.$root.sprite} data-wd-key="modal:settings.sprite">
<FieldJson
lintType="json"
value={this.props.mapStyle.sprite as any}
onChange={(value) => this.changeStyleProperty("sprite", value)}
/>
</Block>
<FieldUrl <FieldUrl
label={t("Glyphs URL")} label={t("Glyphs URL")}
fieldSpec={latest.$root.glyphs} fieldSpec={latest.$root.glyphs}
data-wd-key="modal:settings.glyphs" data-wd-key="modal:settings.glyphs"
value={props.mapStyle.glyphs as string} value={this.props.mapStyle.glyphs as string}
onChange={(value) => changeStyleProperty("glyphs", value)} 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={(value) => onChangeMetadataProperty("maputnik:openmaptiles_access_token", value)} 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={(value) => onChangeMetadataProperty("maputnik:thunderforest_access_token", value)} 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={(value) => onChangeMetadataProperty("maputnik:stadia_access_token", value)} 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={(value) => onChangeMetadataProperty("maputnik:locationiq_access_token", value)} onChange={(value) => onChangeMetadataProperty("maputnik:locationiq_access_token", value)}
/> />
<FieldArray <FieldArray
label={t("Center")} label={t("Center")}
fieldSpec={latest.$root.center} fieldSpec={latest.$root.center}
length={2} length={2}
type="number" type="number"
value={mapStyle.center || []} value={mapStyle.center || []}
default={[0, 0]} default={[0, 0]}
onChange={(value) => changeStyleProperty("center", value)} onChange={(value) => this.changeStyleProperty("center", value)}
/> />
<FieldNumber <FieldNumber
label={t("Zoom")} label={t("Zoom")}
data-wd-key="modal:settings.zoom" data-wd-key="modal:settings.zoom"
fieldSpec={latest.$root.zoom} fieldSpec={latest.$root.zoom}
value={mapStyle.zoom} value={mapStyle.zoom}
default={0} default={0}
onChange={(value) => changeStyleProperty("zoom", value)} onChange={(value) => this.changeStyleProperty("zoom", value)}
/> />
<FieldNumber <FieldNumber
label={t("Bearing")} label={t("Bearing")}
data-wd-key="modal:settings.bearing" data-wd-key="modal:settings.bearing"
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={(value) => changeStyleProperty("bearing", value)} onChange={(value) => this.changeStyleProperty("bearing", value)}
/> />
<FieldNumber <FieldNumber
label={t("Pitch")} label={t("Pitch")}
data-wd-key="modal:settings.pitch" data-wd-key="modal:settings.pitch"
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={(value) => changeStyleProperty("pitch", value)} onChange={(value) => this.changeStyleProperty("pitch", value)}
/> />
<FieldEnum <FieldEnum
label={t("Light anchor")} label={t("Light anchor")}
fieldSpec={latest.light.anchor} fieldSpec={latest.light.anchor}
name="light-anchor" name="light-anchor"
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={(value) => changeLightProperty("anchor", value)} onChange={(value) => this.changeLightProperty("anchor", value)}
/> />
<FieldColor <FieldColor
label={t("Light color")} label={t("Light color")}
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={(value) => changeLightProperty("color", value)} onChange={(value) => this.changeLightProperty("color", value)}
/> />
<FieldNumber <FieldNumber
label={t("Light intensity")} label={t("Light intensity")}
data-wd-key="modal:settings.light-intensity" data-wd-key="modal:settings.light-intensity"
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={(value) => changeLightProperty("intensity", value)} onChange={(value) => this.changeLightProperty("intensity", value)}
/> />
<FieldArray <FieldArray
label={t("Light position")} label={t("Light position")}
fieldSpec={latest.light.position} fieldSpec={latest.light.position}
type="number" type="number"
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={(value) => changeLightProperty("position", value)} onChange={(value) => this.changeLightProperty("position", value)}
/> />
<FieldString <FieldString
label={t("Terrain source")} label={t("Terrain source")}
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={(value) => changeTerrainProperty("source", value)} onChange={(value) => this.changeTerrainProperty("source", value)}
/> />
<FieldNumber <FieldNumber
label={t("Terrain exaggeration")} label={t("Terrain exaggeration")}
data-wd-key="modal:settings.terrain-exaggeration" data-wd-key="modal:settings.terrain-exaggeration"
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={(value) => changeTerrainProperty("exaggeration", value)} onChange={(value) => this.changeTerrainProperty("exaggeration", value)}
/> />
<FieldNumber <FieldNumber
label={t("Transition delay")} label={t("Transition delay")}
data-wd-key="modal:settings.transition-delay" data-wd-key="modal:settings.transition-delay"
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={(value) => changeTransitionProperty("delay", value)} onChange={(value) => this.changeTransitionProperty("delay", value)}
/> />
<FieldNumber <FieldNumber
label={t("Transition duration")} label={t("Transition duration")}
data-wd-key="modal:settings.transition-duration" data-wd-key="modal:settings.transition-duration"
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={(value) => changeTransitionProperty("duration", value)} onChange={(value) => this.changeTransitionProperty("duration", value)}
/> />
<FieldSelect <FieldSelect
label={t("Projection")} label={t("Projection")}
data-wd-key="modal:settings.projection" data-wd-key="modal:settings.projection"
options={[ options={[
["", "Undefined"], ["", "Undefined"],
["mercator", "Mercator"], ["mercator", "Mercator"],
["globe", "Globe"], ["globe", "Globe"],
["vertical-perspective", "Vertical Perspective"] ["vertical-perspective", "Vertical Perspective"]
]} ]}
value={projection?.type?.toString() || ""} value={projection?.type?.toString() || ""}
onChange={(value) => changeProjectionType(value)} onChange={(value) => this.changeProjectionType(value)}
/> />
<FieldSelect <FieldSelect
label={fsa.maputnik.style_renderer.label} label={fsa.maputnik.style_renderer.label}
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={(value) => onChangeMetadataProperty("maputnik:renderer", value)} onChange={(value) => onChangeMetadataProperty("maputnik:renderer", value)}
/> />
</div> </div>
</Modal>; </Modal>;
}; }
}
export const ModalSettings = withTranslation()(ModalSettingsInternal); export const ModalSettings = withTranslation()(ModalSettingsInternal);
+119 -117
View File
@@ -10,126 +10,128 @@ type ModalShortcutsInternalProps = {
} & WithTranslation; } & WithTranslation;
const ModalShortcutsInternal: React.FC<ModalShortcutsInternalProps> = (props) => { class ModalShortcutsInternal extends React.Component<ModalShortcutsInternalProps> {
const t = props.t; render() {
const help = [ const t = this.props.t;
{ const help = [
key: <kbd>?</kbd>, {
text: t("Shortcuts menu") key: <kbd>?</kbd>,
}, text: t("Shortcuts menu")
{ },
key: <kbd>o</kbd>, {
text: t("Open modal") key: <kbd>o</kbd>,
}, text: t("Open modal")
{ },
key: <kbd>e</kbd>, {
text: t("Export modal") key: <kbd>e</kbd>,
}, text: t("Export modal")
{ },
key: <kbd>d</kbd>, {
text: t("Data Sources modal") key: <kbd>d</kbd>,
}, text: t("Data Sources modal")
{ },
key: <kbd>s</kbd>, {
text: t("Style Settings modal") key: <kbd>s</kbd>,
}, text: t("Style Settings modal")
{ },
key: <kbd>i</kbd>, {
text: t("Toggle inspect") key: <kbd>i</kbd>,
}, text: t("Toggle inspect")
{ },
key: <kbd>m</kbd>, {
text: t("Focus map") key: <kbd>m</kbd>,
}, text: t("Focus map")
{ },
key: <kbd>!</kbd>, {
text: t("Debug modal") key: <kbd>!</kbd>,
}, text: t("Debug modal")
]; },
];
const mapShortcuts = [ const mapShortcuts = [
{ {
key: <kbd>+</kbd>, key: <kbd>+</kbd>,
text: t("Increase the zoom level by 1.",) text: t("Increase the zoom level by 1.",)
}, },
{ {
key: <><kbd>Shift</kbd> + <kbd>+</kbd></>, key: <><kbd>Shift</kbd> + <kbd>+</kbd></>,
text: t("Increase the zoom level by 2.",) text: t("Increase the zoom level by 2.",)
}, },
{ {
key: <kbd>-</kbd>, key: <kbd>-</kbd>,
text: t("Decrease the zoom level by 1.",) text: t("Decrease the zoom level by 1.",)
}, },
{ {
key: <><kbd>Shift</kbd> + <kbd>-</kbd></>, key: <><kbd>Shift</kbd> + <kbd>-</kbd></>,
text: t("Decrease the zoom level by 2.",) text: t("Decrease the zoom level by 2.",)
}, },
{ {
key: <kbd>Up</kbd>, key: <kbd>Up</kbd>,
text: t("Pan up by 100 pixels.",) text: t("Pan up by 100 pixels.",)
}, },
{ {
key: <kbd>Down</kbd>, key: <kbd>Down</kbd>,
text: t("Pan down by 100 pixels.",) text: t("Pan down by 100 pixels.",)
}, },
{ {
key: <kbd>Left</kbd>, key: <kbd>Left</kbd>,
text: t("Pan left by 100 pixels.",) text: t("Pan left by 100 pixels.",)
}, },
{ {
key: <kbd>Right</kbd>, key: <kbd>Right</kbd>,
text: t("Pan right by 100 pixels.",) text: t("Pan right by 100 pixels.",)
}, },
{ {
key: <><kbd>Shift</kbd> + <kbd>Right</kbd></>, key: <><kbd>Shift</kbd> + <kbd>Right</kbd></>,
text: t("Increase the rotation by 15 degrees.",) text: t("Increase the rotation by 15 degrees.",)
}, },
{ {
key: <><kbd>Shift</kbd> + <kbd>Left</kbd></>, key: <><kbd>Shift</kbd> + <kbd>Left</kbd></>,
text: t("Decrease the rotation by 15 degrees.") text: t("Decrease the rotation by 15 degrees.")
}, },
{ {
key: <><kbd>Shift</kbd> + <kbd>Up</kbd></>, key: <><kbd>Shift</kbd> + <kbd>Up</kbd></>,
text: t("Increase the pitch by 10 degrees.") text: t("Increase the pitch by 10 degrees.")
}, },
{ {
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={props.isOpen} isOpen={this.props.isOpen}
onOpenToggle={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>
<Trans t={t}> <Trans t={t}>
Press <code>ESC</code> to lose focus of any active elements, then press one of: Press <code>ESC</code> to lose focus of any active elements, then press one of:
</Trans> </Trans>
</p> </p>
<dl> <dl>
{help.map((item, idx) => { {help.map((item, idx) => {
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>
<ul> <ul>
{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>;
}; }
}
export const ModalShortcuts = withTranslation()(ModalShortcutsInternal); export const ModalShortcuts = withTranslation()(ModalShortcutsInternal);
+262 -245
View File
@@ -1,4 +1,4 @@
import React, { useState } 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";
@@ -23,21 +23,23 @@ type PublicSourceProps = {
onSelect(...args: unknown[]): unknown onSelect(...args: unknown[]): unknown
}; };
const PublicSource: React.FC<PublicSourceProps> = (props) => { class PublicSource extends React.Component<PublicSourceProps> {
return <div className="maputnik-public-source"> render() {
<InputButton return <div className="maputnik-public-source">
className="maputnik-public-source-select" <InputButton
onClick={() => props.onSelect(props.id)} className="maputnik-public-source-select"
> onClick={() => this.props.onSelect(this.props.id)}
<div className="maputnik-public-source-info"> >
<p className="maputnik-public-source-name">{props.title}</p> <div className="maputnik-public-source-info">
<p className="maputnik-public-source-id">#{props.id}</p> <p className="maputnik-public-source-name">{this.props.title}</p>
</div> <p className="maputnik-public-source-id">#{this.props.id}</p>
<span className="maputnik-space" /> </div>
<MdAddCircleOutline /> <span className="maputnik-space" />
</InputButton> <MdAddCircleOutline />
</div>; </InputButton>
}; </div>;
}
}
function editorMode(source: SourceSpecification) { function editorMode(source: SourceSpecification) {
if(source.type === "raster") { if(source.type === "raster") {
@@ -77,184 +79,197 @@ type ActiveModalSourcesTypeEditorProps = {
onChange(...args: unknown[]): unknown onChange(...args: unknown[]): unknown
} & WithTranslation; } & WithTranslation;
const ActiveModalSourcesTypeEditor: React.FC<ActiveModalSourcesTypeEditorProps> = (props) => { class ActiveModalSourcesTypeEditor extends React.Component<ActiveModalSourcesTypeEditorProps> {
const t = props.t; render() {
return <div className="maputnik-active-source-type-editor"> const t = this.props.t;
<div className="maputnik-active-source-type-editor-header"> return <div className="maputnik-active-source-type-editor">
<span className="maputnik-active-source-type-editor-header-id">#{props.sourceId}</span> <div className="maputnik-active-source-type-editor-header">
<span className="maputnik-space" /> <span className="maputnik-active-source-type-editor-header-id">#{this.props.sourceId}</span>
<InputButton <span className="maputnik-space" />
aria-label={t("Remove '{{sourceId}}' source", {sourceId: props.sourceId})} <InputButton
className="maputnik-active-source-type-editor-header-delete" aria-label={t("Remove '{{sourceId}}' source", {sourceId: this.props.sourceId})}
onClick={()=> props.onDelete(props.sourceId)} className="maputnik-active-source-type-editor-header-delete"
style={{backgroundColor: "transparent"}} onClick={()=> this.props.onDelete(this.props.sourceId)}
> style={{backgroundColor: "transparent"}}
<MdDelete /> >
</InputButton> <MdDelete />
</div> </InputButton>
<div className="maputnik-active-source-type-editor-content"> </div>
<ModalSourcesTypeEditor <div className="maputnik-active-source-type-editor-content">
onChange={props.onChange} <ModalSourcesTypeEditor
mode={editorMode(props.source)} onChange={this.props.onChange}
source={props.source} mode={editorMode(this.props.source)}
/> source={this.props.source}
</div> />
</div>; </div>
}; </div>;
}
}
type AddSourceProps = { type AddSourceProps = {
onAdd(...args: unknown[]): unknown onAdd(...args: unknown[]): unknown
} & WithTranslation; } & WithTranslation;
function defaultSource(mode: EditorMode, currentSource: SourceSpecification | undefined): SourceSpecification { type AddSourceState = {
const source = currentSource || {}; mode: EditorMode
const {protocol} = window.location; sourceId: string
source: SourceSpecification
};
switch(mode) { class AddSource extends React.Component<AddSourceProps, AddSourceState> {
case "pmtiles_vector": return { constructor(props: AddSourceProps) {
type: "vector", super(props);
url: `${protocol}//localhost:3000/file.pmtiles` this.state = {
mode: "tilejson_vector",
sourceId: generateId(),
source: this.defaultSource("tilejson_vector"),
}; };
case "geojson_url": return { }
type: "geojson",
data: `${protocol}//localhost:3000/geojson.json` defaultSource(mode: EditorMode): SourceSpecification {
const source = (this.state || {}).source || {};
const {protocol} = window.location;
switch(mode) {
case "pmtiles_vector": return {
type: "vector",
url: `${protocol}//localhost:3000/file.pmtiles`
};
case "geojson_url": return {
type: "geojson",
data: `${protocol}//localhost:3000/geojson.json`
};
case "geojson_json": return {
type: "geojson",
cluster: (source as GeoJSONSourceSpecification).cluster || false,
data: ""
};
case "tilejson_vector": return {
type: "vector",
url: (source as VectorSourceSpecification).url || `${protocol}//localhost:3000/tilejson.json`
};
case "tile_vector": return {
type: "vector",
tiles: (source as VectorSourceSpecification).tiles || [`${protocol}//localhost:3000/{x}/{y}/{z}.pbf`],
minzoom: (source as VectorSourceSpecification).minzoom || 0,
maxzoom: (source as VectorSourceSpecification).maxzoom || 14,
scheme: (source as VectorSourceSpecification).scheme || "xyz"
};
case "tilejson_raster": return {
type: "raster",
url: (source as RasterSourceSpecification).url || `${protocol}//localhost:3000/tilejson.json`
};
case "tile_raster": return {
type: "raster",
tiles: (source as RasterSourceSpecification).tiles || [`${protocol}//localhost:3000/{x}/{y}/{z}.png`],
minzoom: (source as RasterSourceSpecification).minzoom || 0,
maxzoom: (source as RasterSourceSpecification).maxzoom || 14,
scheme: (source as RasterSourceSpecification).scheme || "xyz",
tileSize: (source as RasterSourceSpecification).tileSize || 512,
};
case "tilejson_raster-dem": return {
type: "raster-dem",
url: (source as RasterDEMSourceSpecification).url || `${protocol}//localhost:3000/tilejson.json`
};
case "tilexyz_raster-dem": return {
type: "raster-dem",
tiles: (source as RasterDEMSourceSpecification).tiles || [`${protocol}//localhost:3000/{x}/{y}/{z}.png`],
minzoom: (source as RasterDEMSourceSpecification).minzoom || 0,
maxzoom: (source as RasterDEMSourceSpecification).maxzoom || 14,
tileSize: (source as RasterDEMSourceSpecification).tileSize || 512
};
case "image": return {
type: "image",
url: `${protocol}//localhost:3000/image.png`,
coordinates: [
[0,0],
[0,0],
[0,0],
[0,0],
],
};
case "video": return {
type: "video",
urls: [
`${protocol}//localhost:3000/movie.mp4`
],
coordinates: [
[0,0],
[0,0],
[0,0],
[0,0],
],
};
default: return {} as any;
}
}
onAdd = () => {
const {source, sourceId} = this.state;
this.props.onAdd(sourceId, source);
};
onChangeSource = (source: SourceSpecification) => {
this.setState({source});
};
render() {
const t = this.props.t;
// Kind of a hack because the type changes, however maputnik has 1..n
// options per type, for example
//
// - 'geojson' - 'GeoJSON (URL)' and 'GeoJSON (JSON)'
// - 'raster' - 'Raster (TileJSON URL)' and 'Raster (XYZ URL)'
//
// So we just ignore the values entirely as they are self explanatory
const sourceTypeFieldSpec = {
doc: latest.source_vector.type.doc
}; };
case "geojson_json": return {
type: "geojson", return <div className="maputnik-add-source">
cluster: (source as GeoJSONSourceSpecification).cluster || false, <FieldString
data: "" label={t("Source ID")}
}; fieldSpec={{doc: t("Unique ID that identifies the source and is used in the layer to reference the source.")}}
case "tilejson_vector": return { value={this.state.sourceId}
type: "vector", onChange={(v: string) => this.setState({ sourceId: v})}
url: (source as VectorSourceSpecification).url || `${protocol}//localhost:3000/tilejson.json` data-wd-key="modal:sources.add.source_id"
}; />
case "tile_vector": return { <FieldSelect
type: "vector", label={t("Source Type")}
tiles: (source as VectorSourceSpecification).tiles || [`${protocol}//localhost:3000/{x}/{y}/{z}.pbf`], fieldSpec={sourceTypeFieldSpec}
minzoom: (source as VectorSourceSpecification).minzoom || 0, options={[
maxzoom: (source as VectorSourceSpecification).maxzoom || 14, ["geojson_json", t("GeoJSON (JSON)")],
scheme: (source as VectorSourceSpecification).scheme || "xyz" ["geojson_url", t("GeoJSON (URL)")],
}; ["tilejson_vector", t("Vector (TileJSON URL)")],
case "tilejson_raster": return { ["tile_vector", t("Vector (Tile URLs)")],
type: "raster", ["tilejson_raster", t("Raster (TileJSON URL)")],
url: (source as RasterSourceSpecification).url || `${protocol}//localhost:3000/tilejson.json` ["tile_raster", t("Raster (Tile URLs)")],
}; ["tilejson_raster-dem", t("Raster DEM (TileJSON URL)")],
case "tile_raster": return { ["tilexyz_raster-dem", t("Raster DEM (XYZ URLs)")],
type: "raster", ["pmtiles_vector", t("Vector (PMTiles)")],
tiles: (source as RasterSourceSpecification).tiles || [`${protocol}//localhost:3000/{x}/{y}/{z}.png`], ["image", t("Image")],
minzoom: (source as RasterSourceSpecification).minzoom || 0, ["video", t("Video")],
maxzoom: (source as RasterSourceSpecification).maxzoom || 14, ]}
scheme: (source as RasterSourceSpecification).scheme || "xyz", onChange={mode => this.setState({mode: mode as EditorMode, source: this.defaultSource(mode as EditorMode)})}
tileSize: (source as RasterSourceSpecification).tileSize || 512, value={this.state.mode as string}
}; data-wd-key="modal:sources.add.source_type"
case "tilejson_raster-dem": return { />
type: "raster-dem", <ModalSourcesTypeEditor
url: (source as RasterDEMSourceSpecification).url || `${protocol}//localhost:3000/tilejson.json` onChange={this.onChangeSource}
}; mode={this.state.mode}
case "tilexyz_raster-dem": return { source={this.state.source}
type: "raster-dem", />
tiles: (source as RasterDEMSourceSpecification).tiles || [`${protocol}//localhost:3000/{x}/{y}/{z}.png`], <InputButton
minzoom: (source as RasterDEMSourceSpecification).minzoom || 0, className="maputnik-add-source-button"
maxzoom: (source as RasterDEMSourceSpecification).maxzoom || 14, onClick={this.onAdd}
tileSize: (source as RasterDEMSourceSpecification).tileSize || 512 data-wd-key="modal:sources.add.add_source"
}; >
case "image": return { {t("Add Source")}
type: "image", </InputButton>
url: `${protocol}//localhost:3000/image.png`, </div>;
coordinates: [
[0,0],
[0,0],
[0,0],
[0,0],
],
};
case "video": return {
type: "video",
urls: [
`${protocol}//localhost:3000/movie.mp4`
],
coordinates: [
[0,0],
[0,0],
[0,0],
[0,0],
],
};
default: return {} as any;
} }
} }
const AddSource: React.FC<AddSourceProps> = (props) => {
const [mode, setMode] = useState<EditorMode>("tilejson_vector");
const [sourceId, setSourceId] = useState<string>(() => generateId());
const [source, setSource] = useState<SourceSpecification>(() => defaultSource("tilejson_vector", undefined));
const onAdd = () => {
props.onAdd(sourceId, source);
};
const onChangeSource = (newSource: SourceSpecification) => {
setSource(newSource);
};
const t = props.t;
// Kind of a hack because the type changes, however maputnik has 1..n
// options per type, for example
//
// - 'geojson' - 'GeoJSON (URL)' and 'GeoJSON (JSON)'
// - 'raster' - 'Raster (TileJSON URL)' and 'Raster (XYZ URL)'
//
// So we just ignore the values entirely as they are self explanatory
const sourceTypeFieldSpec = {
doc: latest.source_vector.type.doc
};
return <div className="maputnik-add-source">
<FieldString
label={t("Source ID")}
fieldSpec={{doc: t("Unique ID that identifies the source and is used in the layer to reference the source.")}}
value={sourceId}
onChange={(v: string) => setSourceId(v)}
data-wd-key="modal:sources.add.source_id"
/>
<FieldSelect
label={t("Source Type")}
fieldSpec={sourceTypeFieldSpec}
options={[
["geojson_json", t("GeoJSON (JSON)")],
["geojson_url", t("GeoJSON (URL)")],
["tilejson_vector", t("Vector (TileJSON URL)")],
["tile_vector", t("Vector (Tile URLs)")],
["tilejson_raster", t("Raster (TileJSON URL)")],
["tile_raster", t("Raster (Tile URLs)")],
["tilejson_raster-dem", t("Raster DEM (TileJSON URL)")],
["tilexyz_raster-dem", t("Raster DEM (XYZ URLs)")],
["pmtiles_vector", t("Vector (PMTiles)")],
["image", t("Image")],
["video", t("Video")],
]}
onChange={newMode => {
setMode(newMode as EditorMode);
setSource(defaultSource(newMode as EditorMode, source));
}}
value={mode as string}
data-wd-key="modal:sources.add.source_type"
/>
<ModalSourcesTypeEditor
onChange={onChangeSource}
mode={mode}
source={source}
/>
<InputButton
className="maputnik-add-source-button"
onClick={onAdd}
data-wd-key="modal:sources.add.add_source"
>
{t("Add Source")}
</InputButton>
</div>;
};
type ModalSourcesInternalProps = { type ModalSourcesInternalProps = {
mapStyle: StyleSpecificationWithId mapStyle: StyleSpecificationWithId
isOpen: boolean isOpen: boolean
@@ -262,68 +277,70 @@ type ModalSourcesInternalProps = {
onStyleChanged: OnStyleChangedCallback onStyleChanged: OnStyleChangedCallback
} & WithTranslation; } & WithTranslation;
function stripTitle(source: SourceSpecification & {title?: string}): SourceSpecification { class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {
const strippedSource = {...source}; stripTitle(source: SourceSpecification & {title?: string}): SourceSpecification {
delete strippedSource["title"]; const strippedSource = {...source};
return strippedSource; delete strippedSource["title"];
return strippedSource;
}
render() {
const {t, mapStyle} = this.props;
const i18nProps = {t, i18n: this.props.i18n, tReady: this.props.tReady};
const activeSources = Object.keys(mapStyle.sources).map(sourceId => {
const source = mapStyle.sources[sourceId];
return <ActiveModalSourcesTypeEditor
key={sourceId}
sourceId={sourceId}
source={source}
onChange={(src: SourceSpecification) => this.props.onStyleChanged(changeSource(mapStyle, sourceId, src))}
onDelete={() => this.props.onStyleChanged(deleteSource(mapStyle, sourceId))}
{...i18nProps}
/>;
});
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};
return <PublicSource
key={sourceId}
id={sourceId}
type={source.type}
title={source.title}
onSelect={() => this.props.onStyleChanged(addSource(mapStyle, sourceId, this.stripTitle(source)))}
/>;
});
return <Modal
data-wd-key="modal:sources"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={t("Sources")}
>
<section className="maputnik-modal-section">
<h1>{t("Active Sources")}</h1>
{activeSources}
</section>
<section className="maputnik-modal-section">
<h1>{t("Choose Public Source")}</h1>
<p>
{t("Add one of the publicly available sources to your style.")}
</p>
<div className="maputnik-public-sources" style={{maxWidth: 500}}>
{tilesetOptions}
</div>
</section>
<section className="maputnik-modal-section">
<h1>{t("Add New Source")}</h1>
<p>{t("Add a new source to your style. You can only choose the source type and id at creation time!")}</p>
<AddSource
onAdd={(sourceId: string, source: SourceSpecification) => this.props.onStyleChanged(addSource(mapStyle, sourceId, source))}
{...i18nProps}
/>
</section>
</Modal>;
}
} }
const ModalSourcesInternal: React.FC<ModalSourcesInternalProps> = (props) => {
const {t, mapStyle} = props;
const i18nProps = {t, i18n: props.i18n, tReady: props.tReady};
const activeSources = Object.keys(mapStyle.sources).map(sourceId => {
const source = mapStyle.sources[sourceId];
return <ActiveModalSourcesTypeEditor
key={sourceId}
sourceId={sourceId}
source={source}
onChange={(src: SourceSpecification) => props.onStyleChanged(changeSource(mapStyle, sourceId, src))}
onDelete={() => props.onStyleChanged(deleteSource(mapStyle, sourceId))}
{...i18nProps}
/>;
});
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};
return <PublicSource
key={sourceId}
id={sourceId}
type={source.type}
title={source.title}
onSelect={() => props.onStyleChanged(addSource(mapStyle, sourceId, stripTitle(source)))}
/>;
});
return <Modal
data-wd-key="modal:sources"
isOpen={props.isOpen}
onOpenToggle={props.onOpenToggle}
title={t("Sources")}
>
<section className="maputnik-modal-section">
<h1>{t("Active Sources")}</h1>
{activeSources}
</section>
<section className="maputnik-modal-section">
<h1>{t("Choose Public Source")}</h1>
<p>
{t("Add one of the publicly available sources to your style.")}
</p>
<div className="maputnik-public-sources" style={{maxWidth: 500}}>
{tilesetOptions}
</div>
</section>
<section className="maputnik-modal-section">
<h1>{t("Add New Source")}</h1>
<p>{t("Add a new source to your style. You can only choose the source type and id at creation time!")}</p>
<AddSource
onAdd={(sourceId: string, source: SourceSpecification) => props.onStyleChanged(addSource(mapStyle, sourceId, source))}
{...i18nProps}
/>
</section>
</Modal>;
};
export const ModalSources = withTranslation()(ModalSourcesInternal); export const ModalSources = withTranslation()(ModalSourcesInternal);
+259 -242
View File
@@ -24,21 +24,23 @@ type TileJSONSourceEditorProps = {
} & WithTranslation; } & WithTranslation;
const TileJSONSourceEditor: React.FC<TileJSONSourceEditorProps> = (props) => { class TileJSONSourceEditor extends React.Component<TileJSONSourceEditorProps> {
const t = props.t; render() {
return <div> const t = this.props.t;
<FieldUrl return <div>
label={t("TileJSON URL")} <FieldUrl
fieldSpec={latest.source_vector.url} label={t("TileJSON URL")}
value={props.source.url} fieldSpec={latest.source_vector.url}
onChange={url => props.onChange({ value={this.props.source.url}
...props.source, onChange={url => this.props.onChange({
url: url ...this.props.source,
})} url: url
/> })}
{props.children} />
</div>; {this.props.children}
}; </div>;
}
}
type TileURLSourceEditorProps = { type TileURLSourceEditorProps = {
source: { source: {
@@ -51,63 +53,66 @@ type TileURLSourceEditorProps = {
children?: React.ReactNode children?: React.ReactNode
} & WithTranslation; } & WithTranslation;
const TileURLSourceEditor: React.FC<TileURLSourceEditorProps> = (props) => { class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
function changeTileUrls(tiles: string[]) { changeTileUrls(tiles: string[]) {
props.onChange({ this.props.onChange({
...props.source, ...this.props.source,
tiles, tiles,
}); });
} }
function renderTileUrls() { renderTileUrls() {
const tiles = props.source.tiles || []; const tiles = this.props.source.tiles || [];
return <FieldDynamicArray return <FieldDynamicArray
label={props.t("Tile URL")} label={this.props.t("Tile URL")}
fieldSpec={latest.source_vector.tiles} fieldSpec={latest.source_vector.tiles}
type="url" type="url"
value={tiles} value={tiles}
onChange={changeTileUrls} onChange={this.changeTileUrls.bind(this)}
/>; />;
} }
const t = props.t; render() {
return <div> const t = this.props.t;
{renderTileUrls()} return <div>
<FieldSelect {this.renderTileUrls()}
label={t("Scheme Type")} <FieldSelect
fieldSpec={latest.source_vector.scheme} label={t("Scheme Type")}
options={[ fieldSpec={latest.source_vector.scheme}
["xyz", "xyz (Slippy map tilenames scheme)"], options={[
["tms", "tms (OSGeo spec scheme)"], ["xyz", "xyz (Slippy map tilenames scheme)"],
]} ["tms", "tms (OSGeo spec scheme)"],
onChange={scheme => props.onChange({ ]}
...props.source, onChange={scheme => this.props.onChange({
scheme ...this.props.source,
})} scheme
value={props.source.scheme} })}
data-wd-key="modal:sources.add.scheme_type" value={this.props.source.scheme}
/> data-wd-key="modal:sources.add.scheme_type"
<FieldNumber />
label={t("Min Zoom")} <FieldNumber
fieldSpec={latest.source_vector.minzoom} label={t("Min Zoom")}
value={props.source.minzoom || 0} fieldSpec={latest.source_vector.minzoom}
onChange={minzoom => props.onChange({ value={this.props.source.minzoom || 0}
...props.source, onChange={minzoom => this.props.onChange({
minzoom: minzoom ...this.props.source,
})} minzoom: minzoom
/> })}
<FieldNumber />
label={t("Max Zoom")} <FieldNumber
fieldSpec={latest.source_vector.maxzoom} label={t("Max Zoom")}
value={props.source.maxzoom || 22} fieldSpec={latest.source_vector.maxzoom}
onChange={maxzoom => props.onChange({ value={this.props.source.maxzoom || 22}
...props.source, onChange={maxzoom => this.props.onChange({
maxzoom: maxzoom ...this.props.source,
})} maxzoom: maxzoom
/> })}
{props.children} />
</div>; {this.props.children}
}; </div>;
}
}
const createCornerLabels: (t: TFunction) => { label: string, key: string }[] = (t) => ([ const createCornerLabels: (t: TFunction) => { label: string, key: string }[] = (t) => ([
{ label: t("Coord top left"), key: "top left" }, { label: t("Coord top left"), key: "top left" },
@@ -124,43 +129,45 @@ type ImageSourceEditorProps = {
onChange(...args: unknown[]): unknown onChange(...args: unknown[]): unknown
} & WithTranslation; } & WithTranslation;
const ImageSourceEditor: React.FC<ImageSourceEditorProps> = (props) => { class ImageSourceEditor extends React.Component<ImageSourceEditorProps> {
const t = props.t; render() {
const changeCoord = (idx: number, val: [number, number]) => { const t = this.props.t;
const coordinates = props.source.coordinates.slice(0); const changeCoord = (idx: number, val: [number, number]) => {
coordinates[idx] = val; const coordinates = this.props.source.coordinates.slice(0);
coordinates[idx] = val;
props.onChange({ this.props.onChange({
...props.source, ...this.props.source,
coordinates, coordinates,
}); });
}; };
return <div> return <div>
<FieldUrl <FieldUrl
label={t("Image URL")} label={t("Image URL")}
fieldSpec={latest.source_image.url} fieldSpec={latest.source_image.url}
value={props.source.url} value={this.props.source.url}
onChange={url => props.onChange({ onChange={url => this.props.onChange({
...props.source, ...this.props.source,
url, url,
})}
/>
{createCornerLabels(t).map(({label, key}, idx) => {
return (
<FieldArray
label={label}
key={key}
length={2}
type="number"
value={this.props.source.coordinates[idx]}
default={[0, 0]}
onChange={(val: [number, number]) => changeCoord(idx, val)}
/>
);
})} })}
/> </div>;
{createCornerLabels(t).map(({label, key}, idx) => { }
return ( }
<FieldArray
label={label}
key={key}
length={2}
type="number"
value={props.source.coordinates[idx]}
default={[0, 0]}
onChange={(val: [number, number]) => changeCoord(idx, val)}
/>
);
})}
</div>;
};
type VideoSourceEditorProps = { type VideoSourceEditorProps = {
source: { source: {
@@ -170,49 +177,51 @@ type VideoSourceEditorProps = {
onChange(...args: unknown[]): unknown onChange(...args: unknown[]): unknown
} & WithTranslation; } & WithTranslation;
const VideoSourceEditor: React.FC<VideoSourceEditorProps> = (props) => { class VideoSourceEditor extends React.Component<VideoSourceEditorProps> {
const t = props.t; render() {
const changeCoord = (idx: number, val: [number, number]) => { const t = this.props.t;
const coordinates = props.source.coordinates.slice(0); const changeCoord = (idx: number, val: [number, number]) => {
coordinates[idx] = val; const coordinates = this.props.source.coordinates.slice(0);
coordinates[idx] = val;
props.onChange({ this.props.onChange({
...props.source, ...this.props.source,
coordinates, coordinates,
}); });
}; };
const changeUrls = (urls: string[]) => { const changeUrls = (urls: string[]) => {
props.onChange({ this.props.onChange({
...props.source, ...this.props.source,
urls, urls,
}); });
}; };
return <div> return <div>
<FieldDynamicArray <FieldDynamicArray
label={t("Video URL")} label={t("Video URL")}
fieldSpec={latest.source_video.urls} fieldSpec={latest.source_video.urls}
type="string" type="string"
value={props.source.urls} value={this.props.source.urls}
default={[]} default={[]}
onChange={changeUrls} onChange={changeUrls}
/> />
{createCornerLabels(t).map(({label, key}, idx) => { {createCornerLabels(t).map(({label, key}, idx) => {
return ( return (
<FieldArray <FieldArray
label={label} label={label}
key={key} key={key}
length={2} length={2}
type="number" type="number"
value={props.source.coordinates[idx]} value={this.props.source.coordinates[idx]}
default={[0, 0]} default={[0, 0]}
onChange={(val: [number, number]) => changeCoord(idx, val)} onChange={(val: [number, number]) => changeCoord(idx, val)}
/> />
); );
})} })}
</div>; </div>;
}; }
}
type GeoJSONSourceUrlEditorProps = { type GeoJSONSourceUrlEditorProps = {
source: { source: {
@@ -221,18 +230,20 @@ type GeoJSONSourceUrlEditorProps = {
onChange(...args: unknown[]): unknown onChange(...args: unknown[]): unknown
} & WithTranslation; } & WithTranslation;
const GeoJSONSourceUrlEditor: React.FC<GeoJSONSourceUrlEditorProps> = (props) => { class GeoJSONSourceUrlEditor extends React.Component<GeoJSONSourceUrlEditorProps> {
const t = props.t; render() {
return <FieldUrl const t = this.props.t;
label={t("GeoJSON URL")} return <FieldUrl
fieldSpec={latest.source_geojson.data} label={t("GeoJSON URL")}
value={props.source.data} fieldSpec={latest.source_geojson.data}
onChange={data => props.onChange({ value={this.props.source.data}
...props.source, onChange={data => this.props.onChange({
data: data ...this.props.source,
})} data: data
/>; })}
}; />;
}
}
type GeoJSONSourceFieldJsonEditorProps = { type GeoJSONSourceFieldJsonEditorProps = {
source: { source: {
@@ -242,33 +253,35 @@ type GeoJSONSourceFieldJsonEditorProps = {
onChange(...args: unknown[]): unknown onChange(...args: unknown[]): unknown
} & WithTranslation; } & WithTranslation;
const GeoJSONSourceFieldJsonEditor: React.FC<GeoJSONSourceFieldJsonEditorProps> = (props) => { class GeoJSONSourceFieldJsonEditor extends React.Component<GeoJSONSourceFieldJsonEditorProps> {
const t = props.t; render() {
return <div> const t = this.props.t;
<Block label={t("GeoJSON")} fieldSpec={latest.source_geojson.data}> return <div>
<FieldJson <Block label={t("GeoJSON")} fieldSpec={latest.source_geojson.data}>
value={props.source.data} <FieldJson
lintType="json" value={this.props.source.data}
onChange={data => { lintType="json"
props.onChange({ onChange={data => {
...props.source, this.props.onChange({
data, ...this.props.source,
data,
});
}}
/>
</Block>
<FieldCheckbox
label={t("Cluster")}
value={this.props.source.cluster}
onChange={cluster => {
this.props.onChange({
...this.props.source,
cluster: cluster,
}); });
}} }}
/> />
</Block> </div>;
<FieldCheckbox }
label={t("Cluster")} }
value={props.source.cluster}
onChange={cluster => {
props.onChange({
...props.source,
cluster: cluster,
});
}}
/>
</div>;
};
type PMTilesSourceEditorProps = { type PMTilesSourceEditorProps = {
source: { source: {
@@ -278,22 +291,24 @@ type PMTilesSourceEditorProps = {
children?: React.ReactNode children?: React.ReactNode
} & WithTranslation; } & WithTranslation;
const PMTilesSourceEditor: React.FC<PMTilesSourceEditorProps> = (props) => { class PMTilesSourceEditor extends React.Component<PMTilesSourceEditorProps> {
const t = props.t; render() {
return <div> const t = this.props.t;
<FieldUrl return <div>
label={t("PMTiles URL")} <FieldUrl
fieldSpec={latest.source_vector.url} label={t("PMTiles URL")}
value={props.source.url} fieldSpec={latest.source_vector.url}
data-wd-key="modal:sources.add.source_url" value={this.props.source.url}
onChange={(url: string) => props.onChange({ data-wd-key="modal:sources.add.source_url"
...props.source, onChange={(url: string) => this.props.onChange({
url: url.startsWith("pmtiles://") ? url : `pmtiles://${url}` ...this.props.source,
})} url: url.startsWith("pmtiles://") ? url : `pmtiles://${url}`
/> })}
{props.children} />
</div>; {this.props.children}
}; </div>;
}
}
type ModalSourcesTypeEditorInternalProps = { type ModalSourcesTypeEditorInternalProps = {
mode: EditorMode mode: EditorMode
@@ -301,61 +316,63 @@ type ModalSourcesTypeEditorInternalProps = {
onChange(...args: unknown[]): unknown onChange(...args: unknown[]): unknown
} & WithTranslation; } & WithTranslation;
const ModalSourcesTypeEditorInternal: React.FC<ModalSourcesTypeEditorInternalProps> = (props) => { class ModalSourcesTypeEditorInternal extends React.Component<ModalSourcesTypeEditorInternalProps> {
const t = props.t; render() {
const commonProps = { const t = this.props.t;
source: props.source, const commonProps = {
onChange: props.onChange, source: this.props.source,
t: props.t, onChange: this.props.onChange,
i18n: props.i18n, t: this.props.t,
tReady: props.tReady, i18n: this.props.i18n,
}; tReady: this.props.tReady,
switch(props.mode) { };
case "geojson_url": return <GeoJSONSourceUrlEditor {...commonProps} />; switch(this.props.mode) {
case "geojson_json": return <GeoJSONSourceFieldJsonEditor {...commonProps} />; case "geojson_url": return <GeoJSONSourceUrlEditor {...commonProps} />;
case "tilejson_vector": return <TileJSONSourceEditor {...commonProps} />; case "geojson_json": return <GeoJSONSourceFieldJsonEditor {...commonProps} />;
case "tile_vector": return <TileURLSourceEditor {...commonProps} />; case "tilejson_vector": return <TileJSONSourceEditor {...commonProps} />;
case "tilejson_raster": return <TileJSONSourceEditor {...commonProps} />; case "tile_vector": return <TileURLSourceEditor {...commonProps} />;
case "tile_raster": return <TileURLSourceEditor {...commonProps}> case "tilejson_raster": return <TileJSONSourceEditor {...commonProps} />;
<FieldNumber case "tile_raster": return <TileURLSourceEditor {...commonProps}>
label={t("Tile Size")} <FieldNumber
fieldSpec={latest.source_raster.tileSize} label={t("Tile Size")}
onChange={tileSize => props.onChange({ fieldSpec={latest.source_raster.tileSize}
...props.source, onChange={tileSize => this.props.onChange({
tileSize: tileSize ...this.props.source,
})} tileSize: tileSize
value={props.source.tileSize || latest.source_raster.tileSize.default} })}
data-wd-key="modal:sources.add.tile_size" value={this.props.source.tileSize || latest.source_raster.tileSize.default}
/> data-wd-key="modal:sources.add.tile_size"
</TileURLSourceEditor>; />
case "tilejson_raster-dem": return <TileJSONSourceEditor {...commonProps} />; </TileURLSourceEditor>;
case "tilexyz_raster-dem": return <TileURLSourceEditor {...commonProps}> case "tilejson_raster-dem": return <TileJSONSourceEditor {...commonProps} />;
<FieldNumber case "tilexyz_raster-dem": return <TileURLSourceEditor {...commonProps}>
label={t("Tile Size")} <FieldNumber
fieldSpec={latest.source_raster_dem.tileSize} label={t("Tile Size")}
onChange={tileSize => props.onChange({ fieldSpec={latest.source_raster_dem.tileSize}
...props.source, onChange={tileSize => this.props.onChange({
tileSize: tileSize ...this.props.source,
})} tileSize: tileSize
value={props.source.tileSize || latest.source_raster_dem.tileSize.default} })}
data-wd-key="modal:sources.add.tile_size" value={this.props.source.tileSize || latest.source_raster_dem.tileSize.default}
/> data-wd-key="modal:sources.add.tile_size"
<FieldSelect />
label={t("Encoding")} <FieldSelect
fieldSpec={latest.source_raster_dem.encoding} label={t("Encoding")}
options={Object.keys(latest.source_raster_dem.encoding.values)} fieldSpec={latest.source_raster_dem.encoding}
onChange={encoding => props.onChange({ options={Object.keys(latest.source_raster_dem.encoding.values)}
...props.source, onChange={encoding => this.props.onChange({
encoding: encoding ...this.props.source,
})} encoding: encoding
value={props.source.encoding || latest.source_raster_dem.encoding.default} })}
/> value={this.props.source.encoding || latest.source_raster_dem.encoding.default}
</TileURLSourceEditor>; />
case "pmtiles_vector": return <PMTilesSourceEditor {...commonProps} />; </TileURLSourceEditor>;
case "image": return <ImageSourceEditor {...commonProps} />; case "pmtiles_vector": return <PMTilesSourceEditor {...commonProps} />;
case "video": return <VideoSourceEditor {...commonProps} />; case "image": return <ImageSourceEditor {...commonProps} />;
default: return null; case "video": return <VideoSourceEditor {...commonProps} />;
default: return null;
}
} }
}; }
export const ModalSourcesTypeEditor = withTranslation()(ModalSourcesTypeEditorInternal); export const ModalSourcesTypeEditor = withTranslation()(ModalSourcesTypeEditorInternal);
+1 -1
View File
@@ -119,7 +119,7 @@ function createMaplibreExpressionLinter(spec?: StylePropertySpecification) {
const text = view.state.doc.toString(); const text = view.state.doc.toString();
const parsedJson = JSON.parse(text); const parsedJson = JSON.parse(text);
const ast = jsonToAst(text); const ast = jsonToAst(text);
const out = expression.createExpression(parsedJson, spec); const out = expression.createExpression(parsedJson, "expression", spec);
if (out?.result !== "error") { if (out?.result !== "error") {
return []; return [];
} }
+2 -2
View File
@@ -5,14 +5,14 @@ import { type LayerSpecification } from "maplibre-gl";
export function changeType(layer: LayerSpecification, newType: string): LayerSpecification { export function changeType(layer: LayerSpecification, newType: string): LayerSpecification {
const changedPaintProps: LayerSpecification["paint"] = { ...layer.paint }; const changedPaintProps: LayerSpecification["paint"] = { ...layer.paint };
Object.keys(changedPaintProps).forEach(propertyName => { Object.keys(changedPaintProps).forEach(propertyName => {
if(!(propertyName in latest["paint_" + newType])) { if(!(propertyName in ((latest["paint_" + newType as keyof typeof latest]) as string[]))) {
delete changedPaintProps[propertyName as keyof LayerSpecification["paint"]]; delete changedPaintProps[propertyName as keyof LayerSpecification["paint"]];
} }
}); });
const changedLayoutProps: LayerSpecification["layout"] = { ...layer.layout }; const changedLayoutProps: LayerSpecification["layout"] = { ...layer.layout };
Object.keys(changedLayoutProps).forEach(propertyName => { Object.keys(changedLayoutProps).forEach(propertyName => {
if(!(propertyName in latest["layout_" + newType])) { if(!(propertyName in ((latest["layout_" + newType as keyof typeof latest]) as string[]))) {
delete changedLayoutProps[propertyName as keyof LayerSpecification["layout"]]; delete changedLayoutProps[propertyName as keyof LayerSpecification["layout"]];
} }
}); });