Compare commits

...

233 Commits

Author SHA1 Message Date
Yuri Astrakhan
a99cbc00ba Merge pull request #812 from kevinschaul/switch-to-maplibre-ks
Convert from Mapbox GL to MapLibre
2023-07-18 23:32:11 +02:00
Kevin Schaul
fe5f7e8b8c upgrade to setup-go@v3 2023-07-18 15:16:29 -05:00
Kevin Schaul
3ed4b8f2d7 add GOBIN to CI 2023-07-18 11:31:01 -05:00
Kevin Schaul
f17c2e8112 reenable go modules in CI 2023-07-18 10:50:48 -05:00
Kevin Schaul
2be447f105 downgrade to node 16 to avoid ssl issue (for now) 2023-07-18 10:10:08 -05:00
Kevin Schaul
2fe6fa2be6 Update workflow to latest desktop, drop old node 2023-07-18 09:52:29 -05:00
Kevin Schaul
83dd21414b Merge branch 'master' into switch-to-maplibre-ks 2023-07-13 14:48:38 -05:00
Kevin Schaul
56d96a248d Remove JSON.stringify in call to validate 2023-07-13 14:43:18 -05:00
Kevin Schaul
5b1ee7296b Fix Buffer is undefined error 2023-07-13 14:35:16 -05:00
Kevin Schaul
8e0546fba4 Get map rendering with maplibre 2023-07-12 16:37:19 -05:00
Yuri Astrakhan
2ff3d08bb0 Merge pull request #796 from maputnik/dependabot/npm_and_yarn/async-2.6.4
Bump async from 2.6.3 to 2.6.4
2022-12-15 15:14:51 -05:00
dependabot[bot]
afe7a492a7 Bump async from 2.6.3 to 2.6.4
Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4)

---
updated-dependencies:
- dependency-name: async
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-15 19:54:14 +00:00
Yuri Astrakhan
1f26ab707f Merge pull request #792 from maputnik/dependabot/npm_and_yarn/deep-object-diff-1.1.9
Bump deep-object-diff from 1.1.7 to 1.1.9
2022-12-15 14:53:11 -05:00
Yuri Astrakhan
233191e27c Merge pull request #793 from maputnik/dependabot/npm_and_yarn/loader-utils-1.4.2
Bump loader-utils from 1.4.1 to 1.4.2
2022-12-15 14:52:58 -05:00
Yuri Astrakhan
246f9a191d Merge pull request #795 from maputnik/dependabot/npm_and_yarn/decode-uri-component-0.2.2
Bump decode-uri-component from 0.2.0 to 0.2.2
2022-12-15 14:52:17 -05:00
dependabot[bot]
07f6efe45d Bump decode-uri-component from 0.2.0 to 0.2.2
Bumps [decode-uri-component](https://github.com/SamVerschueren/decode-uri-component) from 0.2.0 to 0.2.2.
- [Release notes](https://github.com/SamVerschueren/decode-uri-component/releases)
- [Commits](https://github.com/SamVerschueren/decode-uri-component/compare/v0.2.0...v0.2.2)

---
updated-dependencies:
- dependency-name: decode-uri-component
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-07 23:57:21 +00:00
dependabot[bot]
ccd0402eea Bump loader-utils from 1.4.1 to 1.4.2
Bumps [loader-utils](https://github.com/webpack/loader-utils) from 1.4.1 to 1.4.2.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v1.4.2/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v1.4.1...v1.4.2)

---
updated-dependencies:
- dependency-name: loader-utils
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-18 00:36:06 +00:00
dependabot[bot]
8ccee0ba75 Bump deep-object-diff from 1.1.7 to 1.1.9
Bumps [deep-object-diff](https://github.com/mattphillips/deep-object-diff) from 1.1.7 to 1.1.9.
- [Release notes](https://github.com/mattphillips/deep-object-diff/releases)
- [Commits](https://github.com/mattphillips/deep-object-diff/commits)

---
updated-dependencies:
- dependency-name: deep-object-diff
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-16 23:59:29 +00:00
Yuri Astrakhan
d6b67be7b2 Merge pull request #780 from maputnik/dependabot/npm_and_yarn/ejs-3.1.7
Bump ejs from 3.1.6 to 3.1.7
2022-11-09 14:52:11 -05:00
Yuri Astrakhan
ac56ea4627 Merge pull request #791 from maputnik/dependabot/npm_and_yarn/loader-utils-1.4.1
Bump loader-utils from 1.4.0 to 1.4.1
2022-11-09 14:51:37 -05:00
dependabot[bot]
b00cf66ea6 Bump loader-utils from 1.4.0 to 1.4.1
Bumps [loader-utils](https://github.com/webpack/loader-utils) from 1.4.0 to 1.4.1.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v1.4.1/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v1.4.0...v1.4.1)

---
updated-dependencies:
- dependency-name: loader-utils
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-09 03:55:59 +00:00
Luke Seelenbinder
8e329a0ff9 Remove missed reference to mapbox-gl.
* Also removes specialized support for Mapbox URLs.
2022-11-01 10:24:05 +01:00
Luke Seelenbinder
74cacd5bdf Initial work to convert from Mapbox GL v1.13 to MapLibre v2.4.0. 2022-10-27 14:02:47 +02:00
pathmapper
7d5fb23130 Merge pull request #782 from maputnik/dependabot/npm_and_yarn/got-11.8.5
Bump got from 11.8.3 to 11.8.5
2022-08-14 09:25:13 +02:00
pathmapper
08bbd55f13 Merge pull request #784 from maputnik/dependabot/npm_and_yarn/terser-4.8.1
Bump terser from 4.8.0 to 4.8.1
2022-08-14 09:25:00 +02:00
dependabot[bot]
d6d4930513 Bump terser from 4.8.0 to 4.8.1
Bumps [terser](https://github.com/terser/terser) from 4.8.0 to 4.8.1.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-21 03:45:50 +00:00
dependabot[bot]
6220e15723 Bump got from 11.8.3 to 11.8.5
Bumps [got](https://github.com/sindresorhus/got) from 11.8.3 to 11.8.5.
- [Release notes](https://github.com/sindresorhus/got/releases)
- [Commits](https://github.com/sindresorhus/got/compare/v11.8.3...v11.8.5)

---
updated-dependencies:
- dependency-name: got
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-23 14:02:05 +00:00
dependabot[bot]
72053a2dba Bump ejs from 3.1.6 to 3.1.7
Bumps [ejs](https://github.com/mde/ejs) from 3.1.6 to 3.1.7.
- [Release notes](https://github.com/mde/ejs/releases)
- [Changelog](https://github.com/mde/ejs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mde/ejs/compare/v3.1.6...v3.1.7)

---
updated-dependencies:
- dependency-name: ejs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-07 04:59:25 +00:00
pathmapper
bf27a35ef5 Merge pull request #778 from propheel/feat/updateDependencies
Update dependencies
2022-05-07 06:57:58 +02:00
Filip Proborszcz
4705bf823a Use geckodriver 0.30 for firefox until 0.31 works 2022-05-02 15:13:42 +02:00
Filip Proborszcz
a8f6208561 Fix layer drag&drop and init warning 2022-05-01 11:28:15 +02:00
Filip Proborszcz
af2629be75 Make layer list sortable again 2022-04-10 22:22:16 +02:00
pathmapper
8bfad6c9fd Merge pull request #777 from maputnik/npm-ci
Use npm ci for workflow
2022-04-09 07:51:55 +02:00
Filip Proborszcz
5c3713da90 Use proper version of array-move 2022-04-08 04:14:58 +02:00
Filip Proborszcz
174eae1cf4 Use selenium standalone service to run tests 2022-04-08 03:37:40 +02:00
Filip Proborszcz
d73add77e7 Fixes for breaking changes
- new webpack dev server options
- babel support for async functions in hooks
- new uuid import style
- automatically open browser for testing
2022-04-08 02:23:43 +02:00
Filip Proborszcz
ab00c9f426 Many dependency updates
@storybook 6.4.20 blocks most of others like: react 18, webpack 5,
and keeps lots of unnecessary dependencies due to chakra 2 dep.
2022-04-07 17:21:34 +02:00
pathmapper
d6ab302815 Use npm ci for workflow
https://docs.npmjs.com/cli/v8/commands/npm-ci
2022-04-07 03:28:47 +02:00
pathmapper
f5646f57d1 Merge pull request #776 from propheel/useNode16
Use node 16.x
2022-04-07 03:11:13 +02:00
Filip Proborszcz
c77d8f6625 Use shared SHM for Docker image 2022-04-07 00:37:20 +02:00
Filip Proborszcz
e34c1ca4be Use node 16.x
It required converting mocha tests code into async since [@wdio/sync is
deprecated](https://webdriver.io/docs/sync-vs-async/) starting with
node v16.
It removed the dependency on fibers and on [node-gyp + python](https://
webdriver.io/docs/sync-vs-async/#common-issues-in-sync-mode) indirectly
though which is a great thing.

Also moved away from node-sass to sass since [node-sass is deprecated]
(https://sass-lang.com/blog/libsass-is-deprecated).
2022-04-06 14:05:15 +02:00
Orange Mug
87745f1fc9 Merge pull request #752 from pathmapper/webpack_stats
Webpack stats
2021-08-06 17:59:46 +01:00
pathmapper
9ba0fd5f39 Remove yarn.lock 2021-08-04 09:44:33 +02:00
pathmapper
70decbb5c1 Change webpack-dev-server info 2021-08-04 09:36:37 +02:00
pathmapper
51fa4a4377 Merge pull request #739 from pathmapper/maputnik_tag
Request using 'maputnik' tag for SE questions
2020-10-10 20:16:33 +02:00
pathmapper
fb6f4d73e2 Mention 'maputnik' tag in comment 2020-10-04 12:11:00 +02:00
pathmapper
63b14933ba Merge pull request #729 from orangemug/fix/issue-712-v2
Correctly upgrade old-style filter functions to expressions
2020-09-15 19:10:49 +02:00
Orange Mug
a86c31cefa Merge pull request #727 from orangemug/feature/added-html-option-to-export
Added HTML export option to export modal.
2020-09-10 19:20:40 +01:00
orangemug
25e2554412 Another commit to force a rebuild 2020-09-10 18:07:47 +01:00
Orange Mug
34bb3bc0a7 Merge pull request #726 from orangemug/fix/issue-718
Fix to not change layer key while editing layer
2020-09-10 18:06:03 +01:00
orangemug
852243cd52 onChange -> onInput for <FieldId/> component. 2020-09-09 20:22:52 +01:00
Orange Mug
40faf86adf Merge pull request #725 from orangemug/fix/issue-710
Added 'base' to functions
2020-09-09 20:15:18 +01:00
Orange Mug
bb69f143b8 Merge pull request #724 from orangemug/fix/issue-707
Remove key/value from style when editing style results in empty array
2020-09-09 20:14:58 +01:00
orangemug
bb43200887 Move logic into checkIfSimpleFilter function and added FILTER_OPS check. 2020-09-09 16:12:18 +01:00
orangemug
ae3f79f4ad Attempt to correctly upgrade old-style filter functions to expressions. 2020-09-09 15:55:20 +01:00
orangemug
731a315624 Merge remote-tracking branch 'upstream/master' into feature/added-html-option-to-export 2020-09-09 15:12:23 +01:00
orangemug
5e441454d5 Merge remote-tracking branch 'upstream/master' into fix/issue-718 2020-09-09 15:11:52 +01:00
orangemug
a55716bbd9 Merge remote-tracking branch 'upstream/master' into fix/issue-710 2020-09-09 15:08:42 +01:00
orangemug
44aea3745e Merge remote-tracking branch 'upstream/master' into fix/issue-707 2020-09-09 15:07:35 +01:00
Orange Mug
a572bc02a6 Merge pull request #728 from orangemug/fix/disable-host-check
Disable host check for codesandbox
2020-09-09 15:06:54 +01:00
orangemug
4dee95fa2e Added --disable-host-check 2020-09-09 15:02:14 +01:00
orangemug
381ff6292f Removed disableHostCheck from ./config/webpack.config.js 2020-09-09 14:56:39 +01:00
orangemug
c12db1703b Changed start script for codesandbox. 2020-09-09 14:55:51 +01:00
orangemug
2676583833 Added start-sandbox script. 2020-09-09 14:55:12 +01:00
orangemug
6ca2af7f8a Change codesandbox check. 2020-09-09 14:46:09 +01:00
orangemug
553b17822d Try disable host check for codesandbox. 2020-09-09 14:19:06 +01:00
orangemug
a6148e5f40 Added HTML export option to export modal. 2020-09-09 14:06:48 +01:00
orangemug
4f77629eb7 Fix to not change layer key while editing layer. 2020-09-08 17:58:33 +01:00
orangemug
9103d9560a Added 'base' to functions. 2020-09-08 16:50:29 +01:00
orangemug
06c63509f7 Remove key/value from style when editing style results in empty array 2020-09-08 16:12:41 +01:00
pathmapper
bbe0af6c0e Merge pull request #716 from pathmapper/Use-desktop-v1.0.7
Use desktop v1.0.7 for CI workflow
2020-07-24 19:11:54 +02:00
pathmapper
7455ccc3b7 Use desktop v1.0.7 2020-07-24 18:41:11 +02:00
Orange Mug
8b766777ac Merge pull request #706 from orangemug/fix/input-label-a11y
Improved label accessibility
2020-06-30 10:50:37 +01:00
orangemug
8441abe907 Added padding to 'maputnik-doc-button' and removed cursor from doc label. 2020-06-30 10:30:26 +01:00
orangemug
ca56951256 Fix styling in export model 2020-06-30 09:48:55 +01:00
orangemug
5981151b27 Added cursor pointer to SDK docs button 2020-06-30 09:43:24 +01:00
orangemug
21dbc6c4d9 Added back in https://github.com/maputnik/editor link 2020-06-30 09:42:05 +01:00
Orange Mug
6f060c2a0a Merge pull request #708 from pathmapper/update_mb_deps
Update mb-gl
2020-06-30 09:27:02 +01:00
pathmapper
24327541c5 Update mb-gl 2020-06-30 09:16:53 +02:00
orangemug
0d6b9ee9d4 Fixed source modal CSS for new block definitions. 2020-06-29 16:26:28 +01:00
orangemug
3ad487dce7 Removed isRequired from label incase null. 2020-06-29 16:18:04 +01:00
orangemug
a46c834874 Fixed data functions. 2020-06-29 16:14:35 +01:00
orangemug
67bdea1827 Re-added info button and SDK docs to fields after refactor. 2020-06-29 16:03:59 +01:00
Orange Mug
cc4133aac1 Merge pull request #703 from pathmapper/fix_674
Add again shouldComponentUpdate
2020-06-29 15:41:49 +01:00
orangemug
4a6f58d61c Changed heading sizes for modals. 2020-06-10 20:37:46 +01:00
orangemug
e3dc98b76d <h2/> -> <h1/> 2020-06-10 20:37:08 +01:00
orangemug
09373dda44 Fixed changing between zoom/data functions. 2020-06-10 20:26:39 +01:00
orangemug
c4b05b62b3 Remove logging. 2020-06-10 19:44:05 +01:00
orangemug
06bccfab10 Fix for checkboxes within non-clickable label 2020-06-10 19:21:29 +01:00
orangemug
b83c9a1ad9 Fix block styling issues. 2020-06-10 19:20:18 +01:00
orangemug
0279daf7bd 'getApplicationNode' doesn't appear to be required as the modal is already a dialog 2020-06-10 18:17:25 +01:00
orangemug
bfada7cace Added aria-label to things that are otherwise labelled in the UI. 2020-06-10 18:16:43 +01:00
orangemug
6c751fe1c4 Updated maputnik/design to diagnose axe accessibility checker issue with SVGs 2020-06-10 17:18:11 +01:00
orangemug
34299c94ee Fixed some 'axe' accessibility checker issues. 2020-06-10 16:22:13 +01:00
orangemug
5804b3c72a CSS outline fixes for keyboard users. 2020-06-10 13:04:04 +01:00
orangemug
8ae6e9fc61 Fix to ignore click suppression on inputs 2020-06-10 13:02:38 +01:00
orangemug
40579c3e0c Added back in action buttons to input label/fieldset 2020-06-10 10:59:44 +01:00
orangemug
f3906c8dd8 A role="navigation" should not be on <ul/> as it changes how screen readers announce them. 2020-06-10 10:25:56 +01:00
orangemug
f911ed3522 Fix <InputColor/> issues when trying to close picker. 2020-06-10 10:10:40 +01:00
orangemug
2cc179acc1 Fixed more input accessibility issues, also
- Added searchParams based router for easier testing
 - Added more stories to the storybook
2020-06-09 19:11:07 +01:00
Orange Mug
2912db6e32 Merge pull request #704 from pathmapper/update_omt_styles
Update Basic and Toner styles
2020-06-06 12:32:34 +01:00
pathmapper
70eb3e785a Trigger codesandbox 2020-06-06 12:48:34 +02:00
pathmapper
8f944d9973 Empty Commit to trigger codesanbox again 2020-06-06 12:46:53 +02:00
pathmapper
8faf841f3d Update Basic and Toner 2020-06-06 11:29:24 +02:00
pathmapper
d8ba8fcbfb Add again shouldComponentUpdate 2020-06-05 15:11:49 +02:00
orangemug
d6f31ec82e Block* -> Field* 2020-06-03 17:11:47 +01:00
Orange Mug
b19eacf4f9 Merge pull request #699 from orangemug/maintenance/component-refactor
Tidy of components + added storybook.js
2020-06-03 16:18:04 +01:00
Orange Mug
3d158a791a Merge pull request #698 from pathmapper/issue_668
Integrate desktop builds in ci workflow
2020-06-03 14:53:06 +01:00
pathmapper
04b3b42524 Use desktop version instead of SHA 2020-06-03 15:37:17 +02:00
pathmapper
af92aac7ec Remove tests 2020-06-03 13:45:59 +02:00
orangemug
90dfbf37e0 Added 'a11y' and 'source' addons to storybook as well as more stories 2020-06-03 09:52:54 +01:00
pathmapper
e21f412933 Update ci.yml 2020-06-03 10:26:44 +02:00
pathmapper
da297fe82c Checkout a particular SHA
https://github.com/maputnik/editor/pull/698#issuecomment-636748873
2020-06-03 10:12:32 +02:00
orangemug
624ccb5b00 Tidy of components
- Moved all components into a single directory like nextjs
 - Made component names consistent with each other
 - Made component names consistent with their export class names
 - Added storybook for a few components with the aim to extend this further.
2020-06-01 16:09:32 +01:00
pathmapper
9f0e5641ab Integrate desktop builds in ci workflow 2020-05-31 21:54:53 +02:00
Orange Mug
d07b40ccef Merge pull request #696 from orangemug/feature/add-more-functional-tests
Added more webdriver tests
2020-05-31 16:05:28 +01:00
orangemug
e0abd8251d Remove temp file 2020-05-31 15:57:23 +01:00
orangemug
324452e714 Disable undo/redo again. 2020-05-31 15:48:20 +01:00
orangemug
8d3ad6b1a1 Added more functional tests. 2020-05-31 15:33:09 +01:00
Orange Mug
3d4cc34a08 Merge pull request #695 from pathmapper/node_14
Add Node 14 to CI workflow
2020-05-28 11:48:59 +01:00
pathmapper
ff351716b6 Update node-sass 2020-05-28 12:26:28 +02:00
Orange Mug
c963a8cc59 Merge pull request #694 from pathmapper/issue_templates
Point users to https://gis.stackexchange.com/ in issue template
2020-05-28 09:39:53 +01:00
Orange Mug
52ad980aef Merge pull request #693 from pathmapper/update-mb-deps
Update GL JS and Style Spec
2020-05-28 09:38:56 +01:00
pathmapper
fb04cce650 Update wdio deps 2020-05-28 08:56:23 +02:00
pathmapper
4b8acb10b0 Add Node 14 2020-05-28 08:39:57 +02:00
pathmapper
86d67389fc Update bug_report.md 2020-05-28 08:30:11 +02:00
pathmapper
9dad53e444 Point users to https://gis.stackexchange.com/ 2020-05-28 08:23:14 +02:00
pathmapper
d5afeb14c1 Update bug_report.md 2020-05-28 08:04:15 +02:00
pathmapper
85bb1d4d40 Update MB dependencies 2020-05-28 07:29:55 +02:00
Orange Mug
d95e25d185 Merge pull request #692 from orangemug/feature/codesandbox-ci
Added back in codesandbox CI config
2020-05-27 11:12:00 +01:00
Orange Mug
a88f2bc0a3 Merge pull request #691 from orangemug/fix/readme-styling
Fixed README styling
2020-05-27 11:11:46 +01:00
orangemug
5a4254d300 Added back in codesandbox CI config. 2020-05-27 10:45:52 +01:00
orangemug
6bfe2aa364 Reordered header items in README. 2020-05-27 10:37:01 +01:00
orangemug
0acd1fec0a Fixed README styling, updated logo and removed broken badges. 2020-05-27 10:32:03 +01:00
pathmapper
3046fedb55 Merge pull request #690 from pathmapper/other_issue_template
Add other issue template
2020-05-27 11:26:23 +02:00
Orange Mug
1574b49b01 Merge pull request #687 from orangemug/feature/github-actions-ci
Added new CI workflow using GitHub actions
2020-05-27 10:20:21 +01:00
pathmapper
4417a2d8f1 Add other issue template 2020-05-27 11:18:27 +02:00
pathmapper
1f34e927e7 Merge pull request #689 from pathmapper/master
Add issue template for bug report
2020-05-27 10:58:00 +02:00
pathmapper
9af6a537ef Update issue templates
Add custom bug report
2020-05-27 10:48:54 +02:00
Orange Mug
6e07142f13 Merge pull request #546 from nyurik/dockerfile
Optimize docker image
2020-05-25 09:41:15 +01:00
orangemug
d2853f34a4 Removed meta-demo-comment as it won't work from forked repos 2020-05-25 06:58:57 +01:00
orangemug
7faed0d27e Added firefox tests and fixed docker deploy 2020-05-24 12:56:52 +01:00
Orange Mug
22101f93ad Merge pull request #686 from orangemug/fix/issue-322
Added tip to JSON editor about how to unfocus
2020-05-24 12:04:35 +01:00
orangemug
0661899d54 Fixes for code review comments. 2020-05-24 11:46:47 +01:00
Orange Mug
862ac84464 Merge pull request #683 from orangemug/fix/a11y-issue-320
Fix some accessibility issues
2020-05-24 11:44:41 +01:00
Orange Mug
1e4aadbb6d Merge pull request #684 from orangemug/fix/issue-533
Fix for updating available sources cache when updating style
2020-05-24 11:42:15 +01:00
orangemug
ce731e7d6b Added new CI workflow using GitHub actions.
Also

 - Fixed screenshot tests
 - Fixed code coverage
 - Removed appveyor
 - Removed circleci
 - Updated wdio related dependencies
 - Added docker image deploy to the GitHub registry
2020-05-24 11:13:16 +01:00
orangemug
5448cdbe4e Don't remember state when toggling <AddModal/> 2020-05-21 10:07:22 +01:00
orangemug
315a9b82c0 Fixed for initial focus of JSON editor message. 2020-05-21 09:22:42 +01:00
orangemug
9e1c0e4c82 Added aria-hidden to JSON editor message. 2020-05-21 09:20:47 +01:00
orangemug
7db675e0d1 Added ESC note to JSON editor. 2020-05-19 12:21:12 +01:00
orangemug
0aa629164a Fix for updating available sources when updating style. 2020-05-19 10:24:59 +01:00
orangemug
c2ec77e869 Fixed lint errors. 2020-05-18 19:41:09 +01:00
orangemug
b28407a4a0 Accessibility fixes
- Aria landmarks
 - Title attributes to all icon only buttons
 - <Multibutton/> now internally a radio group
 - Replaced 1 'skip navigation link' with UI group links
 - Added map specific shortcuts to the shortcut menu
 - Hidden layer list actions from tab index
2020-05-18 19:37:49 +01:00
Orange Mug
e3e6647e03 Merge pull request #682 from orangemug/fix/issue-681
Fix open modal URL entry to use form submission
2020-05-14 15:32:09 +01:00
orangemug
eb0f833d49 Fixed typo. 2020-05-14 13:33:57 +01:00
orangemug
c5c1dd12b9 Added missing prop validation 2020-05-14 13:27:43 +01:00
orangemug
b7e414a042 Fix URL entry to use form submission and improved errors if protocol isn't present. 2020-05-14 12:13:47 +01:00
Orange Mug
81a6f31803 Merge pull request #680 from pathmapper/mb_styles
Fix token input
2020-05-14 11:49:33 +01:00
pathmapper
65cd050a18 Fix token input 2020-05-05 09:40:43 +02:00
Orange Mug
c426dd7349 Merge pull request #673 from pathmapper/update_mb_dependencies
Update MB deps
2020-05-02 09:33:10 +01:00
Orange Mug
c5af645546 Merge pull request #675 from nspringer-trimble/feature/inspect-source-name
Add source name to inspect popup
2020-05-02 09:32:07 +01:00
pathmapper
1bf0abfb5a Update MB deps 2020-04-30 09:18:13 +02:00
Nick Springer
18338de21a Add source name to inspect popup 2020-04-27 15:30:07 -04:00
Orange Mug
857117eb71 Merge pull request #666 from orangemug/fix/issue-646
Fix to make layer list header visible at all times
2020-04-26 10:15:33 +01:00
Orange Mug
8d86bca8b3 Merge pull request #671 from pathmapper/update_readme
Remove v1.7.0-beta from readme
2020-04-26 10:02:51 +01:00
orangemug
dc4e6a0925 Fix vertical align of view/select in toolbar 2020-04-26 09:58:21 +01:00
pathmapper
e9d6119ac6 Remove v1.7.0-beta from readme 2020-04-26 10:58:01 +02:00
orangemug
cbdf45c852 Fixed <select/> styling in firefox and improved in chrome. 2020-04-26 09:23:18 +01:00
Orange Mug
a191c36f96 Merge pull request #667 from orangemug/fix/issue-656
Support multiple tiles URLs for source
2020-04-25 13:52:30 +01:00
orangemug
0a8d0974ca Fixed field spec for image/video. 2020-04-25 13:31:15 +01:00
orangemug
8e6c54564b Add <DynamicArrayInput/> to source tile urls to support multiple values. 2020-04-25 11:38:13 +01:00
orangemug
4bbe2ce1ea Fix to make layer list header visible at all times.
Also improved scrollbar styling/positioning for toolbar and layers list.
2020-04-25 11:05:34 +01:00
orangemug
1d48ab7ecf 1.7.0 2020-04-23 21:12:38 +01:00
Orange Mug
d85ed36e70 Merge pull request #663 from nspringer-trimble/patch-1
Fix for desktop build
2020-04-22 20:30:00 +01:00
Nick Springer
b554f4427b Fix for desktop build 2020-04-21 09:17:30 -04:00
orangemug
184bfeeaf8 1.7.0-beta4 2020-04-20 13:13:36 +01:00
orangemug
e45f8d960d Added space for beta tag in logo/version header 2020-04-20 13:12:48 +01:00
Orange Mug
1fede3af3a Merge pull request #661 from orangemug/fix/issue-660-v2
Added JSON linting back into <SourceTypeEditor/>
2020-04-20 13:09:29 +01:00
orangemug
5ad74048bd Added JSON linting back into <SourceTypeEditor/> 2020-04-20 11:07:08 +01:00
orangemug
a0a91474de 1.7.0-beta3 2020-04-19 08:45:18 +01:00
Orange Mug
c3670701e5 Merge pull request #606 from orangemug/fix/issue-591
Added style formatting into the api store
2020-04-19 08:38:37 +01:00
Orange Mug
86923330d9 Merge pull request #658 from pathmapper/feature_id_inspect
Add feature id to FeaturePropertyPopup
2020-04-19 08:00:00 +01:00
pathmapper
4517148e5a Add underscore to label 2020-04-18 11:43:52 +02:00
pathmapper
0433d66f45 Add feature id to FeaturePropertyPopup 2020-04-18 11:25:54 +02:00
Orange Mug
0c592bacab Merge pull request #650 from orangemug/fix/issue-647
Fixed crash raised in issue #647
2020-04-14 21:15:59 +01:00
Orange Mug
d98637cb12 Merge pull request #645 from orangemug/feature/add-support-for-identity-functions
Add support for identity functions
2020-04-14 09:12:35 +01:00
orangemug
1070209cb5 Another attempt and maputnik inspect crashing issue. 2020-04-14 09:11:09 +01:00
orangemug
b6189f77c4 Added icons to buttons. 2020-04-14 08:31:55 +01:00
Orange Mug
25322a3952 Merge pull request #655 from orangemug/fix/issue-653
Added missing inline error for 'source' field
2020-04-13 10:57:01 +01:00
Orange Mug
5943c6f282 Merge pull request #654 from orangemug/fix/issue-649
Fixed default values for <FontInput/>
2020-04-13 10:56:47 +01:00
orangemug
090a26bb40 Added missing inline error for 'source' field. 2020-04-13 09:10:30 +01:00
orangemug
af03b010a4 Fixed default values for <FontInput/> 2020-04-13 08:53:33 +01:00
orangemug
578a920b6d Fixed crash raised in issue #647 2020-04-13 08:39:23 +01:00
pathmapper
0858a16ffc Merge pull request #644 from orangemug/fix/remove-heavy-thunderforest-tiles
Remove notes from thunderforest sources
2020-04-12 21:04:35 +02:00
orangemug
7cfe0563bc Added support for identity functions. 2020-04-12 16:25:32 +01:00
orangemug
ee72389534 Remove mentions of 'heavy' from thunderforest tiles. 2020-04-12 12:14:56 +01:00
pathmapper
8f722c59de Merge pull request #642 from pathmapper/upgrade_thunderforest
Upgrade Thunderforest tilesets from v1 to v2
2020-04-11 15:48:03 +02:00
pathmapper
94d2e958eb Add version to titles 2020-04-10 18:23:31 +02:00
pathmapper
d931c7cb38 Upgrade Thunderforest tilesets 2020-04-10 18:13:23 +02:00
orangemug
6da83c4670 1.7.0-beta2 2020-04-06 16:57:02 +01:00
Orange Mug
d26af16003 Merge pull request #639 from orangemug/fix/only-scroll-layer-list-if-item-not-in-view
Only scroll to selected item in <LayerList/> if not already in view.
2020-04-06 16:55:26 +01:00
Orange Mug
d75b86c927 Merge pull request #638 from orangemug/maintenance/update-deps-20200406
Update all deps
2020-04-06 16:54:06 +01:00
orangemug
a0cd087ccc Revert webdriverio version updates. 2020-04-06 15:47:12 +01:00
orangemug
313b639a5f Only scroll to selected item in <LayerList/> if not already in view. 2020-04-06 15:30:16 +01:00
orangemug
93c45d5340 Update all deps. 2020-04-06 15:14:21 +01:00
Orange Mug
3be6cb5926 Merge pull request #637 from orangemug/fix/console-errors-2020-04-06
Fix a bunch of errors/warnings from the console
2020-04-06 15:06:39 +01:00
Orange Mug
9d151fdc1f Merge pull request #636 from pathmapper/promote_beta
Promote v1.7.0-beta in readme
2020-04-06 14:32:29 +01:00
orangemug
44d1a7a6b0 {arrayMove} will no longer be included in 'react-sortable-hoc', move to array-move. 2020-04-06 14:18:41 +01:00
pathmapper
0e5676eae0 Promote v1.7.0-beta in readme 2020-04-06 15:14:42 +02:00
orangemug
b8739915b2 Lots of smaller fixes found in the console logs during testing. 2020-04-06 13:59:08 +01:00
Orange Mug
a1dedd1aa6 Merge pull request #634 from orangemug/fix/issue-630
Scroll selected <LayerListItem/> into view
2020-04-06 13:11:53 +01:00
Orange Mug
33b4a40c35 Merge pull request #635 from orangemug/fix/issue-633
Fixes for <NumberInput/>
2020-04-06 13:10:48 +01:00
orangemug
a624909819 Reset dirtyValue on resetValue 2020-04-06 10:40:30 +01:00
orangemug
d5d387f349 Removed placeholder on range (doesn't work) in favour 'default' value merged into 'value'. 2020-04-06 10:24:31 +01:00
orangemug
c58ae0f895 Fix <NumberInput/> to allow for decimal numbers. 2020-04-06 09:55:07 +01:00
orangemug
c9e360d675 Fix layer selection via <FeatureLayerPopup/> 2020-04-06 08:47:13 +01:00
orangemug
75ece350bd Merge remote-tracking branch 'upstream/master' into fix/issue-630 2020-04-04 15:48:52 +01:00
Orange Mug
45680151ef Merge pull request #632 from orangemug/fix/color-popup-swatch
Fix color in <FeatureLayerPopup/>
2020-04-04 15:48:35 +01:00
orangemug
87bae82b17 Merge remote-tracking branch 'upstream/master' into fix/issue-630 2020-04-04 15:44:29 +01:00
orangemug
fcad636f85 Scroll selected <LayerListItem/> into view 2020-04-04 15:41:35 +01:00
orangemug
bac8495b3c Made <FeatureLayerPopup/> swatch simplier because colors from features are already evaluated 2020-04-04 10:45:11 +01:00
orangemug
df98cb9c7b Fix layer color swatch in <FeatureLayerPopup/> 2020-04-04 10:21:58 +01:00
Orange Mug
34c3015b42 Merge pull request #625 from orangemug/fix/invalid-style-with-duplicate-layer-ids
Fix UI when loading invalid style with duplicate layer ids
2020-03-30 20:38:25 +01:00
orangemug
ca7bf9f4a7 Fixed lint errors. 2020-03-30 09:57:14 +01:00
orangemug
61ba399e1c Duplicate layer ids now show errors inline. 2020-03-30 09:47:46 +01:00
orangemug
b5c09a4f17 Merge remote-tracking branch 'upstream/master' into fix/invalid-style-with-duplicate-layer-ids 2020-03-30 08:52:37 +01:00
orangemug
6f83839a4c Added missing file. 2020-03-28 11:06:45 +00:00
orangemug
74b47e7e74 Fix UI when loading invalid style with duplicate layer ids.
Also fix for throwing error when 2 layers exist with empty strings as ids.
2020-03-28 10:58:47 +00:00
orangemug
6b45dc8b4d Added style formatting into apistore 2020-01-19 20:05:11 +00:00
Yuri Astrakhan
2a832955c4 Updated docs, rm creds 2019-08-03 15:02:16 -04:00
Yuri Astrakhan
608b836fe0 fix readme 2019-08-03 12:37:19 -04:00
Yuri Astrakhan
de9c4fcc4a Optimize docker image
* Use 2 stage docker building to produce a tiny python3-slim based docker image with just the compilation results.
2019-08-03 12:08:54 -04:00
212 changed files with 46478 additions and 12870 deletions

View File

@@ -6,7 +6,8 @@
"plugins": [ "plugins": [
"static-fs", "static-fs",
"react-hot-loader/babel", "react-hot-loader/babel",
"@babel/plugin-proposal-class-properties" "@babel/plugin-proposal-class-properties",
"@babel/transform-runtime"
], ],
"env": { "env": {
"test": { "test": {

View File

@@ -1,103 +0,0 @@
version: 2
templates:
# Test the build **only** no webdriver
build-steps: &build-steps
- checkout
- run:
name: "Create artifacts directory"
command: mkdir /tmp/artifacts
- restore_cache:
key: v1-dependencies-{{ arch }}-{{ checksum "package.json" }}
- run: npm install
- save_cache:
paths:
- node_modules
key: v1-dependencies-{{ arch }}-{{ checksum "package.json" }}
- run: mkdir -p /tmp/artifacts/logs
- run: npm run build
- run: npm run lint-js
- run: npm run lint-css
- store_artifacts:
path: /tmp/artifacts
destination: /artifacts
# Test in webdriver
wdio-steps: &wdio-steps
- checkout
- run:
name: "Create artifacts directory"
command: mkdir /tmp/artifacts
- restore_cache:
key: v1-dependencies-{{ arch }}-{{ checksum "package.json" }}
- run: npm install
- save_cache:
paths:
- node_modules
key: v1-dependencies-{{ arch }}-{{ checksum "package.json" }}
- run: mkdir -p /tmp/artifacts/logs
- run: npm run build
- run: npm run profiling-build
- run: npm run lint-js
- run: npm run lint-css
- run: DOCKER_HOST=localhost npm test
- run: ./node_modules/.bin/istanbul report --include /tmp/artifacts/coverage/coverage.json --dir /tmp/artifacts/coverage html lcov
- store_artifacts:
path: /tmp/artifacts
destination: /artifacts
jobs:
build-linux-node-v10:
docker:
- image: node:10
- image: selenium/standalone-chrome:3.141.59
working_directory: ~/repo-linux-node-v10
steps: *wdio-steps
build-linux-node-v12:
docker:
- image: node:12
working_directory: ~/repo-linux-node-v12
steps: *build-steps
build-linux-node-v13:
docker:
- image: node:13
working_directory: ~/repo-linux-node-v13
steps: *build-steps
build-osx-node-v10:
macos:
xcode: "9.0"
dependencies:
override:
- brew install node@10
working_directory: ~/repo-osx-node-v10
steps: *build-steps
build-osx-node-v12:
macos:
xcode: "9.0"
dependencies:
override:
- brew install node@12
working_directory: ~/repo-osx-node-v12
steps: *build-steps
build-osx-node-v13:
macos:
xcode: "9.0"
dependencies:
override:
- brew install node@13
working_directory: ~/repo-osx-node-v13
steps: *build-steps
workflows:
version: 2
build:
jobs:
- build-linux-node-v10
- build-linux-node-v12
- build-linux-node-v13
- build-osx-node-v10
- build-osx-node-v12
- build-osx-node-v13

4
.codesandbox/ci.json Normal file
View File

@@ -0,0 +1,4 @@
{
"packages": [],
"sandboxes": ["/"]
}

45
.dockerignore Normal file
View File

@@ -0,0 +1,45 @@
.git
.gitignore
Dockerfile
#
#
# COPIED FROM .gitignore , please keep it in sync
#
#
# Logs
logs
*.log
*.swp
*.swo
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules
# Ignore build files
public
/errorShots
/old
/build

27
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,27 @@
---
name: Bug report
about: Create a report to help us improve Maputnik
title: ''
labels: ''
assignees: ''
---
<!-- Thanks for your feedback! Please complete the following information: -->
**Maputnik version**:<!-- e.g v1.7.0, master -->
**Browser**:
**OS**:<!-- (Windows, macOS, Linux) -->
**Description of the bug**:
**Steps to reproduce the behavior**:
1.
2.
3.
**Style file or style URL**:
<!-- If applicable, attach a style file (zip) or provide a style URL. -->
**Screenshots**:
<!-- If applicable, add screenshots to help explain your problem. -->

11
.github/ISSUE_TEMPLATE/other-issue.md vendored Normal file
View File

@@ -0,0 +1,11 @@
---
name: Other issue
about: Feature request or other issue which is no bug report
title: ''
labels: ''
assignees: ''
---
<!-- Thanks for reaching out! If you are having general Maputnik mapping questions, please asking them at https://gis.stackexchange.com/ using the 'maputnik' tag https://gis.stackexchange.com/questions/tagged/maputnik and read https://gis.stackexchange.com/help/how-to-ask before you do so (please keep in mind that you're asking there in a general GIS forum, not a dedicated support channel) -->

193
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,193 @@
name: ci
on:
pull_request:
branches: [ master ]
push:
branches: [ master ]
jobs:
# post a comment linking to codesandbox with the current branch
# meta-demo-comment:
# name: meta/demo-comment
# runs-on: ubuntu-latest
# if: ${{ github.event_name == 'pull_request' }}
# steps:
# - uses: unsplash/comment-on-pr@v1.2.0
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# with:
# msg: "Demo: <https://codesandbox.io/embed/github/${{ github.repository }}/tree/${{ github.head_ref }}?view=preview>"
build-docker:
name: build/docker
runs-on: ${{ matrix.os }}
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v2
- run: docker build -t docker.pkg.github.com/maputnik/editor/editor:master .
# build the editor
build-node:
name: "build/node@${{ matrix.node-version }} (${{ matrix.os }})"
runs-on: ${{ matrix.os }}
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [16.x]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- uses: actions/cache@v1
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npm ci
- run: npm run build
build-artifacts:
name: "build/artifacts (${{ matrix.os }})"
runs-on: ${{ matrix.os }}
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
node-version: [16.x]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- uses: actions/cache@v1
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npm ci
- run: npm run build
- run: npm run build-storybook
- name: artifacts/editor
uses: actions/upload-artifact@v1
with:
name: editor
path: build/build
- run: npm run profiling-build
- name: artifacts/editor-profiling
uses: actions/upload-artifact@v1
with:
name: editor-profiling
path: build/profiling
- name: artifacts/storybook
uses: actions/upload-artifact@v1
with:
name: storybook
path: build/storybook
# Build and upload desktop CLI artifacts
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: ^1.19.x
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v2
with:
repository: maputnik/desktop
ref: master
path: ./src/github.com/maputnik/desktop/
- name: Make
run: cd src/github.com/maputnik/desktop/ && make
- name: Artifacts/linux
uses: actions/upload-artifact@v1
with:
name: maputnik-linux
path: ./src/github.com/maputnik/desktop/bin/linux/
- name: Artifacts/darwin
uses: actions/upload-artifact@v1
with:
name: maputnik-darwin
path: ./src/github.com/maputnik/desktop/bin/darwin/
- name: Artifacts/windows
uses: actions/upload-artifact@v1
with:
name: maputnik-windows
path: ./src/github.com/maputnik/desktop/bin/windows/
# build and test the editor
test_selenium_standalone:
name: "test/standalone-${{ matrix.browser }} (${{ matrix.os }})"
runs-on: ${{ matrix.os }}
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
node-version: [16]
browser: [chrome, firefox]
container:
image: node:${{ matrix.node-version }}
options: --network-alias testhost
services:
selenium:
# geckodriver-0.31 seems to have problems as of 2022 May 1
image: selenium/standalone-${{ matrix.browser == 'firefox' && 'firefox:99.0-geckodriver-0.30-20220427' || matrix.browser }}
ports:
- 4444:4444
options: --shm-size=2gb
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v1
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npm ci
- run: BROWSER=${{ matrix.browser }} TEST_NETWORK=testhost DOCKER_HOST=selenium npm run test
- if: ${{ matrix.browser == 'chrome' }}
run: ./node_modules/.bin/istanbul report --include build/coverage/coverage.json --dir build/coverage html lcov
- if: ${{ matrix.browser == 'chrome' }}
name: artifacts/coverage
uses: actions/upload-artifact@v1
with:
name: coverage
path: build/coverage
- name: artifacts/screenshots
uses: actions/upload-artifact@v1
with:
name: screenshots-${{ matrix.browser }}
path: build/screenshots

28
.github/workflows/deploy.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: deploy
on:
push:
branches: [ master ]
push:
tags:
- 'v*'
jobs:
# publish docker to github registry
deploy-docker:
name: deploy/docker
runs-on: ${{ matrix.os }}
if: ${{ github.event_name == 'push' }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v2
- run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u orangemug --password-stdin
- run: docker build -t docker.pkg.github.com/maputnik/editor/editor:master .
- run: docker push docker.pkg.github.com/maputnik/editor/editor:master

24
.storybook/main.js Normal file
View File

@@ -0,0 +1,24 @@
const rules = require('../config/webpack.rules');
module.exports = {
stories: ['../stories/**/*.stories.js'],
addons: [
'@storybook/addon-actions',
'@storybook/addon-links',
'@storybook/addon-a11y/register',
'@storybook/addon-storysource',
],
webpackFinal: async config => {
// do mutation to the config
console.log("config.module", config.module);
return {
...config,
module: {
rules: [
...rules,
]
}
};
},
};

7
.storybook/manager.js Normal file
View File

@@ -0,0 +1,7 @@
import { addons } from '@storybook/addons';
import { themes } from '@storybook/theming';
import theme from './maputnik.theme';
addons.setConfig({
theme: theme,
});

View File

@@ -0,0 +1,8 @@
import { create } from '@storybook/theming/create';
export default create({
base: 'light',
brandTitle: 'Maputnik',
brandUrl: 'https://github.com/maputnik/editor',
});

View File

@@ -1,21 +1,22 @@
FROM node:10-slim FROM node:10 as builder
WORKDIR /maputnik
RUN apt-get update && apt-get install -y --no-install-recommends \ # Only copy package.json to prevent npm install from running on every build
git \ COPY package.json package-lock.json ./
python \ RUN npm install
&& rm -rf /var/lib/apt/lists/*
EXPOSE 8888 # Build maputnik
# TODO: we should also do a npm run test here (needs more dependencies)
ENV HOME /maputnik COPY . .
RUN mkdir ${HOME}
COPY . ${HOME}/
WORKDIR ${HOME}
RUN npm install -d
RUN npm run build RUN npm run build
WORKDIR ${HOME}/build/build #---------------------------------------------------------------------------
CMD python -m SimpleHTTPServer 8888
# Create a clean python-based image with just the build results
FROM python:3-slim
WORKDIR /maputnik
COPY --from=builder /maputnik/build/build .
EXPOSE 8888
CMD python -m http.server 8888

View File

@@ -1,32 +1,30 @@
# Maputnik <img width="200" alt="Maputnik logo" src="https://cdn.jsdelivr.net/gh/maputnik/design/logos/logo-color.png" />
[![Build Status](https://circleci.com/gh/maputnik/editor/tree/master.svg?style=shield)][circleci] # Maputnik
[![Windows Build Status](https://ci.appveyor.com/api/projects/status/anelbgv6jdb3qnh9/branch/master?svg=true)][appveyor] [![GitHub CI status](https://github.com/maputnik/editor/workflows/ci/badge.svg)][github-action-ci]
[![Dependency Status](https://david-dm.org/maputnik/editor.svg)][dm-prod]
[![Dev Dependency Status](https://david-dm.org/maputnik/editor/dev-status.svg)][dm-dev]
[![License](https://img.shields.io/badge/license-MIT-blue.svg)][license] [![License](https://img.shields.io/badge/license-MIT-blue.svg)][license]
[circleci]: https://circleci.com/gh/maputnik/editor/tree/master [github-action-ci]: https://github.com/maputnik/editor/actions?query=workflow%3Aci
[appveyor]: https://ci.appveyor.com/project/lukasmartinelli/editor [license]: https://tldrlegal.com/license/mit-license
[dm-prod]: https://david-dm.org/maputnik/editor
[dm-dev]: https://david-dm.org/maputnik/editor?type=dev
[license]: https://tldrlegal.com/license/mit-license
<img width="200" align="right" alt="Maputnik" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/src/img/maputnik.png" />
A free and open visual editor for the [Mapbox GL styles](https://www.mapbox.com/mapbox-gl-style-spec/) A free and open visual editor for the [Mapbox GL styles](https://www.mapbox.com/mapbox-gl-style-spec/)
targeted at developers and map designers. targeted at developers and map designers.
## Usage
- :link: Design your maps online at **<https://maputnik.github.io/editor/>** (all in local storage) - :link: Design your maps online at **<https://maputnik.github.io/editor/>** (all in local storage)
- :link: Use the [Maputnik CLI](https://github.com/maputnik/editor/wiki/Maputnik-CLI) for local style development - :link: Use the [Maputnik CLI](https://github.com/maputnik/editor/wiki/Maputnik-CLI) for local style development
- In a Docker, run this command and browse to http://localhost:8888, Ctrl+C to stop the server.
Mapbox has built one of the best and most amazing OSS ecosystems. A key component to ensure its longevity and independence is an OSS map designer. ```bash
docker run -it --rm -p 8888:8888 maputnik/editor
```
## Donations ## Donations
Mapbox has built one of the best and most amazing OSS ecosystems. A key component to ensure its longevity and independence is an OSS map designer.
If you or your organisation has seen value from Maputnik, please consider donating at <https://maputnik.github.io/donate> If you or your organisation has seen value from Maputnik, please consider donating at <https://maputnik.github.io/donate>
## Documentation ## Documentation
The documentation can be found in the [Wiki](https://github.com/maputnik/editor/wiki). You are welcome to collaborate! The documentation can be found in the [Wiki](https://github.com/maputnik/editor/wiki). You are welcome to collaborate!
@@ -48,14 +46,14 @@ Install the deps, start the dev server and open the web browser on `http://local
# install dependencies # install dependencies
npm install npm install
# start dev server # start dev server
npm start npm run start
``` ```
If you want Maputnik to be accessible externally use the [`--host` option](https://webpack.js.org/configuration/dev-server/#devserverhost): If you want Maputnik to be accessible externally use the [`--host` option](https://webpack.js.org/configuration/dev-server/#devserverhost):
```bash ```bash
# start externally accessible dev server # start externally accessible dev server
npm start -- --host 0.0.0.0 npm run start -- --host 0.0.0.0
``` ```
The build process will watch for changes to the filesystem, rebuild and autoreload the editor. However note this from the [webpack-dev-server docs](https://webpack.js.org/configuration/dev-server/): The build process will watch for changes to the filesystem, rebuild and autoreload the editor. However note this from the [webpack-dev-server docs](https://webpack.js.org/configuration/dev-server/):
@@ -78,26 +76,18 @@ npm run lint-styles
## Tests ## Tests
For testing we use [webdriverio](http://webdriver.io) and [selenium-standalone](https://github.com/vvo/selenium-standalone) For testing we use [webdriverio](https://webdriver.io) and [selenium-standalone](https://github.com/webdriverio/selenium-standalone).
[selenium-standalone](https://github.com/vvo/selenium-standalone) starts a server that will launch browsers on your local machine. We use chrome so you **must** have chrome installed on your machine. [selenium-standalone](https://github.com/webdriverio/selenium-standalone) starts a server that will launch browsers on your local machine. You need to have Java installed on your machine as well as *chrome* or *firefox*.
Now open a terminal and run the following. This will install the drivers on your local machine Now open a terminal and run the following using *chrome*:
``` ```
./node_modules/.bin/selenium-standalone install npm run test
``` ```
or *firefox*:
Now start the standalone server
``` ```
./node_modules/.bin/selenium-standalone start BROWSER=firefox npm run test
```
Then open another terminal and run
```
npm test
``` ```
After some time you should see a browser launch which will be automated by the test runner. After some time you should see a browser launch which will be automated by the test runner.
@@ -105,7 +95,7 @@ After some time you should see a browser launch which will be automated by the t
## Related Projects ## Related Projects
- [maputnik-dev-server](https://github.com/nycplanning/labs-maputnik-dev-server) - An express.js server that allows for quickly loading the style from any mapboxGL map into mapuntnik. - [maputnik-dev-server](https://github.com/nycplanning/labs-maputnik-dev-server) - An express.js server that allows for quickly loading the style from any mapboxGL map into mapuntnik.
## Sponsors ## Sponsors

View File

@@ -1,26 +0,0 @@
image: Visual Studio 2019
environment:
matrix:
- nodejs_version: "10"
- nodejs_version: "12"
- nodejs_version: "13"
platform:
- x86
- x64
install:
# https://github.com/appveyor/ci/issues/2921#issuecomment-501016533
- ps: |
try {
Install-Product node $env:nodejs_version $env:platform
} catch {
echo "Unable to install node $env:nodejs_version, trying update..."
Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version) $env:platform
}
- md public
- npm install --global windows-build-tools
- npm install
build_script:
- npm run build
test_script:
- npm run lint-js
- npm run lint-css

View File

@@ -3,289 +3,45 @@ var WebpackDevServer = require("webpack-dev-server");
var webpackConfig = require("./webpack.config"); var webpackConfig = require("./webpack.config");
var testConfig = require("../test/config/specs"); var testConfig = require("../test/config/specs");
var artifacts = require("../test/artifacts"); var artifacts = require("../test/artifacts");
var isDocker = require("is-docker");
var server; var server;
var SCREENSHOT_PATH = artifacts.pathSync("screenshots"); var SCREENSHOT_PATH = artifacts.pathSync("screenshots");
exports.config = { exports.config = {
// runner: 'local',
// ==================== path: '/wd/hub',
// Runner Configuration specs: [
// ==================== './test/functional/index.js'
// ],
// WebdriverIO allows it to run your tests in arbitrary locations (e.g. locally or maxInstances: 10,
// on a remote machine). capabilities: [
runner: 'local', {
// maxInstances: 5,
// ================== browserName: (process.env.BROWSER || 'chrome'),
// Specify Test Files
// ==================
// Define which test specs should run. The pattern is relative to the directory
// from which `wdio` was called. Notice that, if you are calling `wdio` from an
// NPM script (see https://docs.npmjs.com/cli/run-script) then the current working
// directory is where your package.json resides, so `wdio` will be called from there.
//
specs: [
'./test/functional/index.js'
],
// Patterns to exclude.
exclude: [
// 'path/to/excluded/files'
],
//
// ============
// Capabilities
// ============
// Define your capabilities here. WebdriverIO can run multiple capabilities at the same
// time. Depending on the number of capabilities, WebdriverIO launches several test
// sessions. Within your capabilities you can overwrite the spec and exclude options in
// order to group specific specs to a specific capability.
//
// First, you can define how many instances should be started at the same time. Let's
// say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have
// set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec
// files and you set maxInstances to 10, all spec files will get tested at the same time
// and 30 processes will get spawned. The property handles how many capabilities
// from the same test should run tests.
//
maxInstances: 10,
//
// If you have trouble getting all important capabilities together, check out the
// Sauce Labs platform configurator - a great tool to configure your capabilities:
// https://docs.saucelabs.com/reference/platforms-configurator
//
capabilities: [{
// maxInstances can get overwritten per capability. So if you have an in-house Selenium
// grid with only 5 firefox instances available you can make sure that not more than
// 5 instances get started at a time.
maxInstances: 5,
//
browserName: 'chrome',
// If outputDir is provided WebdriverIO can capture driver session logs
// it is possible to configure which logTypes to include/exclude.
// excludeDriverLogs: ['*'], // pass '*' to exclude all driver session logs
// excludeDriverLogs: ['bugreport', 'server'],
}],
//
// ===================
// Test Configurations
// ===================
// Define all options that are relevant for the WebdriverIO instance here
//
// Level of logging verbosity: trace | debug | info | warn | error | silent
logLevel: 'info',
//
// Set specific log levels per logger
// loggers:
// - webdriver, webdriverio
// - @wdio/applitools-service, @wdio/browserstack-service, @wdio/devtools-service, @wdio/sauce-service
// - @wdio/mocha-framework, @wdio/jasmine-framework
// - @wdio/local-runner, @wdio/lambda-runner
// - @wdio/sumologic-reporter
// - @wdio/cli, @wdio/config, @wdio/sync, @wdio/utils
// Level of logging verbosity: trace | debug | info | warn | error | silent
// logLevels: {
// webdriver: 'debug',
// '@wdio/applitools-service': 'info'
// },
//
// If you only want to run your tests until a specific amount of tests have failed use
// bail (default is 0 - don't bail, run all tests).
bail: 0,
//
screenshotPath: SCREENSHOT_PATH,
// Note: This is here because @orangemug currently runs Maputnik inside a docker container.
hostname: process.env.DOCKER_HOST || "0.0.0.0",
// Set a base URL in order to shorten url command calls. If your `url` parameter starts
// with `/`, the base url gets prepended, not including the path portion of your baseUrl.
// If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url
// gets prepended directly.
baseUrl: 'http://localhost',
//
// Default timeout for all waitFor* commands.
waitforTimeout: 10000,
//
// Default timeout in milliseconds for request
// if Selenium Grid doesn't send response
connectionRetryTimeout: 90000,
//
// Default request retries count
connectionRetryCount: 3,
//
// Test runner services
// Services take over a specific job you don't want to take care of. They enhance
// your test setup with almost no effort. Unlike plugins, they don't add new
// commands. Instead, they hook themselves up into the test process.
//
// Framework you want to run your specs with.
// The following are supported: Mocha, Jasmine, and Cucumber
// see also: https://webdriver.io/docs/frameworks.html
//
// Make sure you have the wdio adapter package for the specific framework installed
// before running any tests.
framework: 'mocha',
//
// The number of times to retry the entire specfile when it fails as a whole
// specFileRetries: 1,
//
// Test reporter for stdout.
// The only one supported by default is 'dot'
// see also: https://webdriver.io/docs/dot-reporter.html
reporters: ['spec'],
//
// Options to be passed to Mocha.
// See the full list at http://mochajs.org/
mochaOpts: {
ui: 'bdd',
// Because we don't know how long the initial build will take...
timeout: 4*60*1000
},
onPrepare: function (config, capabilities) {
return new Promise(function(resolve, reject) {
var compiler = webpack(webpackConfig);
const serverHost = isDocker() ? "0.0.0.0" : "localhost";
server = new WebpackDevServer(compiler, {
host: serverHost,
stats: {
colors: true
}
});
server.listen(testConfig.port, serverHost, function(err) {
if(err) {
reject(err);
}
else {
resolve();
}
});
})
},
onComplete: function(exitCode) {
server.close()
} }
// ],
// ===== // geckodriver-0.31 seems to have problems as of 2022 May 1
// Hooks services: process.env.DOCKER_HOST ? [] : [ ['selenium-standalone', { drivers: { firefox: '0.30.0', chrome: 'latest' } } ] ],
// ===== logLevel: 'info',
// WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance bail: 0,
// it and to build services around it. You can either apply a single function or an array of screenshotPath: SCREENSHOT_PATH,
// methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got hostname: process.env.DOCKER_HOST || "0.0.0.0",
// resolved to continue. framework: 'mocha',
/** reporters: ['spec'],
* Gets executed once before all workers get launched. mochaOpts: {
* @param {Object} config wdio configuration object ui: 'bdd',
* @param {Array.<Object>} capabilities list of capabilities details // Because we don't know how long the initial build will take...
*/ timeout: 4*60*1000,
// onPrepare: function (config, capabilities) { },
// }, onPrepare: async function (config, capabilities) {
/** webpackConfig.devServer.host = testConfig.testNetwork;
* Gets executed just before initialising the webdriver session and test framework. It allows you webpackConfig.devServer.port = testConfig.port;
* to manipulate configurations depending on the capability or spec. const compiler = webpack(webpackConfig);
* @param {Object} config wdio configuration object server = new WebpackDevServer(webpackConfig.devServer, compiler);
* @param {Array.<Object>} capabilities list of capabilities details await server.start();
* @param {Array.<String>} specs List of spec file paths that are to be run },
*/ onComplete: async function (exitCode, config, capabilities) {
// beforeSession: function (config, capabilities, specs) { await server.stop();
// }, }
/**
* Gets executed before test execution begins. At this point you can access to all global
* variables like `browser`. It is the perfect place to define custom commands.
* @param {Array.<Object>} capabilities list of capabilities details
* @param {Array.<String>} specs List of spec file paths that are to be run
*/
// before: function (capabilities, specs) {
// },
/**
* Runs before a WebdriverIO command gets executed.
* @param {String} commandName hook command name
* @param {Array} args arguments that command would receive
*/
// beforeCommand: function (commandName, args) {
// },
/**
* Hook that gets executed before the suite starts
* @param {Object} suite suite details
*/
// beforeSuite: function (suite) {
// },
/**
* Function to be executed before a test (in Mocha/Jasmine) or a step (in Cucumber) starts.
* @param {Object} test test details
*/
// beforeTest: function (test) {
// },
/**
* Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling
* beforeEach in Mocha)
*/
// beforeHook: function () {
// },
/**
* Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling
* afterEach in Mocha)
*/
// afterHook: function () {
// },
/**
* Function to be executed after a test (in Mocha/Jasmine) or a step (in Cucumber) starts.
* @param {Object} test test details
*/
// afterTest: function (test) {
// },
/**
* Hook that gets executed after the suite has ended
* @param {Object} suite suite details
*/
// afterSuite: function (suite) {
// },
/**
* Runs after a WebdriverIO command gets executed
* @param {String} commandName hook command name
* @param {Array} args arguments that command would receive
* @param {Number} result 0 - command success, 1 - command error
* @param {Object} error error object if any
*/
// afterCommand: function (commandName, args, result, error) {
// },
/**
* Gets executed after all tests are done. You still have access to all global variables from
* the test.
* @param {Number} result 0 - test pass, 1 - test fail
* @param {Array.<Object>} capabilities list of capabilities details
* @param {Array.<String>} specs List of spec file paths that ran
*/
// after: function (result, capabilities, specs) {
// },
/**
* Gets executed right after terminating the webdriver session.
* @param {Object} config wdio configuration object
* @param {Array.<Object>} capabilities list of capabilities details
* @param {Array.<String>} specs List of spec file paths that ran
*/
// afterSession: function (config, capabilities, specs) {
// },
/**
* Gets executed after all workers got shut down and the process is about to exit. An error
* thrown in the onComplete hook will result in the test run failing.
* @param {Object} exitCode 0 - success, 1 - fail
* @param {Object} config wdio configuration object
* @param {Array.<Object>} capabilities list of capabilities details
* @param {<Object>} results object containing test results
*/
// onComplete: function(exitCode, config, capabilities, results) {
// },
/**
* Gets executed when a refresh happens.
* @param {String} oldSessionId session ID of the old session
* @param {String} newSessionId session ID of the new session
*/
//onReload: function(oldSessionId, newSessionId) {
//}
} }

View File

@@ -1,5 +1,4 @@
"use strict"; "use strict";
var webpack = require('webpack');
var path = require('path'); var path = require('path');
var rules = require('./webpack.rules'); var rules = require('./webpack.rules');
var HtmlWebpackPlugin = require('html-webpack-plugin'); var HtmlWebpackPlugin = require('html-webpack-plugin');
@@ -37,27 +36,25 @@ module.exports = {
tls: 'empty' tls: 'empty'
}, },
devServer: { devServer: {
contentBase: "./public",
// do not print bundle build stats
noInfo: true,
// enable HMR // enable HMR
hot: true, hot: true,
// embed the webpack-dev-server runtime into the bundle
inline: true,
// serve index.html in place of 404 responses to allow HTML5 history // serve index.html in place of 404 responses to allow HTML5 history
historyApiFallback: true, historyApiFallback: true,
port: PORT, port: PORT,
host: HOST, host: HOST,
watchOptions: { watchFiles: {
// Disabled polling by default as it causes lots of CPU usage and hence drains laptop batteries. To enable polling add WEBPACK_DEV_SERVER_POLLING to your environment options: {
// See <https://webpack.js.org/configuration/watch/#watchoptions-poll> for details // Disabled polling by default as it causes lots of CPU usage and hence drains laptop batteries. To enable polling add WEBPACK_DEV_SERVER_POLLING to your environment
poll: (!!process.env.WEBPACK_DEV_SERVER_POLLING ? true : false), // See <https://webpack.js.org/configuration/watch/#watchoptions-poll> for details
watch: false usePolling: (!!process.env.WEBPACK_DEV_SERVER_POLLING ? true : false),
watch: false
}
} }
}, },
optimization: {
noEmitOnErrors: true,
},
plugins: [ plugins: [
new webpack.NoEmitOnErrorsPlugin(),
new webpack.HotModuleReplacementPlugin(),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
title: 'Maputnik', title: 'Maputnik',
template: './src/template.html' template: './src/template.html'
@@ -65,11 +62,13 @@ module.exports = {
new HtmlWebpackInlineSVGPlugin({ new HtmlWebpackInlineSVGPlugin({
runPreEmit: true, runPreEmit: true,
}), }),
new CopyWebpackPlugin([ new CopyWebpackPlugin({
{ patterns: [
from: './src/manifest.json', {
to: 'manifest.json' from: './src/manifest.json',
} to: 'manifest.json'
]) }
]
})
] ]
}; };

View File

@@ -48,12 +48,14 @@ module.exports = {
new HtmlWebpackInlineSVGPlugin({ new HtmlWebpackInlineSVGPlugin({
runPreEmit: true, runPreEmit: true,
}), }),
new CopyWebpackPlugin([ new CopyWebpackPlugin({
{ patterns: [
from: './src/manifest.json', {
to: 'manifest.json' from: './src/manifest.json',
} to: 'manifest.json'
]), }
]
}),
new BundleAnalyzerPlugin({ new BundleAnalyzerPlugin({
analyzerMode: 'static', analyzerMode: 'static',
defaultSizes: 'gzip', defaultSizes: 'gzip',

49416
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,22 @@
{ {
"name": "maputnik", "name": "maputnik",
"version": "1.7.0-beta", "version": "2.0.0-pre.1",
"description": "A MapboxGL visual style editor", "description": "A MapLibre GL visual style editor",
"main": "''", "main": "''",
"scripts": { "scripts": {
"stats": "webpack --config config/webpack.production.config.js --profile --json > stats.json", "stats": "webpack --config config/webpack.production.config.js --progress=profile --json > stats.json",
"build": "webpack --config config/webpack.production.config.js --progress --profile --colors", "build": "webpack --config config/webpack.production.config.js --progress=profile --color",
"profiling-build": "webpack --config config/webpack.profiling.config.js --progress --profile --colors", "profiling-build": "webpack --config config/webpack.profiling.config.js --progress=profile --color",
"test": "cross-env NODE_ENV=test wdio config/wdio.conf.js", "test": "cross-env NODE_ENV=test wdio config/wdio.conf.js",
"test-watch": "cross-env NODE_ENV=test wdio config/wdio.conf.js --watch", "test-watch": "cross-env NODE_ENV=test wdio config/wdio.conf.js --watch",
"start": "webpack-dev-server --progress --profile --colors --config config/webpack.config.js", "start": "webpack-dev-server --progress=profile --color --config config/webpack.config.js",
"start-prod": "webpack-dev-server --progress=profile --color --config config/webpack.production.config.js",
"start-sandbox": "webpack-dev-server --disable-host-check --host 0.0.0.0 --progress=profile --color --config config/webpack.production.config.js",
"lint-js": "eslint --ext js --ext jsx src test", "lint-js": "eslint --ext js --ext jsx src test",
"lint-css": "stylelint \"src/styles/*.scss\"", "lint-css": "stylelint \"src/styles/*.scss\"",
"lint": "npm run lint-js && npm run lint-css" "lint": "npm run lint-js && npm run lint-css",
"storybook": "start-storybook -h 0.0.0.0 -p 6006",
"build-storybook": "build-storybook -o build/storybook"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -22,46 +26,50 @@
"license": "MIT", "license": "MIT",
"homepage": "https://github.com/maputnik/editor#readme", "homepage": "https://github.com/maputnik/editor#readme",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.8.4", "@babel/runtime": "^7.17.9",
"@mapbox/mapbox-gl-rtl-text": "^0.2.3", "@mapbox/mapbox-gl-rtl-text": "^0.2.3",
"@mapbox/mapbox-gl-style-spec": "^13.12.0", "@maplibre/maplibre-gl-style-spec": "^17.0.1",
"@mdi/react": "^1.3.0", "@mdi/react": "^1.5.0",
"classnames": "^2.2.6", "array-move": "^4.0.0",
"codemirror": "^5.52.0", "buffer": "^6.0.3",
"color": "^3.1.2", "classnames": "^2.3.1",
"detect-browser": "^5.0.0", "codemirror": "^5.65.2",
"file-saver": "^2.0.2", "color": "^4.2.3",
"detect-browser": "^5.3.0",
"file-saver": "^2.0.5",
"json-stringify-pretty-compact": "^3.0.0",
"json-to-ast": "^2.1.0", "json-to-ast": "^2.1.0",
"jsonlint": "github:josdejong/jsonlint#85a19d7", "jsonlint": "github:josdejong/jsonlint#85a19d7",
"lodash": "^4.17.15", "lodash": "^4.17.21",
"lodash.capitalize": "^4.2.1", "lodash.capitalize": "^4.2.1",
"lodash.clamp": "^4.0.3", "lodash.clamp": "^4.0.3",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"mapbox-gl": "^1.9.0",
"mapbox-gl-inspect": "^1.3.1", "mapbox-gl-inspect": "^1.3.1",
"maputnik-design": "github:maputnik/design#f7a2b4d", "maplibre-gl": "^2.4.0",
"ol": "^6.2.1", "maputnik-design": "github:maputnik/design#172b06c",
"ol-mapbox-style": "^6.0.1", "ol": "^6.14.1",
"prop-types": "^15.7.2", "ol-mapbox-style": "^7.1.1",
"react": "^16.12.0", "prop-types": "^15.8.1",
"react-accessible-accordion": "^3.0.1", "react": "^16.0.0",
"react-aria-menubutton": "^6.3.0", "react-accessible-accordion": "^4.0.0",
"react-aria-modal": "^4.0.0", "react-aria-menubutton": "^7.0.3",
"react-aria-modal": "^4.0.1",
"react-autobind": "^1.0.6", "react-autobind": "^1.0.6",
"react-autocomplete": "^1.8.1", "react-autocomplete": "^1.8.1",
"react-collapse": "^5.0.1", "react-collapse": "^5.1.1",
"react-color": "^2.18.0", "react-color": "^2.19.3",
"react-dom": "^16.12.0", "react-dom": "^16.0.0",
"react-file-reader-input": "^2.0.0", "react-file-reader-input": "^2.0.0",
"react-icon-base": "^2.1.2", "react-icon-base": "^2.1.2",
"react-icons": "^3.9.0", "react-icons": "^4.3.1",
"react-motion": "^0.5.2", "react-sortable-hoc": "^2.0.0",
"react-sortable-hoc": "^1.11.0",
"reconnecting-websocket": "^4.4.0", "reconnecting-websocket": "^4.4.0",
"slugify": "^1.3.6", "sass": "^1.50.0",
"slugify": "^1.6.5",
"string-hash": "^1.1.3",
"url": "^0.11.0" "url": "^0.11.0"
}, },
"jshintConfig": { "jshintConfig": {
@@ -93,7 +101,7 @@
"node": true, "node": true,
"es6": true "es6": true
}, },
"parser": "babel-eslint", "parser": "@babel/eslint-parser",
"parserOptions": { "parserOptions": {
"ecmaVersion": 6, "ecmaVersion": 6,
"sourceType": "module", "sourceType": "module",
@@ -102,57 +110,66 @@
"experimentalObjectRestSpread": true, "experimentalObjectRestSpread": true,
"jsx": true "jsx": true
} }
},
"settings": {
"react": {
"version": "detect"
}
} }
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.8.4", "@babel/core": "^7.17.9",
"@babel/plugin-proposal-class-properties": "^7.5.5", "@babel/eslint-parser": "^7.19.1",
"@babel/plugin-transform-runtime": "^7.6.2", "@babel/plugin-proposal-class-properties": "^7.16.7",
"@babel/preset-env": "^7.6.3", "@babel/plugin-transform-runtime": "^7.17.0",
"@babel/preset-flow": "^7.0.0", "@babel/preset-env": "^7.16.11",
"@babel/preset-react": "^7.6.3", "@babel/preset-flow": "^7.16.7",
"@mdi/js": "^5.0.45", "@babel/preset-react": "^7.16.7",
"@wdio/cli": "^6.0.5", "@mdi/js": "^6.6.96",
"@wdio/local-runner": "^6.0.5", "@storybook/addon-a11y": "^6.4.20",
"@wdio/mocha-framework": "^6.0.4", "@storybook/addon-actions": "^6.4.20",
"@wdio/selenium-standalone-service": "^6.0.4", "@storybook/addon-links": "^6.4.20",
"@wdio/spec-reporter": "^6.0.4", "@storybook/addon-storysource": "^6.4.20",
"@wdio/sync": "^6.0.1", "@storybook/addons": "^6.4.20",
"babel-eslint": "^10.0.3", "@storybook/react": "^6.4.20",
"babel-loader": "^8.1.0", "@storybook/theming": "^6.4.20",
"babel-plugin-istanbul": "^6.0.0", "@wdio/cli": "^7.19.3",
"@wdio/local-runner": "^7.19.3",
"@wdio/mocha-framework": "^7.19.3",
"@wdio/selenium-standalone-service": "^7.19.1",
"@wdio/spec-reporter": "^7.19.1",
"babel-loader": "^8.2.4",
"babel-plugin-istanbul": "^6.1.1",
"babel-plugin-static-fs": "^3.0.0", "babel-plugin-static-fs": "^3.0.0",
"copy-webpack-plugin": "^5.1.1", "copy-webpack-plugin": "^6.4.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"cross-env": "^7.0.0", "cross-env": "^7.0.3",
"css-loader": "^3.4.2", "css-loader": "^5.2.7",
"eslint": "^6.8.0", "eslint": "^8.12.0",
"eslint-plugin-react": "^7.18.3", "eslint-plugin-react": "^7.29.4",
"express": "^4.17.1", "express": "^4.17.3",
"file-loader": "^6.0.0", "html-webpack-inline-svg-plugin": "^2.3.0",
"html-webpack-inline-svg-plugin": "^1.3.0", "html-webpack-plugin": "^4.5.2",
"html-webpack-plugin": "^3.2.0",
"is-docker": "^2.0.0",
"istanbul": "^0.4.5", "istanbul": "^0.4.5",
"istanbul-lib-coverage": "^3.0.0", "istanbul-lib-coverage": "^3.2.0",
"mkdirp": "^1.0.3", "mkdirp": "^1.0.4",
"mocha": "^7.0.1", "mocha": "^9.2.2",
"node-sass": "^4.13.1", "postcss": "^8.4.12",
"react-hot-loader": "^4.12.19", "react-hot-loader": "^4.13.0",
"sass-loader": "^8.0.2", "sass-loader": "^10.2.1",
"selenium-standalone": "^6.17.0", "style-loader": "^2.0.0",
"style-loader": "^1.1.3", "stylelint": "^14.6.1",
"stylelint": "^13.2.0", "stylelint-config-recommended-scss": "^6.0.0",
"stylelint-config-recommended-scss": "^4.2.0", "stylelint-scss": "^4.2.0",
"stylelint-scss": "^3.14.2",
"svg-inline-loader": "^0.8.2", "svg-inline-loader": "^0.8.2",
"transform-loader": "^0.2.4", "transform-loader": "^0.2.4",
"uuid": "^7.0.2", "typescript": "^4.6.3",
"webdriverio": "^6.0.5", "uuid": "^8.3.2",
"webpack": "^4.41.6", "webdriverio": "^7.19.3",
"webpack-bundle-analyzer": "^3.6.0", "webpack": "^4.46.0",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cleanup-plugin": "^0.5.1", "webpack-cleanup-plugin": "^0.5.1",
"webpack-cli": "^3.3.11", "webpack-cli": "^4.9.2",
"webpack-dev-server": "^3.10.3" "webpack-dev-server": "^4.8.1"
} }
} }

5
sandbox.config.json Normal file
View File

@@ -0,0 +1,5 @@
{
"container": {
"startScript": "start-sandbox"
}
}

View File

@@ -2,29 +2,31 @@ import autoBind from 'react-autobind';
import React from 'react' import React from 'react'
import cloneDeep from 'lodash.clonedeep' import cloneDeep from 'lodash.clonedeep'
import clamp from 'lodash.clamp' import clamp from 'lodash.clamp'
import buffer from 'buffer'
import get from 'lodash.get' import get from 'lodash.get'
import {unset} from 'lodash' import {unset} from 'lodash'
import {arrayMove} from 'react-sortable-hoc' import {arrayMoveMutable} from 'array-move'
import url from 'url' import url from 'url'
import hash from "string-hash";
import MapboxGlMap from './map/MapboxGlMap' import MapMapboxGl from './MapMapboxGl'
import OpenLayersMap from './map/OpenLayersMap' import MapOpenLayers from './MapOpenLayers'
import LayerList from './layers/LayerList' import LayerList from './LayerList'
import LayerEditor from './layers/LayerEditor' import LayerEditor from './LayerEditor'
import Toolbar from './Toolbar' import AppToolbar from './AppToolbar'
import AppLayout from './AppLayout' import AppLayout from './AppLayout'
import MessagePanel from './MessagePanel' import MessagePanel from './AppMessagePanel'
import SettingsModal from './modals/SettingsModal' import ModalSettings from './ModalSettings'
import ExportModal from './modals/ExportModal' import ModalExport from './ModalExport'
import SourcesModal from './modals/SourcesModal' import ModalSources from './ModalSources'
import OpenModal from './modals/OpenModal' import ModalOpen from './ModalOpen'
import ShortcutsModal from './modals/ShortcutsModal' import ModalShortcuts from './ModalShortcuts'
import SurveyModal from './modals/SurveyModal' import ModalSurvey from './ModalSurvey'
import DebugModal from './modals/DebugModal' import ModalDebug from './ModalDebug'
import { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata' import { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata'
import {latest, validate} from '@mapbox/mapbox-gl-style-spec' import {latest, validate} from '@maplibre/maplibre-gl-style-spec'
import style from '../libs/style' import style from '../libs/style'
import { initialStyleUrl, loadStyleUrl, removeStyleQuerystring } from '../libs/urlopen' import { initialStyleUrl, loadStyleUrl, removeStyleQuerystring } from '../libs/urlopen'
import { undoMessages, redoMessages } from '../libs/diffmessage' import { undoMessages, redoMessages } from '../libs/diffmessage'
@@ -35,22 +37,10 @@ import LayerWatcher from '../libs/layerwatcher'
import tokens from '../config/tokens.json' import tokens from '../config/tokens.json'
import isEqual from 'lodash.isequal' import isEqual from 'lodash.isequal'
import Debug from '../libs/debug' import Debug from '../libs/debug'
import queryUtil from '../libs/query-util' import {formatLayerId} from '../util/format';
import MapboxGl from 'mapbox-gl' // Buffer must be defined globally for @maplibre/maplibre-gl-style-spec validate() function to succeed.
window.Buffer = buffer.Buffer;
// Similar functionality as <https://github.com/mapbox/mapbox-gl-js/blob/7e30aadf5177486c2cfa14fe1790c60e217b5e56/src/util/mapbox.js>
function normalizeSourceURL (url, apiToken="") {
const matches = url.match(/^mapbox:\/\/(.*)/);
if (matches) {
// mapbox://mapbox.mapbox-streets-v7
return `https://api.mapbox.com/v4/${matches[1]}.json?secure&access_token=${apiToken}`
}
else {
return url;
}
}
function setFetchAccessToken(url, mapStyle) { function setFetchAccessToken(url, mapStyle) {
const matchesTilehosting = url.match(/\.tilehosting\.com/); const matchesTilehosting = url.match(/\.tilehosting\.com/);
@@ -188,7 +178,7 @@ export default class App extends React.Component {
console.log('Falling back to local storage for storing styles') console.log('Falling back to local storage for storing styles')
this.styleStore = new StyleStore() this.styleStore = new StyleStore()
} }
this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle)) this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle, {initialLoad: true}))
if(Debug.enabled()) { if(Debug.enabled()) {
Debug.set("maputnik", "styleStore", this.styleStore); Debug.set("maputnik", "styleStore", this.styleStore);
@@ -321,11 +311,68 @@ export default class App extends React.Component {
opts = { opts = {
save: true, save: true,
addRevision: true, addRevision: true,
initialLoad: false,
...opts, ...opts,
}; };
if (opts.initialLoad) {
this.getInitialStateFromUrl(newStyle);
}
const errors = validate(newStyle, latest) || []; const errors = validate(newStyle, latest) || [];
const mappedErrors = errors.map(error => {
// The validate function doesn't give us errors for duplicate error with
// empty string for layer.id, manually deal with that here.
const layerErrors = [];
if (newStyle && newStyle.layers) {
const foundLayers = new Map();
newStyle.layers.forEach((layer, index) => {
if (layer.id === "" && foundLayers.has(layer.id)) {
const message = `Duplicate layer: ${formatLayerId(layer.id)}`;
const error = new Error(
`layers[${index}]: duplicate layer id [empty_string], previously used`
);
layerErrors.push(error);
}
foundLayers.set(layer.id, true);
});
}
const mappedErrors = layerErrors.concat(errors).map(error => {
// Special case: Duplicate layer id
const dupMatch = error.message.match(/layers\[(\d+)\]: (duplicate layer id "?(.*)"?, previously used)/);
if (dupMatch) {
const [matchStr, index, message] = dupMatch;
return {
message: error.message,
parsed: {
type: "layer",
data: {
index: parseInt(index, 10),
key: "id",
message,
}
}
}
}
// Special case: Invalid source
const invalidSourceMatch = error.message.match(/layers\[(\d+)\]: (source "(?:.*)" not found)/);
if (invalidSourceMatch) {
const [matchStr, index, message] = invalidSourceMatch;
return {
message: error.message,
parsed: {
type: "layer",
data: {
index: parseInt(index, 10),
key: "source",
message,
}
}
}
}
const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/); const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/);
if (layerMatch) { if (layerMatch) {
const [matchStr, index, group, property, message] = layerMatch; const [matchStr, index, group, property, message] = layerMatch;
@@ -335,7 +382,7 @@ export default class App extends React.Component {
parsed: { parsed: {
type: "layer", type: "layer",
data: { data: {
index, index: parseInt(index, 10),
key, key,
message message
} }
@@ -347,7 +394,7 @@ export default class App extends React.Component {
message: error.message, message: error.message,
}; };
} }
}) });
let dirtyMapStyle = undefined; let dirtyMapStyle = undefined;
if (errors.length > 0) { if (errors.length > 0) {
@@ -382,14 +429,16 @@ export default class App extends React.Component {
if (opts.save) { if (opts.save) {
this.saveStyle(newStyle); this.saveStyle(newStyle);
} }
this.setState({ this.setState({
mapStyle: newStyle, mapStyle: newStyle,
dirtyMapStyle: dirtyMapStyle, dirtyMapStyle: dirtyMapStyle,
errors: mappedErrors, errors: mappedErrors,
}, () => {
this.fetchSources();
this.setStateInUrl();
}) })
this.fetchSources();
} }
onUndo = () => { onUndo = () => {
@@ -425,7 +474,7 @@ export default class App extends React.Component {
} }
layers = layers.slice(0); layers = layers.slice(0);
layers = arrayMove(layers, oldIndex, newIndex); arrayMoveMutable(layers, oldIndex, newIndex);
this.onLayersChange(layers); this.onLayersChange(layers);
} }
@@ -437,56 +486,50 @@ export default class App extends React.Component {
this.onStyleChanged(changedStyle) this.onStyleChanged(changedStyle)
} }
onLayerDestroy = (layerId) => { onLayerDestroy = (index) => {
let layers = this.state.mapStyle.layers; let layers = this.state.mapStyle.layers;
const remainingLayers = layers.slice(0); const remainingLayers = layers.slice(0);
const idx = style.indexOfLayer(remainingLayers, layerId) remainingLayers.splice(index, 1);
remainingLayers.splice(idx, 1);
this.onLayersChange(remainingLayers); this.onLayersChange(remainingLayers);
} }
onLayerCopy = (layerId) => { onLayerCopy = (index) => {
let layers = this.state.mapStyle.layers; let layers = this.state.mapStyle.layers;
const changedLayers = layers.slice(0) const changedLayers = layers.slice(0)
const idx = style.indexOfLayer(changedLayers, layerId)
const clonedLayer = cloneDeep(changedLayers[idx]) const clonedLayer = cloneDeep(changedLayers[index])
clonedLayer.id = clonedLayer.id + "-copy" clonedLayer.id = clonedLayer.id + "-copy"
changedLayers.splice(idx, 0, clonedLayer) changedLayers.splice(index, 0, clonedLayer)
this.onLayersChange(changedLayers) this.onLayersChange(changedLayers)
} }
onLayerVisibilityToggle = (layerId) => { onLayerVisibilityToggle = (index) => {
let layers = this.state.mapStyle.layers; let layers = this.state.mapStyle.layers;
const changedLayers = layers.slice(0) const changedLayers = layers.slice(0)
const idx = style.indexOfLayer(changedLayers, layerId)
const layer = { ...changedLayers[idx] } const layer = { ...changedLayers[index] }
const changedLayout = 'layout' in layer ? {...layer.layout} : {} const changedLayout = 'layout' in layer ? {...layer.layout} : {}
changedLayout.visibility = changedLayout.visibility === 'none' ? 'visible' : 'none' changedLayout.visibility = changedLayout.visibility === 'none' ? 'visible' : 'none'
layer.layout = changedLayout layer.layout = changedLayout
changedLayers[idx] = layer changedLayers[index] = layer
this.onLayersChange(changedLayers) this.onLayersChange(changedLayers)
} }
onLayerIdChange = (oldId, newId) => { onLayerIdChange = (index, oldId, newId) => {
const changedLayers = this.state.mapStyle.layers.slice(0) const changedLayers = this.state.mapStyle.layers.slice(0)
const idx = style.indexOfLayer(changedLayers, oldId) changedLayers[index] = {
...changedLayers[index],
changedLayers[idx] = {
...changedLayers[idx],
id: newId id: newId
} }
this.onLayersChange(changedLayers) this.onLayersChange(changedLayers)
} }
onLayerChanged = (layer) => { onLayerChanged = (index, layer) => {
const changedLayers = this.state.mapStyle.layers.slice(0) const changedLayers = this.state.mapStyle.layers.slice(0)
const idx = style.indexOfLayer(changedLayers, layer.id) changedLayers[index] = layer
changedLayers[idx] = layer
this.onLayersChange(changedLayers) this.onLayersChange(changedLayers)
} }
@@ -494,7 +537,7 @@ export default class App extends React.Component {
setMapState = (newState) => { setMapState = (newState) => {
this.setState({ this.setState({
mapState: newState mapState: newState
}) }, this.setStateInUrl);
} }
setDefaultValues = (styleObj) => { setDefaultValues = (styleObj) => {
@@ -519,25 +562,20 @@ export default class App extends React.Component {
} }
fetchSources() { fetchSources() {
const sourceList = {...this.state.sources}; const sourceList = {};
for(let [key, val] of Object.entries(this.state.mapStyle.sources)) { for(let [key, val] of Object.entries(this.state.mapStyle.sources)) {
if(sourceList.hasOwnProperty(key)) { if(
continue; !this.state.sources.hasOwnProperty(key) &&
} val.type === "vector" &&
val.hasOwnProperty("url")
) {
sourceList[key] = {
type: val.type,
layers: []
};
sourceList[key] = {
type: val.type,
layers: []
};
if(!this.state.sources.hasOwnProperty(key) && val.type === "vector" && val.hasOwnProperty("url")) {
let url = val.url; let url = val.url;
try {
url = normalizeSourceURL(url, MapboxGl.accessToken);
} catch(err) {
console.warn("Failed to normalizeSourceURL: ", err);
}
try { try {
url = setFetchAccessToken(url, this.state.mapStyle) url = setFetchAccessToken(url, this.state.mapStyle)
@@ -548,29 +586,33 @@ export default class App extends React.Component {
fetch(url, { fetch(url, {
mode: 'cors', mode: 'cors',
}) })
.then((response) => { .then(response => response.json())
return response.json(); .then(json => {
})
.then((json) => {
if(!json.hasOwnProperty("vector_layers")) {
return;
}
// Create new objects before setState if(!json.hasOwnProperty("vector_layers")) {
const sources = Object.assign({}, this.state.sources); return;
}
for(let layer of json.vector_layers) { // Create new objects before setState
sources[key].layers.push(layer.id) const sources = Object.assign({}, {
} [key]: this.state.sources[key],
});
console.debug("Updating source: "+key); for(let layer of json.vector_layers) {
this.setState({ sources[key].layers.push(layer.id)
sources: sources }
});
}) console.debug("Updating source: "+key);
.catch((err) => { this.setState({
console.error("Failed to process sources for '%s'", url, err); sources: sources
}) });
})
.catch(err => {
console.error("Failed to process sources for '%s'", url, err);
});
}
else {
sourceList[key] = this.state.sources[key] || this.state.mapStyle.sources[key];
} }
} }
@@ -616,14 +658,14 @@ export default class App extends React.Component {
// Check if OL code has been loaded? // Check if OL code has been loaded?
if(renderer === 'ol') { if(renderer === 'ol') {
mapElement = <OpenLayersMap mapElement = <MapOpenLayers
{...mapProps} {...mapProps}
onChange={this.onMapChange} onChange={this.onMapChange}
debugToolbox={this.state.openlayersDebugOptions.debugToolbox} debugToolbox={this.state.openlayersDebugOptions.debugToolbox}
onLayerSelect={this.onLayerSelect} onLayerSelect={this.onLayerSelect}
/> />
} else { } else {
mapElement = <MapboxGlMap {...mapProps} mapElement = <MapMapboxGl {...mapProps}
onChange={this.onMapChange} onChange={this.onMapChange}
options={this.state.mapboxGlDebugOptions} options={this.state.mapboxGlDebugOptions}
inspectModeEnabled={this.state.mapState === "inspect"} inspectModeEnabled={this.state.mapState === "inspect"}
@@ -638,16 +680,98 @@ export default class App extends React.Component {
const elementStyle = {}; const elementStyle = {};
if (filterName) { if (filterName) {
elementStyle.filter = `url('#${filterName}')`; elementStyle.filter = `url('#${filterName}')`;
}; }
return <div style={elementStyle} className="maputnik-map__container"> return <div style={elementStyle} className="maputnik-map__container">
{mapElement} {mapElement}
</div> </div>
} }
onLayerSelect = (layerId) => { setStateInUrl = () => {
const idx = style.indexOfLayer(this.state.mapStyle.layers, layerId) const {mapState, mapStyle, isOpen} = this.state;
this.setState({ selectedLayerIndex: idx }) const {selectedLayerIndex} = this.state;
const url = new URL(location.href);
const hashVal = hash(JSON.stringify(mapStyle));
url.searchParams.set("layer", `${hashVal}~${selectedLayerIndex}`);
const openModals = Object.entries(isOpen)
.map(([key, val]) => (val === true ? key : null))
.filter(val => val !== null);
if (openModals.length > 0) {
url.searchParams.set("modal", openModals.join(","));
}
else {
url.searchParams.delete("modal");
}
if (mapState === "map") {
url.searchParams.delete("view");
}
else if (mapState === "inspect") {
url.searchParams.set("view", "inspect");
}
history.replaceState({selectedLayerIndex}, "Maputnik", url.href);
}
getInitialStateFromUrl = (mapStyle) => {
const url = new URL(location.href);
const modalParam = url.searchParams.get("modal");
if (modalParam && modalParam !== "") {
const modals = modalParam.split(",");
const modalObj = {};
modals.forEach(modalName => {
modalObj[modalName] = true;
});
this.setState({
isOpen: {
...this.state.isOpen,
...modalObj,
}
});
}
const view = url.searchParams.get("view");
if (view && view !== "") {
this.setMapState(view);
}
const path = url.searchParams.get("layer");
if (path) {
try {
const parts = path.split("~");
const [hashVal, selectedLayerIndex] = [
parts[0],
parseInt(parts[1], 10),
];
let valid = true;
if (hashVal !== "-") {
const currentHashVal = hash(JSON.stringify(mapStyle));
if (currentHashVal !== parseInt(hashVal, 10)) {
valid = false;
}
}
if (valid) {
this.setState({
selectedLayerIndex,
selectedLayerOriginalId: mapStyle.layers[selectedLayerIndex].id,
});
}
}
catch (err) {
console.warn(err);
}
}
}
onLayerSelect = (index) => {
this.setState({
selectedLayerIndex: index,
selectedLayerOriginalId: this.state.mapStyle.layers[index].id,
}, this.setStateInUrl);
} }
setModal(modalName, value) { setModal(modalName, value) {
@@ -660,7 +784,7 @@ export default class App extends React.Component {
...this.state.isOpen, ...this.state.isOpen,
[modalName]: value [modalName]: value
} }
}) }, this.setStateInUrl)
} }
toggleModal(modalName) { toggleModal(modalName) {
@@ -690,7 +814,7 @@ export default class App extends React.Component {
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : null const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : null
const metadata = this.state.mapStyle.metadata || {} const metadata = this.state.mapStyle.metadata || {}
const toolbar = <Toolbar const toolbar = <AppToolbar
renderer={this._getRenderer()} renderer={this._getRenderer()}
mapState={this.state.mapState} mapState={this.state.mapState}
mapStyle={this.state.mapStyle} mapStyle={this.state.mapStyle}
@@ -716,7 +840,7 @@ export default class App extends React.Component {
/> />
const layerEditor = selectedLayer ? <LayerEditor const layerEditor = selectedLayer ? <LayerEditor
key={selectedLayer.id} key={this.state.selectedLayerOriginalId}
layer={selectedLayer} layer={selectedLayer}
layerIndex={this.state.selectedLayerIndex} layerIndex={this.state.selectedLayerIndex}
isFirstLayer={this.state.selectedLayerIndex < 1} isFirstLayer={this.state.selectedLayerIndex < 1}
@@ -735,6 +859,7 @@ export default class App extends React.Component {
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
currentLayer={selectedLayer} currentLayer={selectedLayer}
selectedLayerIndex={this.state.selectedLayerIndex}
onLayerSelect={this.onLayerSelect} onLayerSelect={this.onLayerSelect}
mapStyle={this.state.mapStyle} mapStyle={this.state.mapStyle}
errors={this.state.errors} errors={this.state.errors}
@@ -743,7 +868,7 @@ export default class App extends React.Component {
const modals = <div> const modals = <div>
<DebugModal <ModalDebug
renderer={this._getRenderer()} renderer={this._getRenderer()}
mapboxGlDebugOptions={this.state.mapboxGlDebugOptions} mapboxGlDebugOptions={this.state.mapboxGlDebugOptions}
openlayersDebugOptions={this.state.openlayersDebugOptions} openlayersDebugOptions={this.state.openlayersDebugOptions}
@@ -753,12 +878,12 @@ export default class App extends React.Component {
onOpenToggle={this.toggleModal.bind(this, 'debug')} onOpenToggle={this.toggleModal.bind(this, 'debug')}
mapView={this.state.mapView} mapView={this.state.mapView}
/> />
<ShortcutsModal <ModalShortcuts
ref={(el) => this.shortcutEl = el} ref={(el) => this.shortcutEl = el}
isOpen={this.state.isOpen.shortcuts} isOpen={this.state.isOpen.shortcuts}
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')} onOpenToggle={this.toggleModal.bind(this, 'shortcuts')}
/> />
<SettingsModal <ModalSettings
mapStyle={this.state.mapStyle} mapStyle={this.state.mapStyle}
onStyleChanged={this.onStyleChanged} onStyleChanged={this.onStyleChanged}
onChangeMetadataProperty={this.onChangeMetadataProperty} onChangeMetadataProperty={this.onChangeMetadataProperty}
@@ -766,24 +891,24 @@ export default class App extends React.Component {
onOpenToggle={this.toggleModal.bind(this, 'settings')} onOpenToggle={this.toggleModal.bind(this, 'settings')}
openlayersDebugOptions={this.state.openlayersDebugOptions} openlayersDebugOptions={this.state.openlayersDebugOptions}
/> />
<ExportModal <ModalExport
mapStyle={this.state.mapStyle} mapStyle={this.state.mapStyle}
onStyleChanged={this.onStyleChanged} onStyleChanged={this.onStyleChanged}
isOpen={this.state.isOpen.export} isOpen={this.state.isOpen.export}
onOpenToggle={this.toggleModal.bind(this, 'export')} onOpenToggle={this.toggleModal.bind(this, 'export')}
/> />
<OpenModal <ModalOpen
isOpen={this.state.isOpen.open} isOpen={this.state.isOpen.open}
onStyleOpen={this.openStyle} onStyleOpen={this.openStyle}
onOpenToggle={this.toggleModal.bind(this, 'open')} onOpenToggle={this.toggleModal.bind(this, 'open')}
/> />
<SourcesModal <ModalSources
mapStyle={this.state.mapStyle} mapStyle={this.state.mapStyle}
onStyleChanged={this.onStyleChanged} onStyleChanged={this.onStyleChanged}
isOpen={this.state.isOpen.sources} isOpen={this.state.isOpen.sources}
onOpenToggle={this.toggleModal.bind(this, 'sources')} onOpenToggle={this.toggleModal.bind(this, 'sources')}
/> />
<SurveyModal <ModalSurvey
isOpen={this.state.isOpen.survey} isOpen={this.state.isOpen.survey}
onOpenToggle={this.toggleModal.bind(this, 'survey')} onOpenToggle={this.toggleModal.bind(this, 'survey')}
/> />

View File

@@ -26,9 +26,7 @@ class AppLayout extends React.Component {
return <div className="maputnik-layout"> return <div className="maputnik-layout">
{this.props.toolbar} {this.props.toolbar}
<div className="maputnik-layout-list"> <div className="maputnik-layout-list">
<ScrollContainer> {this.props.layerList}
{this.props.layerList}
</ScrollContainer>
</div> </div>
<div className="maputnik-layout-drawer"> <div className="maputnik-layout-drawer">
<ScrollContainer> <ScrollContainer>

View File

@@ -1,13 +1,15 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import {formatLayerId} from '../util/format';
class MessagePanel extends React.Component { export default class AppMessagePanel extends React.Component {
static propTypes = { static propTypes = {
errors: PropTypes.array, errors: PropTypes.array,
infos: PropTypes.array, infos: PropTypes.array,
mapStyle: PropTypes.object, mapStyle: PropTypes.object,
onLayerSelect: PropTypes.func, onLayerSelect: PropTypes.func,
currentLayer: PropTypes.object, currentLayer: PropTypes.object,
selectedLayerIndex: PropTypes.number,
} }
static defaultProps = { static defaultProps = {
@@ -15,6 +17,7 @@ class MessagePanel extends React.Component {
} }
render() { render() {
const {selectedLayerIndex} = this.props;
const errors = this.props.errors.map((error, idx) => { const errors = this.props.errors.map((error, idx) => {
let content; let content;
if (error.parsed && error.parsed.type === "layer") { if (error.parsed && error.parsed.type === "layer") {
@@ -23,13 +26,13 @@ class MessagePanel extends React.Component {
const layerId = mapStyle.layers[parsed.data.index].id; const layerId = mapStyle.layers[parsed.data.index].id;
content = ( content = (
<> <>
Layer <span>&apos;{layerId}&apos;</span>: {parsed.data.message} Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
{currentLayer.id !== layerId && {selectedLayerIndex !== parsed.data.index &&
<> <>
&nbsp;&mdash;&nbsp; &nbsp;&mdash;&nbsp;
<button <button
className="maputnik-message-panel__switch-button" className="maputnik-message-panel__switch-button"
onClick={() => this.props.onLayerSelect(layerId)} onClick={() => this.props.onLayerSelect(parsed.data.index)}
> >
switch to layer switch to layer
</button> </button>
@@ -57,5 +60,3 @@ class MessagePanel extends React.Component {
} }
} }
export default MessagePanel

View File

@@ -101,7 +101,7 @@ class ToolbarAction extends React.Component {
} }
} }
export default class Toolbar extends React.Component { export default class AppToolbar extends React.Component {
static propTypes = { static propTypes = {
mapStyle: PropTypes.object.isRequired, mapStyle: PropTypes.object.isRequired,
inspectModeEnabled: PropTypes.bool.isRequired, inspectModeEnabled: PropTypes.bool.isRequired,
@@ -131,35 +131,51 @@ export default class Toolbar extends React.Component {
this.props.onSetMapState(val); this.props.onSetMapState(val);
} }
onSkip = (target) => {
if (target === "map") {
document.querySelector(".mapboxgl-canvas").focus();
}
else {
const el = document.querySelector("#skip-target-"+target);
el.focus();
}
}
render() { render() {
const views = [ const views = [
{ {
id: "map", id: "map",
group: "general",
title: "Map", title: "Map",
}, },
{ {
id: "inspect", id: "inspect",
group: "general",
title: "Inspect", title: "Inspect",
disabled: this.props.renderer !== 'mbgljs', disabled: this.props.renderer !== 'mbgljs',
}, },
{ {
id: "filter-deuteranopia", id: "filter-deuteranopia",
title: "Map (deuteranopia)", group: "color-accessibility",
title: "Deuteranopia filter",
disabled: !colorAccessibilityFiltersEnabled, disabled: !colorAccessibilityFiltersEnabled,
}, },
{ {
id: "filter-protanopia", id: "filter-protanopia",
title: "Map (protanopia)", group: "color-accessibility",
title: "Protanopia filter",
disabled: !colorAccessibilityFiltersEnabled, disabled: !colorAccessibilityFiltersEnabled,
}, },
{ {
id: "filter-tritanopia", id: "filter-tritanopia",
title: "Map (tritanopia)", group: "color-accessibility",
title: "Tritanopia filter",
disabled: !colorAccessibilityFiltersEnabled, disabled: !colorAccessibilityFiltersEnabled,
}, },
{ {
id: "filter-achromatopsia", id: "filter-achromatopsia",
title: "Map (achromatopsia)", group: "color-accessibility",
title: "Achromatopsia filter",
disabled: !colorAccessibilityFiltersEnabled, disabled: !colorAccessibilityFiltersEnabled,
}, },
]; ];
@@ -168,19 +184,38 @@ export default class Toolbar extends React.Component {
return view.id === this.props.mapState; return view.id === this.props.mapState;
}); });
return <div 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"
> >
<a className="maputnik-toolbar-skip" href="#skip-menu"> {/* Keyboard accessible quick links */}
Skip navigation <button
</a> data-wd-key="root:skip:layer-list"
className="maputnik-toolbar-skip"
onClick={e => this.onSkip("layer-list")}
>
Layers list
</button>
<button
data-wd-key="root:skip:layer-editor"
className="maputnik-toolbar-skip"
onClick={e => this.onSkip("layer-editor")}
>
Layer editor
</button>
<button
data-wd-key="root:skip:map-view"
className="maputnik-toolbar-skip"
onClick={e => this.onSkip("map")}
>
Map view
</button>
<a <a
href="https://github.com/maputnik/editor"
rel="noopener noreferrer"
target="_blank"
className="maputnik-toolbar-logo" className="maputnik-toolbar-logo"
target="blank"
rel="noreferrer noopener"
href="https://github.com/maputnik/editor"
> >
<span dangerouslySetInnerHTML={{__html: logoImage}} /> <span dangerouslySetInnerHTML={{__html: logoImage}} />
<h1> <h1>
@@ -189,7 +224,7 @@ export default class Toolbar extends React.Component {
</h1> </h1>
</a> </a>
</div> </div>
<div className="maputnik-toolbar__actions"> <div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar">
<ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}> <ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}>
<MdOpenInBrowser /> <MdOpenInBrowser />
<IconText>Open</IconText> <IconText>Open</IconText>
@@ -209,16 +244,30 @@ export default class Toolbar extends React.Component {
<ToolbarSelect wdKey="nav:inspect"> <ToolbarSelect wdKey="nav:inspect">
<MdFindInPage /> <MdFindInPage />
<IconText>View </IconText> <label>View
<select onChange={(e) => this.handleSelection(e.target.value)} value={currentView.id}> <select
{views.map((item) => { className="maputnik-select"
return ( onChange={(e) => this.handleSelection(e.target.value)}
<option key={item.id} value={item.id} disabled={item.disabled}> value={currentView.id}
{item.title} >
</option> {views.filter(v => v.group === "general").map((item) => {
); return (
})} <option key={item.id} value={item.id} disabled={item.disabled}>
</select> {item.title}
</option>
);
})}
<optgroup label="Color accessibility">
{views.filter(v => v.group === "color-accessibility").map((item) => {
return (
<option key={item.id} value={item.id} disabled={item.disabled}>
{item.title}
</option>
);
})}
</optgroup>
</select>
</label>
</ToolbarSelect> </ToolbarSelect>
<ToolbarLink href={"https://github.com/maputnik/editor/wiki"}> <ToolbarLink href={"https://github.com/maputnik/editor/wiki"}>
@@ -231,6 +280,6 @@ export default class Toolbar extends React.Component {
</ToolbarLinkHighlighted> </ToolbarLinkHighlighted>
</div> </div>
</div> </div>
</div> </nav>
} }
} }

103
src/components/Block.jsx Normal file
View File

@@ -0,0 +1,103 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import FieldDocLabel from './FieldDocLabel'
import Doc from './Doc'
/** Wrap a component with a label */
export default class Block extends React.Component {
static propTypes = {
"data-wd-key": PropTypes.string,
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.element,
]),
action: PropTypes.element,
children: PropTypes.node.isRequired,
style: PropTypes.object,
onChange: PropTypes.func,
fieldSpec: PropTypes.object,
wideMode: PropTypes.bool,
error: PropTypes.array,
}
constructor (props) {
super(props);
this.state = {
showDoc: false,
}
}
onChange(e) {
const value = e.target.value
return this.props.onChange(value === "" ? undefined : value)
}
onToggleDoc = (val) => {
this.setState({
showDoc: val
});
}
/**
* Some fields for example <InputColor/> bind click events inside the element
* to close the picker. This in turn propagates to the <label/> element
* causing the picker to reopen. This causes a scenario where the picker can
* never be closed once open.
*/
onLabelClick = (event) => {
const el = event.nativeEvent.target;
const nativeEvent = event.nativeEvent;
const contains = this._blockEl.contains(el);
if (event.nativeEvent.target.nodeName !== "INPUT" && !contains) {
event.stopPropagation();
}
event.preventDefault();
}
render() {
const errors = [].concat(this.props.error || []);
return <label style={this.props.style}
data-wd-key={this.props["data-wd-key"]}
className={classnames({
"maputnik-input-block": true,
"maputnik-input-block--wide": this.props.wideMode,
"maputnik-action-block": this.props.action
})}
onClick={this.onLabelClick}
>
{this.props.fieldSpec &&
<div className="maputnik-input-block-label">
<FieldDocLabel
label={this.props.label}
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 className="maputnik-input-block-content" ref={el => this._blockEl = el}>
{this.props.children}
</div>
{this.props.fieldSpec &&
<div
className="maputnik-doc-inline"
style={{display: this.state.showDoc ? '' : 'none'}}
>
<Doc fieldSpec={this.props.fieldSpec} />
</div>
}
</label>
}
}

View File

@@ -1,10 +1,10 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import Collapse from 'react-collapse' import { Collapse as ReactCollapse } from 'react-collapse'
import accessibility from '../../libs/accessibility' import accessibility from '../../libs/accessibility'
export default class CollapseAlt extends React.Component { export default class Collapse extends React.Component {
static propTypes = { static propTypes = {
isActive: PropTypes.bool.isRequired, isActive: PropTypes.bool.isRequired,
children: PropTypes.element.isRequired children: PropTypes.element.isRequired
@@ -24,9 +24,9 @@ export default class CollapseAlt extends React.Component {
} }
else { else {
return ( return (
<Collapse isOpened={this.props.isActive}> <ReactCollapse isOpened={this.props.isActive}>
{this.props.children} {this.props.children}
</Collapse> </ReactCollapse>
) )
} }
} }

View File

@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
export default class SpecDoc extends React.Component { export default class Doc extends React.Component {
static propTypes = { static propTypes = {
fieldSpec: PropTypes.object.isRequired, fieldSpec: PropTypes.object.isRequired,
} }

View File

@@ -0,0 +1,21 @@
import React from 'react'
import PropTypes from 'prop-types'
import Block from './Block'
import InputArray from './InputArray'
import Fieldset from './Fieldset'
export default class FieldArray extends React.Component {
static propTypes = {
...InputArray.propTypes,
name: PropTypes.string,
}
render() {
const {props} = this;
return <Fieldset label={props.label}>
<InputArray {...props} />
</Fieldset>
}
}

View File

@@ -0,0 +1,20 @@
import React from 'react'
import PropTypes from 'prop-types'
import Block from './Block'
import InputAutocomplete from './InputAutocomplete'
export default class FieldAutocomplete extends React.Component {
static propTypes = {
...InputAutocomplete.propTypes,
}
render() {
const {props} = this;
return <Block label={props.label}>
<InputAutocomplete {...props} />
</Block>
}
}

View File

@@ -0,0 +1,20 @@
import React from 'react'
import PropTypes from 'prop-types'
import Block from './Block'
import InputCheckbox from './InputCheckbox'
export default class FieldCheckbox extends React.Component {
static propTypes = {
...InputCheckbox.propTypes,
}
render() {
const {props} = this;
return <Block label={this.props.label}>
<InputCheckbox {...props} />
</Block>
}
}

View File

@@ -0,0 +1,20 @@
import React from 'react'
import PropTypes from 'prop-types'
import Block from './Block'
import InputColor from './InputColor'
export default class FieldColor extends React.Component {
static propTypes = {
...InputColor.propTypes,
}
render() {
const {props} = this;
return <Block label={props.label}>
<InputColor {...props} />
</Block>
}
}

View File

@@ -0,0 +1,31 @@
import React from 'react'
import PropTypes from 'prop-types'
import Block from './Block'
import InputString from './InputString'
export default class FieldComment extends React.Component {
static propTypes = {
value: PropTypes.string,
onChange: PropTypes.func.isRequired,
}
render() {
const fieldSpec = {
doc: "Comments for the current layer. This is non-standard and not in the spec."
};
return <Block
label={"Comments"}
fieldSpec={fieldSpec}
data-wd-key="layer-comment"
>
<InputString
multi={true}
value={this.props.value}
onChange={this.props.onChange}
default="Comment..."
/>
</Block>
}
}

View File

@@ -3,14 +3,14 @@ import PropTypes from 'prop-types'
import {MdInfoOutline, MdHighlightOff} from 'react-icons/md' import {MdInfoOutline, MdHighlightOff} from 'react-icons/md'
export default class DocLabel extends React.Component { export default class FieldDocLabel extends React.Component {
static propTypes = { static propTypes = {
label: PropTypes.oneOfType([ label: PropTypes.oneOfType([
PropTypes.object, PropTypes.object,
PropTypes.string PropTypes.string
]).isRequired, ]).isRequired,
fieldSpec: PropTypes.object.isRequired, fieldSpec: PropTypes.object,
onToggleDoc: PropTypes.func.isRequired, onToggleDoc: PropTypes.func,
} }
constructor (props) { constructor (props) {

View File

@@ -0,0 +1,21 @@
import React from 'react'
import PropTypes from 'prop-types'
import Block from './Block'
import InputDynamicArray from './InputDynamicArray'
import Fieldset from './Fieldset'
export default class FieldDynamicArray extends React.Component {
static propTypes = {
...InputDynamicArray.propTypes,
name: PropTypes.string,
}
render() {
const {props} = this;
return <Fieldset label={props.label}>
<InputDynamicArray {...props} />
</Fieldset>
}
}

View File

@@ -0,0 +1,20 @@
import React from 'react'
import PropTypes from 'prop-types'
import InputEnum from './InputEnum'
import Block from './Block';
import Fieldset from './Fieldset';
export default class FieldEnum extends React.Component {
static propTypes = {
...InputEnum.propTypes,
}
render() {
const {props} = this;
return <Fieldset label={props.label}>
<InputEnum {...props} />
</Fieldset>
}
}

View File

@@ -5,13 +5,22 @@ import SpecProperty from './_SpecProperty'
import DataProperty from './_DataProperty' import DataProperty from './_DataProperty'
import ZoomProperty from './_ZoomProperty' import ZoomProperty from './_ZoomProperty'
import ExpressionProperty from './_ExpressionProperty' import ExpressionProperty from './_ExpressionProperty'
import {function as styleFunction} from '@mapbox/mapbox-gl-style-spec'; import {function as styleFunction} from '@maplibre/maplibre-gl-style-spec';
import {findDefaultFromSpec} from '../util/spec-helper';
function isLiteralExpression (value) { function isLiteralExpression (value) {
return (Array.isArray(value) && value.length === 2 && value[0] === "literal"); return (Array.isArray(value) && value.length === 2 && value[0] === "literal");
} }
function isGetExpression (value) {
return (
Array.isArray(value) &&
value.length === 2 &&
value[0] === "get"
);
}
function isZoomField(value) { function isZoomField(value) {
return ( return (
typeof(value) === 'object' && typeof(value) === 'object' &&
@@ -28,7 +37,15 @@ function isZoomField(value) {
); );
} }
function isDataField(value) { function isIdentityProperty (value) {
return (
typeof(value) === 'object' &&
value.type === "identity" &&
value.hasOwnProperty("property")
);
}
function isDataStopProperty (value) {
return ( return (
typeof(value) === 'object' && typeof(value) === 'object' &&
value.stops && value.stops &&
@@ -45,6 +62,13 @@ function isDataField(value) {
); );
} }
function isDataField(value) {
return (
isIdentityProperty(value) ||
isDataStopProperty(value)
);
}
function isPrimative (value) { function isPrimative (value) {
const valid = ["string", "boolean", "number"]; const valid = ["string", "boolean", "number"];
return valid.includes(typeof(value)); return valid.includes(typeof(value));
@@ -78,29 +102,11 @@ function getDataType (value, fieldSpec={}) {
} }
} }
/**
* If we don't have a default value just make one up
*/
function findDefaultFromSpec (spec) {
if (spec.hasOwnProperty('default')) {
return spec.default;
}
const defaults = {
'color': '#000000',
'string': '',
'boolean': false,
'number': 0,
'array': [],
}
return defaults[spec.type] || '';
}
/** Supports displaying spec field for zoom function objects /** Supports displaying spec field for zoom function objects
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property * https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
*/ */
export default class FunctionSpecProperty extends React.Component { export default class FieldFunction extends React.Component {
static propTypes = { static propTypes = {
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
fieldName: PropTypes.string.isRequired, fieldName: PropTypes.string.isRequired,
@@ -194,19 +200,53 @@ export default class FunctionSpecProperty extends React.Component {
} }
makeZoomFunction = () => { makeZoomFunction = () => {
const zoomFunc = { const {value} = this.props;
stops: [
[6, this.props.value || findDefaultFromSpec(this.props.fieldSpec)], let zoomFunc;
[10, this.props.value || findDefaultFromSpec(this.props.fieldSpec)] if (typeof(value) === "object") {
] if (value.stops) {
zoomFunc = {
base: value.base,
stops: value.stops.map(stop => {
return [stop[0].zoom, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
})
}
}
else {
zoomFunc = {
base: value.base,
stops: [
[6, findDefaultFromSpec(this.props.fieldSpec)],
[10, findDefaultFromSpec(this.props.fieldSpec)]
]
}
}
} }
else {
zoomFunc = {
stops: [
[6, value || findDefaultFromSpec(this.props.fieldSpec)],
[10, value || findDefaultFromSpec(this.props.fieldSpec)]
]
}
}
this.props.onChange(this.props.fieldName, zoomFunc) this.props.onChange(this.props.fieldName, zoomFunc)
} }
undoExpression = () => { undoExpression = () => {
const {value, fieldName} = this.props; const {value, fieldName} = this.props;
if (isLiteralExpression(value)) { if (isGetExpression(value)) {
this.props.onChange(fieldName, {
"type": "identity",
"property": value[1]
});
this.setState({
dataType: "value",
});
}
else if (isLiteralExpression(value)) {
this.props.onChange(fieldName, value[1]); this.props.onChange(fieldName, value[1]);
this.setState({ this.setState({
dataType: "value", dataType: "value",
@@ -217,6 +257,7 @@ export default class FunctionSpecProperty extends React.Component {
canUndo = () => { canUndo = () => {
const {value, fieldSpec} = this.props; const {value, fieldSpec} = this.props;
return ( return (
isGetExpression(value) ||
isLiteralExpression(value) || isLiteralExpression(value) ||
isPrimative(value) || isPrimative(value) ||
(Array.isArray(value) && fieldSpec.type === "array") (Array.isArray(value) && fieldSpec.type === "array")
@@ -230,6 +271,9 @@ export default class FunctionSpecProperty extends React.Component {
if (typeof(value) === "object" && 'stops' in value) { if (typeof(value) === "object" && 'stops' in value) {
expression = styleFunction.convertFunction(value, fieldSpec); expression = styleFunction.convertFunction(value, fieldSpec);
} }
else if (isIdentityProperty(value)) {
expression = ["get", value.property];
}
else { else {
expression = ["literal", value || this.props.fieldSpec.default]; expression = ["literal", value || this.props.fieldSpec.default];
} }
@@ -239,14 +283,44 @@ export default class FunctionSpecProperty extends React.Component {
makeDataFunction = () => { makeDataFunction = () => {
const functionType = this.getFieldFunctionType(this.props.fieldSpec); const functionType = this.getFieldFunctionType(this.props.fieldSpec);
const stopValue = functionType === 'categorical' ? '' : 0; const stopValue = functionType === 'categorical' ? '' : 0;
const dataFunc = { const {value} = this.props;
property: "", let dataFunc;
type: functionType,
stops: [ if (typeof(value) === "object") {
[{zoom: 6, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)], if (value.stops) {
[{zoom: 10, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)] dataFunc = {
] property: "",
type: functionType,
base: value.base,
stops: value.stops.map(stop => {
return [{zoom: stop[0], value: stopValue}, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
})
}
}
else {
dataFunc = {
property: "",
type: functionType,
base: value.base,
stops: [
[{zoom: 6, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)],
[{zoom: 10, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)]
]
}
}
} }
else {
dataFunc = {
property: "",
type: functionType,
base: value.base,
stops: [
[{zoom: 6, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)],
[{zoom: 10, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)]
]
}
}
this.props.onChange(this.props.fieldName, dataFunc) this.props.onChange(this.props.fieldName, dataFunc)
} }
@@ -291,11 +365,13 @@ export default class FunctionSpecProperty extends React.Component {
value={this.props.value} value={this.props.value}
onDeleteStop={this.deleteStop} onDeleteStop={this.deleteStop}
onAddStop={this.addStop} onAddStop={this.addStop}
onExpressionClick={this.makeExpression} onChangeToDataFunction={this.makeDataFunction}
onExpressionClick={this.makeExpression}
/> />
) )
} }
else if (dataType === "data_function") { else if (dataType === "data_function") {
// TODO: Rename to FieldFunction **this file** shouldn't be called that
specField = ( specField = (
<DataProperty <DataProperty
errors={this.props.errors} errors={this.props.errors}
@@ -306,7 +382,8 @@ export default class FunctionSpecProperty extends React.Component {
value={this.props.value} value={this.props.value}
onDeleteStop={this.deleteStop} onDeleteStop={this.deleteStop}
onAddStop={this.addStop} onAddStop={this.addStop}
onExpressionClick={this.makeExpression} onChangeToZoomFunction={this.makeZoomFunction}
onExpressionClick={this.makeExpression}
/> />
) )
} }
@@ -320,8 +397,8 @@ export default class FunctionSpecProperty extends React.Component {
fieldSpec={this.props.fieldSpec} fieldSpec={this.props.fieldSpec}
value={this.props.value} value={this.props.value}
onZoomClick={this.makeZoomFunction} onZoomClick={this.makeZoomFunction}
onDataClick={this.makeDataFunction} onDataClick={this.makeDataFunction}
onExpressionClick={this.makeExpression} onExpressionClick={this.makeExpression}
/> />
) )
} }

View File

@@ -0,0 +1,27 @@
import React from 'react'
import PropTypes from 'prop-types'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import Block from './Block'
import InputString from './InputString'
export default class FieldId extends React.Component {
static propTypes = {
value: PropTypes.string.isRequired,
wdKey: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
error: PropTypes.object,
}
render() {
return <Block label={"ID"} fieldSpec={latest.layer.id}
data-wd-key={this.props.wdKey}
error={this.props.error}
>
<InputString
value={this.props.value}
onInput={this.props.onChange}
/>
</Block>
}
}

View File

@@ -0,0 +1,16 @@
import React from 'react'
import PropTypes from 'prop-types'
import InputJson from './InputJson'
export default class FieldJson extends React.Component {
static propTypes = {
...InputJson.propTypes,
}
render() {
const {props} = this;
return <InputJson {...props} />
}
}

View File

@@ -0,0 +1,30 @@
import React from 'react'
import PropTypes from 'prop-types'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import Block from './Block'
import InputNumber from './InputNumber'
export default class FieldMaxZoom extends React.Component {
static propTypes = {
value: PropTypes.number,
onChange: PropTypes.func.isRequired,
error: PropTypes.object,
}
render() {
return <Block label={"Max Zoom"} fieldSpec={latest.layer.maxzoom}
error={this.props.error}
data-wd-key="max-zoom"
>
<InputNumber
allowRange={true}
value={this.props.value}
onChange={this.props.onChange}
min={latest.layer.maxzoom.minimum}
max={latest.layer.maxzoom.maximum}
default={latest.layer.maxzoom.maximum}
/>
</Block>
}
}

View File

@@ -0,0 +1,30 @@
import React from 'react'
import PropTypes from 'prop-types'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import Block from './Block'
import InputNumber from './InputNumber'
export default class FieldMinZoom extends React.Component {
static propTypes = {
value: PropTypes.number,
onChange: PropTypes.func.isRequired,
error: PropTypes.object,
}
render() {
return <Block label={"Min Zoom"} fieldSpec={latest.layer.minzoom}
error={this.props.error}
data-wd-key="min-zoom"
>
<InputNumber
allowRange={true}
value={this.props.value}
onChange={this.props.onChange}
min={latest.layer.minzoom.minimum}
max={latest.layer.minzoom.maximum}
default={latest.layer.minzoom.minimum}
/>
</Block>
}
}

View File

@@ -0,0 +1,21 @@
import React from 'react'
import PropTypes from 'prop-types'
import Block from './Block'
import InputMultiInput from './InputMultiInput'
import Fieldset from './Fieldset'
export default class FieldMultiInput extends React.Component {
static propTypes = {
...InputMultiInput.propTypes,
}
render() {
const {props} = this;
return <Fieldset label={props.label}>
<InputMultiInput {...props} />
</Fieldset>
}
}

View File

@@ -0,0 +1,19 @@
import React from 'react'
import PropTypes from 'prop-types'
import InputNumber from './InputNumber'
import Block from './Block'
export default class FieldNumber extends React.Component {
static propTypes = {
...InputNumber.propTypes,
}
render() {
const {props} = this;
return <Block label={props.label}>
<InputNumber {...props} />
</Block>
}
}

View File

@@ -0,0 +1,20 @@
import React from 'react'
import PropTypes from 'prop-types'
import Block from './Block'
import InputSelect from './InputSelect'
export default class FieldSelect extends React.Component {
static propTypes = {
...InputSelect.propTypes,
}
render() {
const {props} = this;
return <Block label={props.label}>
<InputSelect {...props}/>
</Block>
}
}

View File

@@ -0,0 +1,36 @@
import React from 'react'
import PropTypes from 'prop-types'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import Block from './Block'
import InputAutocomplete from './InputAutocomplete'
export default class FieldSource extends React.Component {
static propTypes = {
value: PropTypes.string,
wdKey: PropTypes.string,
onChange: PropTypes.func,
sourceIds: PropTypes.array,
error: PropTypes.object,
}
static defaultProps = {
onChange: () => {},
sourceIds: [],
}
render() {
return <Block
label={"Source"}
fieldSpec={latest.layer.source}
error={this.props.error}
data-wd-key={this.props.wdKey}
>
<InputAutocomplete
value={this.props.value}
onChange={this.props.onChange}
options={this.props.sourceIds.map(src => [src, src])}
/>
</Block>
}
}

View File

@@ -0,0 +1,34 @@
import React from 'react'
import PropTypes from 'prop-types'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import Block from './Block'
import InputAutocomplete from './InputAutocomplete'
export default class FieldSourceLayer extends React.Component {
static propTypes = {
value: PropTypes.string,
onChange: PropTypes.func,
sourceLayerIds: PropTypes.array,
isFixed: PropTypes.bool,
}
static defaultProps = {
onChange: () => {},
sourceLayerIds: [],
isFixed: false
}
render() {
return <Block label={"Source Layer"} fieldSpec={latest.layer['source-layer']}
data-wd-key="layer-source-layer"
>
<InputAutocomplete
keepMenuWithinWindowBounds={!!this.props.isFixed}
value={this.props.value}
onChange={this.props.onChange}
options={this.props.sourceLayerIds.map(l => [l, l])}
/>
</Block>
}
}

View File

@@ -0,0 +1,20 @@
import React from 'react'
import PropTypes from 'prop-types'
import Block from './Block'
import InputString from './InputString'
export default class FieldString extends React.Component {
static propTypes = {
...InputString.propTypes,
name: PropTypes.string,
}
render() {
const {props} = this;
return <Block label={props.label}>
<InputString {...props} />
</Block>
}
}

View File

@@ -0,0 +1,52 @@
import React from 'react'
import PropTypes from 'prop-types'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import Block from './Block'
import InputSelect from './InputSelect'
import InputString from './InputString'
export default class FieldType extends React.Component {
static propTypes = {
value: PropTypes.string.isRequired,
wdKey: PropTypes.string,
onChange: PropTypes.func.isRequired,
error: PropTypes.object,
disabled: PropTypes.bool,
}
static defaultProps = {
disabled: false,
}
render() {
return <Block label={"Type"} fieldSpec={latest.layer.type}
data-wd-key={this.props.wdKey}
error={this.props.error}
>
{this.props.disabled &&
<InputString
value={this.props.value}
disabled={true}
/>
}
{!this.props.disabled &&
<InputSelect
options={[
['background', 'Background'],
['fill', 'Fill'],
['line', 'Line'],
['symbol', 'Symbol'],
['raster', 'Raster'],
['circle', 'Circle'],
['fill-extrusion', 'Fill Extrusion'],
['hillshade', 'Hillshade'],
['heatmap', 'Heatmap'],
]}
onChange={this.props.onChange}
value={this.props.value}
/>
}
</Block>
}
}

View File

@@ -0,0 +1,22 @@
import React, {Fragment} from 'react'
import PropTypes from 'prop-types'
import InputUrl from './InputUrl'
import Block from './Block'
export default class FieldUrl extends React.Component {
static propTypes = {
...InputUrl.propTypes,
}
render () {
const {props} = this;
return (
<Block label={this.props.label}>
<InputUrl {...props} />
</Block>
);
}
}

View File

@@ -0,0 +1,58 @@
import React from 'react'
import PropTypes from 'prop-types'
import FieldDocLabel from './FieldDocLabel'
import Doc from './Doc'
let IDX = 0;
export default class Fieldset extends React.Component {
constructor (props) {
super(props);
this._labelId = `fieldset_label_${(IDX++)}`;
this.state = {
showDoc: false,
}
}
onToggleDoc = (val) => {
this.setState({
showDoc: val
});
}
render () {
const {props} = this;
return <div className="maputnik-input-block" role="group" aria-labelledby={this._labelId}>
{this.props.fieldSpec &&
<div className="maputnik-input-block-label">
<FieldDocLabel
label={this.props.label}
onToggleDoc={this.onToggleDoc}
fieldSpec={this.props.fieldSpec}
/>
</div>
}
{!this.props.fieldSpec &&
<div className="maputnik-input-block-label">
{props.label}
</div>
}
<div className="maputnik-input-block-action">
{this.props.action}
</div>
<div className="maputnik-input-block-content">
{props.children}
</div>
{this.props.fieldSpec &&
<div
className="maputnik-doc-inline"
style={{display: this.state.showDoc ? '' : 'none'}}
>
<Doc fieldSpec={this.props.fieldSpec} />
</div>
}
</div>
}
}

View File

@@ -1,16 +1,17 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { combiningFilterOps } from '../../libs/filterops.js' import { combiningFilterOps } from '../libs/filterops.js'
import {mdiTableRowPlusAfter} from '@mdi/js';
import {isEqual} from 'lodash';
import {latest, validate, migrate} from '@mapbox/mapbox-gl-style-spec' import {latest, migrate, convertFilter} from '@maplibre/maplibre-gl-style-spec'
import DocLabel from '../fields/DocLabel' import InputSelect from './InputSelect'
import SelectInput from '../inputs/SelectInput' import Block from './Block'
import InputBlock from '../inputs/InputBlock'
import SingleFilterEditor from './SingleFilterEditor' import SingleFilterEditor from './SingleFilterEditor'
import FilterEditorBlock from './FilterEditorBlock' import FilterEditorBlock from './FilterEditorBlock'
import Button from '../Button' import InputButton from './InputButton'
import SpecDoc from '../inputs/SpecDoc' import Doc from './Doc'
import ExpressionProperty from '../fields/_ExpressionProperty'; import ExpressionProperty from './_ExpressionProperty';
import {mdiFunctionVariant} from '@mdi/js'; import {mdiFunctionVariant} from '@mdi/js';
@@ -61,24 +62,19 @@ function createStyleFromFilter (filter) {
}; };
} }
/** const FILTER_OPS = [
* This is doing way more work than we need it to, however validating a whole "all",
* style if the only thing that's exported from mapbox-gl-style-spec at the "any",
* moment. Not really an issue though as it take ~0.1ms to calculate. "none"
*/ ];
// If we convert a filter that is an expression to an expression it'll remain the same in value
function checkIfSimpleFilter (filter) { function checkIfSimpleFilter (filter) {
if (!filter || !combiningFilterOps.includes(filter[0])) { if (filter.length === 1 && FILTER_OPS.includes(filter[0])) {
return false; return true;
} }
const expression = convertFilter(filter);
// Because "none" isn't supported by the next expression syntax we can test return !isEqual(expression, filter);
// with ["none", ...] because it'll return false if it's a new style
// expression.
const moddedFilter = ["none", ...filter.slice(1)];
const tmpStyle = createStyleFromFilter(moddedFilter)
const errors = validate(tmpStyle);
return (errors.length < 1);
} }
function hasCombiningFilter(filter) { function hasCombiningFilter(filter) {
@@ -93,7 +89,7 @@ function hasNestedCombiningFilter(filter) {
return false return false
} }
export default class CombiningFilterEditor extends React.Component { export default class FilterEditor extends React.Component {
static propTypes = { static propTypes = {
/** Properties of the vector layer and the available fields */ /** Properties of the vector layer and the available fields */
properties: PropTypes.object, properties: PropTypes.object,
@@ -191,14 +187,15 @@ export default class CombiningFilterEditor extends React.Component {
<p> <p>
Nested filters are not supported. Nested filters are not supported.
</p> </p>
<Button <InputButton
onClick={this.makeExpression} onClick={this.makeExpression}
title="Convert to expression"
> >
<svg style={{marginRight: "0.2em", width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24"> <svg style={{marginRight: "0.2em", width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiFunctionVariant} /> <path fill="currentColor" d={mdiFunctionVariant} />
</svg> </svg>
Upgrade to expression Upgrade to expression
</Button> </InputButton>
</div> </div>
} }
else if (displaySimpleFilter) { else if (displaySimpleFilter) {
@@ -208,14 +205,15 @@ export default class CombiningFilterEditor extends React.Component {
const actions = ( const actions = (
<div> <div>
<Button <InputButton
onClick={this.makeExpression} onClick={this.makeExpression}
title="Convert to expression"
className="maputnik-make-zoom-function" className="maputnik-make-zoom-function"
> >
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24"> <svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiFunctionVariant} /> <path fill="currentColor" d={mdiFunctionVariant} />
</svg> </svg>
</Button> </InputButton>
</div> </div>
); );
@@ -223,7 +221,7 @@ export default class CombiningFilterEditor extends React.Component {
const error = errors[`filter[${idx+1}]`]; const error = errors[`filter[${idx+1}]`];
return ( return (
<> <div key={`block-${idx}`}>
<FilterEditorBlock key={idx} onDelete={this.deleteFilterItem.bind(this, idx)}> <FilterEditorBlock key={idx} onDelete={this.deleteFilterItem.bind(this, idx)}>
<SingleFilterEditor <SingleFilterEditor
properties={this.props.properties} properties={this.props.properties}
@@ -232,45 +230,48 @@ export default class CombiningFilterEditor extends React.Component {
/> />
</FilterEditorBlock> </FilterEditorBlock>
{error && {error &&
<div className="maputnik-inline-error">{error.message}</div> <div key="error" className="maputnik-inline-error">{error.message}</div>
} }
</> </div>
); );
}) })
return ( return (
<> <>
<InputBlock <Block
key="top" key="top"
fieldSpec={fieldSpec} fieldSpec={fieldSpec}
label={"Filter"} label={"Filter"}
action={actions} action={actions}
> >
<SelectInput <InputSelect
value={combiningOp} value={combiningOp}
onChange={this.onFilterPartChanged.bind(this, 0)} onChange={this.onFilterPartChanged.bind(this, 0)}
options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]} options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
/> />
</InputBlock> </Block>
{editorBlocks} {editorBlocks}
<div <div
key="buttons" key="buttons"
className="maputnik-filter-editor-add-wrapper" className="maputnik-filter-editor-add-wrapper"
> >
<Button <InputButton
data-wd-key="layer-filter-button" data-wd-key="layer-filter-button"
className="maputnik-add-filter" className="maputnik-add-filter"
onClick={this.addFilterItem}> onClick={this.addFilterItem}
Add filter >
</Button> <svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiTableRowPlusAfter} />
</svg> Add filter
</InputButton>
</div> </div>
<div <div
key="doc" key="doc"
className="maputnik-doc-inline" className="maputnik-doc-inline"
style={{display: this.state.showDoc ? '' : 'none'}} style={{display: this.state.showDoc ? '' : 'none'}}
> >
<SpecDoc fieldSpec={fieldSpec} /> <Doc fieldSpec={fieldSpec} />
</div> </div>
</> </>
); );

View File

@@ -1,9 +1,9 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import Button from '../Button' import InputButton from './InputButton'
import {MdDelete} from 'react-icons/md' import {MdDelete} from 'react-icons/md'
class FilterEditorBlock extends React.Component { export default class FilterEditorBlock extends React.Component {
static propTypes = { static propTypes = {
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
children: PropTypes.element.isRequired, children: PropTypes.element.isRequired,
@@ -12,12 +12,13 @@ class FilterEditorBlock extends React.Component {
render() { render() {
return <div className="maputnik-filter-editor-block"> return <div className="maputnik-filter-editor-block">
<div className="maputnik-filter-editor-block-action"> <div className="maputnik-filter-editor-block-action">
<Button <InputButton
className="maputnik-delete-filter" className="maputnik-delete-filter"
onClick={this.props.onDelete} onClick={this.props.onDelete}
title="Delete filter block"
> >
<MdDelete /> <MdDelete />
</Button> </InputButton>
</div> </div>
<div className="maputnik-filter-editor-block-content"> <div className="maputnik-filter-editor-block-content">
{this.props.children} {this.props.children}
@@ -26,4 +27,3 @@ class FilterEditorBlock extends React.Component {
} }
} }
export default FilterEditorBlock

View File

@@ -2,7 +2,7 @@ import React from 'react'
import IconBase from 'react-icon-base' import IconBase from 'react-icon-base'
export default class BackgroundIcon extends React.Component { export default class IconBackground extends React.Component {
render() { render() {
return ( return (
<IconBase viewBox="0 0 20 20" {...this.props}> <IconBase viewBox="0 0 20 20" {...this.props}>

View File

@@ -2,7 +2,7 @@ import React from 'react'
import IconBase from 'react-icon-base' import IconBase from 'react-icon-base'
export default class FillIcon extends React.Component { export default class IconCircle extends React.Component {
render() { render() {
return ( return (
<IconBase viewBox="0 0 20 20" {...this.props}> <IconBase viewBox="0 0 20 20" {...this.props}>

View File

@@ -2,7 +2,7 @@ import React from 'react'
import IconBase from 'react-icon-base' import IconBase from 'react-icon-base'
export default class FillIcon extends React.Component { export default class IconFill extends React.Component {
render() { render() {
return ( return (
<IconBase viewBox="0 0 20 20" {...this.props}> <IconBase viewBox="0 0 20 20" {...this.props}>

View File

@@ -0,0 +1,33 @@
import React from 'react'
import PropTypes from 'prop-types'
import IconLine from './IconLine.jsx'
import IconFill from './IconFill.jsx'
import IconSymbol from './IconSymbol.jsx'
import IconBackground from './IconBackground.jsx'
import IconCircle from './IconCircle.jsx'
import IconMissing from './IconMissing.jsx'
export default class IconLayer extends React.Component {
static propTypes = {
type: PropTypes.string.isRequired,
style: PropTypes.object,
}
render() {
const iconProps = { style: this.props.style }
switch(this.props.type) {
case 'fill-extrusion': return <IconBackground {...iconProps} />
case 'raster': return <IconFill {...iconProps} />
case 'hillshade': return <IconFill {...iconProps} />
case 'heatmap': return <IconFill {...iconProps} />
case 'fill': return <IconFill {...iconProps} />
case 'background': return <IconBackground {...iconProps} />
case 'line': return <IconLine {...iconProps} />
case 'symbol': return <IconSymbol {...iconProps} />
case 'circle': return <IconCircle {...iconProps} />
default: return <IconMissing {...iconProps} />
}
}
}

View File

@@ -2,7 +2,7 @@ import React from 'react'
import IconBase from 'react-icon-base' import IconBase from 'react-icon-base'
export default class FillIcon extends React.Component { export default class IconLine extends React.Component {
render() { render() {
return ( return (
<IconBase viewBox="0 0 20 20" {...this.props}> <IconBase viewBox="0 0 20 20" {...this.props}>

View File

@@ -2,7 +2,7 @@ import React from 'react'
import {MdPriorityHigh} from 'react-icons/md' import {MdPriorityHigh} from 'react-icons/md'
export default class MissingIcon extends React.Component { export default class IconMissing extends React.Component {
render() { render() {
return ( return (
<MdPriorityHigh {...this.props} /> <MdPriorityHigh {...this.props} />

View File

@@ -2,7 +2,7 @@ import React from 'react'
import IconBase from 'react-icon-base' import IconBase from 'react-icon-base'
export default class SymbolIcon extends React.Component { export default class IconSymbol extends React.Component {
render() { render() {
return ( return (
<IconBase viewBox="0 0 20 20" {...this.props}> <IconBase viewBox="0 0 20 20" {...this.props}>

View File

@@ -1,15 +1,16 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import StringInput from './StringInput' import InputString from './InputString'
import NumberInput from './NumberInput' import InputNumber from './InputNumber'
class ArrayInput extends React.Component { export default class FieldArray extends React.Component {
static propTypes = { static propTypes = {
value: PropTypes.array, value: PropTypes.array,
type: PropTypes.string, type: PropTypes.string,
length: PropTypes.number, length: PropTypes.number,
default: PropTypes.array, default: PropTypes.array,
onChange: PropTypes.func, onChange: PropTypes.func,
'aria-label': PropTypes.string,
} }
static defaultProps = { static defaultProps = {
@@ -82,28 +83,31 @@ class ArrayInput extends React.Component {
const inputs = Array(this.props.length).fill(null).map((_, i) => { const inputs = Array(this.props.length).fill(null).map((_, i) => {
if(this.props.type === 'number') { if(this.props.type === 'number') {
return <NumberInput return <InputNumber
key={i} key={i}
default={containsValues ? undefined : this.props.default[i]} default={containsValues ? undefined : this.props.default[i]}
value={value[i]} value={value[i]}
required={containsValues ? true : false} required={containsValues ? true : false}
onChange={this.changeValue.bind(this, i)} onChange={this.changeValue.bind(this, i)}
aria-label={this.props['aria-label'] || this.props.label}
/> />
} else { } else {
return <StringInput return <InputString
key={i} key={i}
default={containsValues ? undefined : this.props.default[i]} default={containsValues ? undefined : this.props.default[i]}
value={value[i]} value={value[i]}
required={containsValues ? true : false} required={containsValues ? true : false}
onChange={this.changeValue.bind(this, i)} onChange={this.changeValue.bind(this, i)}
aria-label={this.props['aria-label'] || this.props.label}
/> />
} }
}) })
return <div className="maputnik-array"> return (
{inputs} <div className="maputnik-array">
</div> {inputs}
</div>
)
} }
} }
export default ArrayInput

View File

@@ -6,12 +6,13 @@ import Autocomplete from 'react-autocomplete'
const MAX_HEIGHT = 140; const MAX_HEIGHT = 140;
class AutocompleteInput extends React.Component { export default class InputAutocomplete extends React.Component {
static propTypes = { static propTypes = {
value: PropTypes.string, value: PropTypes.string,
options: PropTypes.array, options: PropTypes.array,
onChange: PropTypes.func, onChange: PropTypes.func,
keepMenuWithinWindowBounds: PropTypes.bool keepMenuWithinWindowBounds: PropTypes.bool,
'aria-label': PropTypes.string,
} }
state = { state = {
@@ -66,6 +67,7 @@ class AutocompleteInput extends React.Component {
style: null style: null
}} }}
inputProps={{ inputProps={{
'aria-label': this.props['aria-label'],
className: "maputnik-string", className: "maputnik-string",
spellCheck: false spellCheck: false
}} }}
@@ -95,4 +97,4 @@ class AutocompleteInput extends React.Component {
} }
} }
export default AutocompleteInput

View File

@@ -2,7 +2,7 @@ import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import classnames from 'classnames' import classnames from 'classnames'
class Button extends React.Component { export default class InputButton extends React.Component {
static propTypes = { static propTypes = {
"data-wd-key": PropTypes.string, "data-wd-key": PropTypes.string,
"aria-label": PropTypes.string, "aria-label": PropTypes.string,
@@ -11,10 +11,16 @@ class Button extends React.Component {
className: PropTypes.string, className: PropTypes.string,
children: PropTypes.node, children: PropTypes.node,
disabled: PropTypes.bool, disabled: PropTypes.bool,
type: PropTypes.string,
id: PropTypes.string,
title: PropTypes.string,
} }
render() { render() {
return <button return <button
id={this.props.id}
title={this.props.title}
type={this.props.type}
onClick={this.props.onClick} onClick={this.props.onClick}
disabled={this.props.disabled} disabled={this.props.disabled}
aria-label={this.props["aria-label"]} aria-label={this.props["aria-label"]}
@@ -27,4 +33,3 @@ class Button extends React.Component {
} }
} }
export default Button

View File

@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
class CheckboxInput extends React.Component { export default class InputCheckbox extends React.Component {
static propTypes = { static propTypes = {
value: PropTypes.bool, value: PropTypes.bool,
style: PropTypes.object, style: PropTypes.object,
@@ -12,13 +12,18 @@ class CheckboxInput extends React.Component {
value: false, value: false,
} }
onChange = () => {
this.props.onChange(!this.props.value);
}
render() { render() {
return <label className="maputnik-checkbox-wrapper"> return <div className="maputnik-checkbox-wrapper">
<input <input
className="maputnik-checkbox" className="maputnik-checkbox"
type="checkbox" type="checkbox"
style={this.props.style} style={this.props.style}
onChange={e => this.props.onChange(!this.props.value)} onChange={this.onChange}
onClick={this.onChange}
checked={this.props.value} checked={this.props.value}
/> />
<div className="maputnik-checkbox-box"> <div className="maputnik-checkbox-box">
@@ -28,8 +33,7 @@ class CheckboxInput extends React.Component {
<path d='M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z' /> <path d='M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z' />
</svg> </svg>
</div> </div>
</label> </div>
} }
} }
export default CheckboxInput

View File

@@ -10,7 +10,7 @@ function formatColor(color) {
} }
/*** Number fields with support for min, max and units and documentation*/ /*** Number fields with support for min, max and units and documentation*/
class ColorField extends React.Component { export default class InputColor extends React.Component {
static propTypes = { static propTypes = {
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
name: PropTypes.string, name: PropTypes.string,
@@ -18,6 +18,7 @@ class ColorField extends React.Component {
doc: PropTypes.string, doc: PropTypes.string,
style: PropTypes.object, style: PropTypes.object,
default: PropTypes.string, default: PropTypes.string,
'aria-label': PropTypes.string,
} }
state = { state = {
@@ -116,7 +117,9 @@ class ColorField extends React.Component {
{this.state.pickerOpened && picker} {this.state.pickerOpened && picker}
<div className="maputnik-color-swatch" style={swatchStyle}></div> <div className="maputnik-color-swatch" style={swatchStyle}></div>
<input <input
aria-label={this.props['aria-label']}
spellCheck="false" spellCheck="false"
autoComplete="off"
className="maputnik-color" className="maputnik-color"
ref={(input) => this.colorInput = input} ref={(input) => this.colorInput = input}
onClick={this.togglePicker} onClick={this.togglePicker}
@@ -130,4 +133,3 @@ class ColorField extends React.Component {
} }
} }
export default ColorField

View File

@@ -1,15 +1,16 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import StringInput from './StringInput' import InputString from './InputString'
import NumberInput from './NumberInput' import InputNumber from './InputNumber'
import Button from '../Button' import InputButton from './InputButton'
import {MdDelete} from 'react-icons/md' import {MdDelete} from 'react-icons/md'
import DocLabel from '../fields/DocLabel' import FieldDocLabel from './FieldDocLabel'
import EnumInput from '../inputs/SelectInput' import InputEnum from './InputEnum'
import capitalize from 'lodash.capitalize' import capitalize from 'lodash.capitalize'
import InputUrl from './InputUrl'
class DynamicArrayInput extends React.Component { export default class FieldDynamicArray extends React.Component {
static propTypes = { static propTypes = {
value: PropTypes.array, value: PropTypes.array,
type: PropTypes.string, type: PropTypes.string,
@@ -17,10 +18,10 @@ class DynamicArrayInput extends React.Component {
onChange: PropTypes.func, onChange: PropTypes.func,
style: PropTypes.object, style: PropTypes.object,
fieldSpec: PropTypes.object, fieldSpec: PropTypes.object,
'aria-label': PropTypes.string,
} }
changeValue(idx, newValue) { changeValue(idx, newValue) {
console.log(idx, newValue)
const values = this.values.slice(0) const values = this.values.slice(0)
values[idx] = newValue values[idx] = newValue
this.props.onChange(values) this.props.onChange(values)
@@ -35,6 +36,9 @@ class DynamicArrayInput extends React.Component {
if (this.props.type === 'number') { if (this.props.type === 'number') {
values.push(0) values.push(0)
} }
else if (this.props.type === 'url') {
values.push("");
}
else if (this.props.type === 'enum') { else if (this.props.type === 'enum') {
const {fieldSpec} = this.props; const {fieldSpec} = this.props;
const defaultValue = Object.keys(fieldSpec.values)[0]; const defaultValue = Object.keys(fieldSpec.values)[0];
@@ -50,32 +54,41 @@ class DynamicArrayInput extends React.Component {
const values = this.values.slice(0) const values = this.values.slice(0)
values.splice(valueIdx, 1) values.splice(valueIdx, 1)
this.props.onChange(values) this.props.onChange(values.length > 0 ? values : undefined);
} }
render() { render() {
const inputs = this.values.map((v, i) => { const inputs = this.values.map((v, i) => {
const deleteValueBtn= <DeleteValueButton onClick={this.deleteValue.bind(this, i)} /> const deleteValueBtn= <DeleteValueInputButton onClick={this.deleteValue.bind(this, i)} />
let input; let input;
if (this.props.type === 'number') { if(this.props.type === 'url') {
input = <NumberInput input = <InputUrl
value={v} value={v}
onChange={this.changeValue.bind(this, i)} onChange={this.changeValue.bind(this, i)}
aria-label={this.props['aria-label'] || this.props.label}
/>
}
else if (this.props.type === 'number') {
input = <InputNumber
value={v}
onChange={this.changeValue.bind(this, i)}
aria-label={this.props['aria-label'] || this.props.label}
/> />
} }
else if (this.props.type === 'enum') { else if (this.props.type === 'enum') {
const options = Object.keys(this.props.fieldSpec.values).map(v => [v, capitalize(v)]); const options = Object.keys(this.props.fieldSpec.values).map(v => [v, capitalize(v)]);
input = <InputEnum
input = <EnumInput
options={options} options={options}
value={v} value={v}
onChange={this.changeValue.bind(this, i)} onChange={this.changeValue.bind(this, i)}
aria-label={this.props['aria-label'] || this.props.label}
/> />
} }
else { else {
input = <StringInput input = <InputString
value={v} value={v}
onChange={this.changeValue.bind(this, i)} onChange={this.changeValue.bind(this, i)}
aria-label={this.props['aria-label'] || this.props.label}
/> />
} }
@@ -93,34 +106,36 @@ class DynamicArrayInput extends React.Component {
</div> </div>
}) })
return <div className="maputnik-array"> return (
{inputs} <div className="maputnik-array">
<Button {inputs}
className="maputnik-array-add-value" <InputButton
onClick={this.addValue} className="maputnik-array-add-value"
> onClick={this.addValue}
Add value >
</Button> Add value
</div> </InputButton>
</div>
);
} }
} }
class DeleteValueButton extends React.Component { class DeleteValueInputButton extends React.Component {
static propTypes = { static propTypes = {
onClick: PropTypes.func, onClick: PropTypes.func,
} }
render() { render() {
return <Button return <InputButton
className="maputnik-delete-stop" className="maputnik-delete-stop"
onClick={this.props.onClick} onClick={this.props.onClick}
title="Remove array item"
> >
<DocLabel <FieldDocLabel
label={<MdDelete />} label={<MdDelete />}
doc={"Remove array entry."} doc={"Remove array item."}
/> />
</Button> </InputButton>
} }
} }
export default DynamicArrayInput

View File

@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import SelectInput from '../inputs/SelectInput' import InputSelect from './InputSelect'
import MultiButtonInput from '../inputs/MultiButtonInput' import InputMultiInput from './InputMultiInput'
function optionsLabelLength(options) { function optionsLabelLength(options) {
@@ -13,33 +13,37 @@ function optionsLabelLength(options) {
} }
class EnumInput extends React.Component { export default class InputEnum extends React.Component {
static propTypes = { static propTypes = {
"data-wd-key": PropTypes.string, "data-wd-key": PropTypes.string,
value: PropTypes.string, value: PropTypes.string,
style: PropTypes.object, style: PropTypes.object,
default: PropTypes.string, default: PropTypes.string,
name: PropTypes.string,
onChange: PropTypes.func, onChange: PropTypes.func,
options: PropTypes.array, options: PropTypes.array,
'aria-label': PropTypes.string,
} }
render() { render() {
const {options, value, onChange} = this.props; const {options, value, onChange, name, label} = this.props;
if(options.length <= 3 && optionsLabelLength(options) <= 20) { if(options.length <= 3 && optionsLabelLength(options) <= 20) {
return <MultiButtonInput return <InputMultiInput
name={name}
options={options} options={options}
value={value || this.props.default} value={value || this.props.default}
onChange={onChange} onChange={onChange}
aria-label={this.props['aria-label'] || label}
/> />
} else { } else {
return <SelectInput return <InputSelect
options={options} options={options}
value={value || this.props.default} value={value || this.props.default}
onChange={onChange} onChange={onChange}
aria-label={this.props['aria-label'] || label}
/> />
} }
} }
} }
export default EnumInput

View File

@@ -0,0 +1,61 @@
import React from 'react'
import PropTypes from 'prop-types'
import InputAutocomplete from './InputAutocomplete'
export default class FieldFont extends React.Component {
static propTypes = {
value: PropTypes.array,
default: PropTypes.array,
fonts: PropTypes.array,
style: PropTypes.object,
onChange: PropTypes.func.isRequired,
'aria-label': PropTypes.string,
}
static defaultProps = {
fonts: []
}
get values() {
const out = this.props.value || this.props.default || [];
// Always put a "" in the last field to you can keep adding entries
if (out[out.length-1] !== ""){
return out.concat("");
}
else {
return out;
}
}
changeFont(idx, newValue) {
const changedValues = this.values.slice(0)
changedValues[idx] = newValue
const filteredValues = changedValues
.filter(v => v !== undefined)
.filter(v => v !== "")
this.props.onChange(filteredValues);
}
render() {
const inputs = this.values.map((value, i) => {
return <li
key={i}
>
<InputAutocomplete
aria-label={this.props['aria-label'] || this.props.name}
value={value}
options={this.props.fonts.map(f => [f, f])}
onChange={this.changeFont.bind(this, i)}
/>
</li>
})
return (
<ul className="maputnik-font">
{inputs}
</ul>
);
}
}

View File

@@ -2,8 +2,8 @@ import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import classnames from 'classnames'; import classnames from 'classnames';
import InputBlock from '../inputs/InputBlock' import Block from './Block'
import StringInput from '../inputs/StringInput' import FieldString from './FieldString'
import CodeMirror from 'codemirror'; import CodeMirror from 'codemirror';
import 'codemirror/mode/javascript/javascript' import 'codemirror/mode/javascript/javascript'
@@ -16,7 +16,7 @@ import stringifyPretty from 'json-stringify-pretty-compact'
import '../util/codemirror-mgl'; import '../util/codemirror-mgl';
class JSONEditor extends React.Component { export default class InputJson extends React.Component {
static propTypes = { static propTypes = {
layer: PropTypes.any.isRequired, layer: PropTypes.any.isRequired,
maxHeight: PropTypes.number, maxHeight: PropTypes.number,
@@ -51,9 +51,11 @@ class JSONEditor extends React.Component {
} }
constructor(props) { constructor(props) {
super(props) super(props);
this._keyEvent = "keyboard";
this.state = { this.state = {
isEditing: false, isEditing: false,
showMessage: false,
prevValue: this.props.getValue(this.props.layer), prevValue: this.props.getValue(this.props.layer),
}; };
} }
@@ -82,17 +84,24 @@ class JSONEditor extends React.Component {
this._doc.on('blur', this.onBlur); this._doc.on('blur', this.onBlur);
} }
onFocus = () => { onPointerDown = (cm, e) => {
this._keyEvent = "pointer";
}
onFocus = (cm, e) => {
this.props.onFocus(); this.props.onFocus();
this.setState({ this.setState({
isEditing: true isEditing: true,
showMessage: (this._keyEvent === "keyboard"),
}); });
} }
onBlur = () => { onBlur = () => {
this._keyEvent = "keyboard";
this.props.onBlur(); this.props.onBlur();
this.setState({ this.setState({
isEditing: false isEditing: false,
showMessage: false,
}); });
} }
@@ -145,17 +154,21 @@ class JSONEditor extends React.Component {
} }
render() { render() {
const {showMessage} = this.state;
const style = {}; const style = {};
if (this.props.maxHeight) { if (this.props.maxHeight) {
style.maxHeight = this.props.maxHeight; style.maxHeight = this.props.maxHeight;
} }
return <div return <div className="JSONEditor" onPointerDown={this.onPointerDown} aria-hidden="true">
className={classnames("codemirror-container", this.props.className)} <div className={classnames("JSONEditor__message", {"JSONEditor__message--on": showMessage})}>
ref={(el) => this._el = el} Press <kbd>ESC</kbd> to lose focus
style={style} </div>
/> <div
className={classnames("codemirror-container", this.props.className)}
ref={(el) => this._el = el}
style={style}
/>
</div>
} }
} }
export default JSONEditor

View File

@@ -0,0 +1,42 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import InputButton from './InputButton'
export default class InputMultiInput extends React.Component {
static propTypes = {
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
options: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired,
}
render() {
let options = this.props.options
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-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>
}
}

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types'
let IDX = 0; let IDX = 0;
class NumberInput extends React.Component { export default class InputNumber extends React.Component {
static propTypes = { static propTypes = {
value: PropTypes.number, value: PropTypes.number,
default: PropTypes.number, default: PropTypes.number,
@@ -14,6 +14,7 @@ class NumberInput extends React.Component {
rangeStep: PropTypes.number, rangeStep: PropTypes.number,
wdKey: PropTypes.string, wdKey: PropTypes.string,
required: PropTypes.bool, required: PropTypes.bool,
"aria-label": PropTypes.string,
} }
static defaultProps = { static defaultProps = {
@@ -31,7 +32,7 @@ class NumberInput extends React.Component {
} }
static getDerivedStateFromProps(props, state) { static getDerivedStateFromProps(props, state) {
if (!state.editing) { if (!state.editing && props.value !== state.value) {
return { return {
value: props.value, value: props.value,
dirtyValue: props.value, dirtyValue: props.value,
@@ -49,12 +50,17 @@ class NumberInput extends React.Component {
if(this.isValid(value) && hasChanged) { if(this.isValid(value) && hasChanged) {
this.props.onChange(value) this.props.onChange(value)
this.setState({ this.setState({
dirtyValue: newValue, value: newValue,
});
}
else if (!this.isValid(value) && hasChanged) {
this.setState({
value: undefined,
}); });
} }
this.setState({ this.setState({
value: newValue, dirtyValue: newValue === "" ? undefined : newValue,
}) })
} }
@@ -87,11 +93,13 @@ class NumberInput extends React.Component {
} }
// If set value is invalid fall back to the last valid value from props or at last resort the default value // If set value is invalid fall back to the last valid value from props or at last resort the default value
if(!this.isValid(this.state.value)) { if (!this.isValid(this.state.value)) {
if(this.isValid(this.props.value)) { if(this.isValid(this.props.value)) {
this.changeValue(this.props.value) this.changeValue(this.props.value)
this.setState({dirtyValue: this.props.value});
} else { } else {
this.changeValue(undefined); this.changeValue(undefined);
this.setState({dirtyValue: undefined});
} }
} }
} }
@@ -144,8 +152,15 @@ class NumberInput extends React.Component {
this.props.min !== undefined && this.props.max !== undefined && this.props.min !== undefined && this.props.max !== undefined &&
this.props.allowRange this.props.allowRange
) { ) {
const dirtyValue = this.state.dirtyValue === undefined ? this.props.default : this.state.dirtyValue const value = this.state.editing ? this.state.dirtyValue : this.state.value;
const value = this.state.value === undefined ? "" : this.state.value; const defaultValue = this.props.default === undefined ? "" : this.props.default;
let inputValue;
if (this.state.editingRange) {
inputValue = this.state.value;
}
else {
inputValue = value;
}
return <div className="maputnik-number-container"> return <div className="maputnik-number-container">
<input <input
@@ -156,21 +171,24 @@ class NumberInput extends React.Component {
min={this.props.min} min={this.props.min}
step="any" step="any"
spellCheck="false" spellCheck="false"
value={dirtyValue} value={value === undefined ? defaultValue : value}
aria-hidden="true"
onChange={this.onChangeRange} onChange={this.onChangeRange}
onKeyDown={() => { onKeyDown={() => {
this._keyboardEvent = true; this._keyboardEvent = true;
}} }}
onPointerDown={() => { onPointerDown={() => {
this.setState({editing: true}); this.setState({editing: true, editingRange: true});
}} }}
onPointerUp={() => { onPointerUp={() => {
// Safari doesn't get onBlur event // Safari doesn't get onBlur event
this.setState({editing: false}); this.setState({editing: false, editingRange: false});
}} }}
onBlur={() => { onBlur={() => {
this.setState({editing: false}); this.setState({
editing: false,
editingRange: false,
dirtyValue: this.state.value,
});
}} }}
/> />
<input <input
@@ -179,25 +197,33 @@ class NumberInput extends React.Component {
spellCheck="false" spellCheck="false"
className="maputnik-number" className="maputnik-number"
placeholder={this.props.default} placeholder={this.props.default}
value={value} value={inputValue === undefined ? "" : inputValue}
onChange={e => { onFocus={e => {
if (!this.state.editing) { this.setState({editing: true});
this.changeValue(e.target.value); }}
} onChange={e => {
this.changeValue(e.target.value);
}}
onBlur={e => {
this.setState({editing: false});
this.resetValue()
}} }}
onBlur={this.resetValue}
/> />
</div> </div>
} }
else { else {
const value = this.state.value === undefined ? "" : this.state.value; const value = this.state.editing ? this.state.dirtyValue : this.state.value;
return <input return <input
aria-label={this.props['aria-label']}
spellCheck="false" spellCheck="false"
className="maputnik-number" className="maputnik-number"
placeholder={this.props.default} placeholder={this.props.default}
value={value} value={value === undefined ? "" : value}
onChange={e => this.changeValue(e.target.value)} onChange={e => this.changeValue(e.target.value)}
onFocus={() => {
this.setState({editing: true});
}}
onBlur={this.resetValue} onBlur={this.resetValue}
required={this.props.required} required={this.props.required}
/> />
@@ -205,4 +231,4 @@ class NumberInput extends React.Component {
} }
} }
export default NumberInput

View File

@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
class SelectInput extends React.Component { export default class InputSelect extends React.Component {
static propTypes = { static propTypes = {
value: PropTypes.string.isRequired, value: PropTypes.string.isRequired,
"data-wd-key": PropTypes.string, "data-wd-key": PropTypes.string,
@@ -9,6 +9,7 @@ class SelectInput extends React.Component {
style: PropTypes.object, style: PropTypes.object,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
title: PropTypes.string, title: PropTypes.string,
'aria-label': PropTypes.string,
} }
@@ -25,10 +26,11 @@ class SelectInput extends React.Component {
title={this.props.title} title={this.props.title}
value={this.props.value} value={this.props.value}
onChange={e => this.props.onChange(e.target.value)} onChange={e => this.props.onChange(e.target.value)}
aria-label={this.props['aria-label']}
> >
{ options.map(([val, label]) => <option key={val} value={val}>{label}</option>) } { options.map(([val, label]) => <option key={val} value={val}>{label}</option>) }
</select> </select>
} }
} }
export default SelectInput

View File

@@ -1,17 +1,17 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import ColorField from './ColorField' import InputColor from './InputColor'
import NumberInput from '../inputs/NumberInput' import InputNumber from './InputNumber'
import CheckboxInput from '../inputs/CheckboxInput' import InputCheckbox from './InputCheckbox'
import StringInput from '../inputs/StringInput' import InputString from './InputString'
import SelectInput from '../inputs/SelectInput' import InputSelect from './InputSelect'
import MultiButtonInput from '../inputs/MultiButtonInput' import InputMultiInput from './InputMultiInput'
import ArrayInput from '../inputs/ArrayInput' import InputArray from './InputArray'
import DynamicArrayInput from '../inputs/DynamicArrayInput' import InputDynamicArray from './InputDynamicArray'
import FontInput from '../inputs/FontInput' import InputFont from './InputFont'
import IconInput from '../inputs/IconInput' import InputAutocomplete from './InputAutocomplete'
import EnumInput from '../inputs/EnumInput' import InputEnum from './InputEnum'
import capitalize from 'lodash.capitalize' import capitalize from 'lodash.capitalize'
const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image'] const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image']
@@ -48,21 +48,27 @@ export default class SpecField extends React.Component {
]), ]),
/** Override the style of the field */ /** Override the style of the field */
style: PropTypes.object, style: PropTypes.object,
'aria-label': PropTypes.string,
} }
render() { render() {
const commonProps = { const commonProps = {
error: this.props.error,
fieldSpec: this.props.fieldSpec,
label: this.props.label,
action: this.props.action,
style: this.props.style, style: this.props.style,
value: this.props.value, value: this.props.value,
default: this.props.fieldSpec.default, default: this.props.fieldSpec.default,
name: this.props.fieldName, name: this.props.fieldName,
onChange: newValue => this.props.onChange(this.props.fieldName, newValue) onChange: newValue => this.props.onChange(this.props.fieldName, newValue),
'aria-label': this.props['aria-label'],
} }
function childNodes() { function childNodes() {
switch(this.props.fieldSpec.type) { switch(this.props.fieldSpec.type) {
case 'number': return ( case 'number': return (
<NumberInput <InputNumber
{...commonProps} {...commonProps}
min={this.props.fieldSpec.minimum} min={this.props.fieldSpec.minimum}
max={this.props.fieldSpec.maximum} max={this.props.fieldSpec.maximum}
@@ -71,48 +77,49 @@ export default class SpecField extends React.Component {
case 'enum': case 'enum':
const options = Object.keys(this.props.fieldSpec.values).map(v => [v, capitalize(v)]) const options = Object.keys(this.props.fieldSpec.values).map(v => [v, capitalize(v)])
return <EnumInput return <InputEnum
{...commonProps} {...commonProps}
options={options} options={options}
/> />
case 'resolvedImage': case 'resolvedImage':
case 'formatted': case 'formatted':
case 'string': case 'string':
if(iconProperties.indexOf(this.props.fieldName) >= 0) { if (iconProperties.indexOf(this.props.fieldName) >= 0) {
return <IconInput const options = this.props.fieldSpec.values || [];
return <InputAutocomplete
{...commonProps} {...commonProps}
icons={this.props.fieldSpec.values} options={options.map(f => [f, f])}
/> />
} else { } else {
return <StringInput return <InputString
{...commonProps} {...commonProps}
/> />
} }
case 'color': return ( case 'color': return (
<ColorField <InputColor
{...commonProps} {...commonProps}
/> />
) )
case 'boolean': return ( case 'boolean': return (
<CheckboxInput <InputCheckbox
{...commonProps} {...commonProps}
/> />
) )
case 'array': case 'array':
if(this.props.fieldName === 'text-font') { if(this.props.fieldName === 'text-font') {
return <FontInput return <InputFont
{...commonProps} {...commonProps}
fonts={this.props.fieldSpec.values} fonts={this.props.fieldSpec.values}
/> />
} else { } else {
if (this.props.fieldSpec.length) { if (this.props.fieldSpec.length) {
return <ArrayInput return <InputArray
{...commonProps} {...commonProps}
type={this.props.fieldSpec.value} type={this.props.fieldSpec.value}
length={this.props.fieldSpec.length} length={this.props.fieldSpec.length}
/> />
} else { } else {
return <DynamicArrayInput return <InputDynamicArray
{...commonProps} {...commonProps}
fieldSpec={this.props.fieldSpec} fieldSpec={this.props.fieldSpec}
type={this.props.fieldSpec.value} type={this.props.fieldSpec.value}
@@ -125,7 +132,7 @@ export default class SpecField extends React.Component {
return ( return (
<div data-wd-key={"spec-field:"+this.props.fieldName}> <div data-wd-key={"spec-field:"+this.props.fieldName}>
{childNodes.call(this)} {childNodes.call(this)}
</div> </div>
); );
} }

View File

@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
class StringInput extends React.Component { export default class InputString extends React.Component {
static propTypes = { static propTypes = {
"data-wd-key": PropTypes.string, "data-wd-key": PropTypes.string,
value: PropTypes.string, value: PropTypes.string,
@@ -13,6 +13,7 @@ class StringInput extends React.Component {
required: PropTypes.bool, required: PropTypes.bool,
disabled: PropTypes.bool, disabled: PropTypes.bool,
spellCheck: PropTypes.bool, spellCheck: PropTypes.bool,
'aria-label': PropTypes.string,
} }
static defaultProps = { static defaultProps = {
@@ -33,6 +34,7 @@ class StringInput extends React.Component {
value: props.value value: props.value
}; };
} }
return {};
} }
render() { render() {
@@ -58,6 +60,7 @@ class StringInput extends React.Component {
} }
return React.createElement(tag, { return React.createElement(tag, {
"aria-label": this.props["aria-label"],
"data-wd-key": this.props["data-wd-key"], "data-wd-key": this.props["data-wd-key"],
spellCheck: this.props.hasOwnProperty("spellCheck") ? this.props.spellCheck : !(tag === "input"), spellCheck: this.props.hasOwnProperty("spellCheck") ? this.props.spellCheck : !(tag === "input"),
disabled: this.props.disabled, disabled: this.props.disabled,
@@ -79,9 +82,14 @@ class StringInput extends React.Component {
this.props.onChange(this.state.value); this.props.onChange(this.state.value);
} }
}, },
onKeyDown: (e) => {
if (e.keyCode === 13) {
this.props.onChange(this.state.value);
}
},
required: this.props.required, required: this.props.required,
}); });
} }
} }
export default StringInput

View File

@@ -1,10 +1,14 @@
import React from 'react' import React, {Fragment} from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import StringInput from './StringInput' import InputString from './InputString'
import SmallError from '../util/SmallError' import SmallError from './SmallError'
function validate (url) { function validate (url) {
if (url === "") {
return;
}
let error; let error;
const getProtocol = (url) => { const getProtocol = (url) => {
try { try {
@@ -16,7 +20,20 @@ function validate (url) {
} }
}; };
const protocol = getProtocol(url); const protocol = getProtocol(url);
if ( const isSsl = window.location.protocol === "https:";
if (!protocol) {
error = (
<SmallError>
Must provide protocol {
isSsl
? <code>https://</code>
: <><code>http://</code> or <code>https://</code></>
}
</SmallError>
);
}
else if (
protocol && protocol &&
protocol === "http:" && protocol === "http:" &&
window.location.protocol === "https:" window.location.protocol === "https:"
@@ -31,7 +48,7 @@ function validate (url) {
return error; return error;
} }
class UrlInput extends React.Component { export default class FieldUrl extends React.Component {
static propTypes = { static propTypes = {
"data-wd-key": PropTypes.string, "data-wd-key": PropTypes.string,
value: PropTypes.string, value: PropTypes.string,
@@ -41,6 +58,7 @@ class UrlInput extends React.Component {
onInput: PropTypes.func, onInput: PropTypes.func,
multi: PropTypes.bool, multi: PropTypes.bool,
required: PropTypes.bool, required: PropTypes.bool,
'aria-label': PropTypes.string,
} }
static defaultProps = { static defaultProps = {
@@ -61,12 +79,21 @@ class UrlInput extends React.Component {
this.props.onInput(url); this.props.onInput(url);
} }
onChange = (url) => {
this.setState({
error: validate(url)
});
this.props.onChange(url);
}
render () { render () {
return ( return (
<div> <div>
<StringInput <InputString
{...this.props} {...this.props}
onInput={this.onInput} onInput={this.onInput}
onChange={this.onChange}
aria-label={this.props['aria-label']}
/> />
{this.state.error} {this.state.error}
</div> </div>
@@ -74,4 +101,3 @@ class UrlInput extends React.Component {
} }
} }
export default UrlInput

View File

@@ -2,23 +2,24 @@ import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Wrapper, Button, Menu, MenuItem } from 'react-aria-menubutton' import { Wrapper, Button, Menu, MenuItem } from 'react-aria-menubutton'
import JSONEditor from './JSONEditor' import FieldJson from './FieldJson'
import FilterEditor from '../filter/FilterEditor' import FilterEditor from './FilterEditor'
import PropertyGroup from '../fields/PropertyGroup' import PropertyGroup from './PropertyGroup'
import LayerEditorGroup from './LayerEditorGroup' import LayerEditorGroup from './LayerEditorGroup'
import LayerTypeBlock from './LayerTypeBlock' import FieldType from './FieldType'
import LayerIdBlock from './LayerIdBlock' import FieldId from './FieldId'
import MinZoomBlock from './MinZoomBlock' import FieldMinZoom from './FieldMinZoom'
import MaxZoomBlock from './MaxZoomBlock' import FieldMaxZoom from './FieldMaxZoom'
import CommentBlock from './CommentBlock' import FieldComment from './FieldComment'
import LayerSourceBlock from './LayerSourceBlock' import FieldSource from './FieldSource'
import LayerSourceLayerBlock from './LayerSourceLayerBlock' import FieldSourceLayer from './FieldSourceLayer'
import {Accordion} from 'react-accessible-accordion'; import {Accordion} from 'react-accessible-accordion';
import {MdMoreVert} from 'react-icons/md' import {MdMoreVert} from 'react-icons/md'
import { changeType, changeProperty } from '../../libs/layer' import { changeType, changeProperty } from '../libs/layer'
import layout from '../../config/layout.json' import layout from '../config/layout.json'
import {formatLayerId} from '../util/format';
function getLayoutForType (type) { function getLayoutForType (type) {
@@ -108,7 +109,10 @@ export default class LayerEditor extends React.Component {
} }
changeProperty(group, property, newValue) { changeProperty(group, property, newValue) {
this.props.onLayerChanged(changeProperty(this.props.layer, group, property, newValue)) this.props.onLayerChanged(
this.props.layerIndex,
changeProperty(this.props.layer, group, property, newValue)
)
} }
onGroupToggle(groupTitle, active) { onGroupToggle(groupTitle, active) {
@@ -148,44 +152,47 @@ export default class LayerEditor extends React.Component {
switch(type) { switch(type) {
case 'layer': return <div> case 'layer': return <div>
<LayerIdBlock <FieldId
value={this.props.layer.id} value={this.props.layer.id}
wdKey="layer-editor.layer-id" wdKey="layer-editor.layer-id"
error={errorData.id} error={errorData.id}
onChange={newId => this.props.onLayerIdChange(this.props.layer.id, newId)} onChange={newId => this.props.onLayerIdChange(this.props.layerIndex, this.props.layer.id, newId)}
/> />
<LayerTypeBlock <FieldType
disabled={true} disabled={true}
error={errorData.type} error={errorData.type}
value={this.props.layer.type} value={this.props.layer.type}
onChange={newType => this.props.onLayerChanged(changeType(this.props.layer, newType))} onChange={newType => this.props.onLayerChanged(
this.props.layerIndex,
changeType(this.props.layer, newType)
)}
/> />
{this.props.layer.type !== 'background' && <LayerSourceBlock {this.props.layer.type !== 'background' && <FieldSource
error={errorData.sources} error={errorData.source}
sourceIds={Object.keys(this.props.sources)} sourceIds={Object.keys(this.props.sources)}
value={this.props.layer.source} value={this.props.layer.source}
onChange={v => this.changeProperty(null, 'source', v)} onChange={v => this.changeProperty(null, 'source', v)}
/> />
} }
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.props.layer.type) < 0 && {['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.props.layer.type) < 0 &&
<LayerSourceLayerBlock <FieldSourceLayer
error={errorData['source-layer']} error={errorData['source-layer']}
sourceLayerIds={sourceLayerIds} sourceLayerIds={sourceLayerIds}
value={this.props.layer['source-layer']} value={this.props.layer['source-layer']}
onChange={v => this.changeProperty(null, 'source-layer', v)} onChange={v => this.changeProperty(null, 'source-layer', v)}
/> />
} }
<MinZoomBlock <FieldMinZoom
error={errorData.minzoom} error={errorData.minzoom}
value={this.props.layer.minzoom} value={this.props.layer.minzoom}
onChange={v => this.changeProperty(null, 'minzoom', v)} onChange={v => this.changeProperty(null, 'minzoom', v)}
/> />
<MaxZoomBlock <FieldMaxZoom
error={errorData.maxzoom} error={errorData.maxzoom}
value={this.props.layer.maxzoom} value={this.props.layer.maxzoom}
onChange={v => this.changeProperty(null, 'maxzoom', v)} onChange={v => this.changeProperty(null, 'maxzoom', v)}
/> />
<CommentBlock <FieldComment
error={errorData.comment} error={errorData.comment}
value={comment} value={comment}
onChange={v => this.changeProperty('metadata', 'maputnik:comment', v == "" ? undefined : v)} onChange={v => this.changeProperty('metadata', 'maputnik:comment', v == "" ? undefined : v)}
@@ -201,17 +208,24 @@ export default class LayerEditor extends React.Component {
/> />
</div> </div>
</div> </div>
case 'properties': return <PropertyGroup case 'properties':
errors={errorData} return <PropertyGroup
layer={this.props.layer} errors={errorData}
groupFields={fields} layer={this.props.layer}
spec={this.props.spec} groupFields={fields}
onChange={this.changeProperty.bind(this)} spec={this.props.spec}
/> onChange={this.changeProperty.bind(this)}
case 'jsoneditor': return <JSONEditor />
layer={this.props.layer} case 'jsoneditor':
onChange={this.props.onLayerChanged} return <FieldJson
/> layer={this.props.layer}
onChange={(layer) => {
this.props.onLayerChanged(
this.props.layerIndex,
layer
);
}}
/>
} }
} }
@@ -247,15 +261,15 @@ export default class LayerEditor extends React.Component {
const items = { const items = {
delete: { delete: {
text: "Delete", text: "Delete",
handler: () => this.props.onLayerDestroy(this.props.layer.id) handler: () => this.props.onLayerDestroy(this.props.layerIndex)
}, },
duplicate: { duplicate: {
text: "Duplicate", text: "Duplicate",
handler: () => this.props.onLayerCopy(this.props.layer.id) handler: () => this.props.onLayerCopy(this.props.layerIndex)
}, },
hide: { hide: {
text: (layout.visibility === "none") ? "Show" : "Hide", text: (layout.visibility === "none") ? "Show" : "Hide",
handler: () => this.props.onLayerVisibilityToggle(this.props.layer.id) handler: () => this.props.onLayerVisibilityToggle(this.props.layerIndex)
}, },
moveLayerUp: { moveLayerUp: {
text: "Move layer up", text: "Move layer up",
@@ -276,12 +290,14 @@ export default class LayerEditor extends React.Component {
items[id].handler(); items[id].handler();
} }
return <div className="maputnik-layer-editor" return <section className="maputnik-layer-editor"
> role="main"
aria-label="Layer editor"
>
<header> <header>
<div className="layer-header"> <div className="layer-header">
<h2 className="layer-header__title"> <h2 className="layer-header__title">
Layer: {this.props.layer.id} Layer: {formatLayerId(this.props.layer.id)}
</h2> </h2>
<div className="layer-header__info"> <div className="layer-header__info">
<Wrapper <Wrapper
@@ -289,7 +305,7 @@ export default class LayerEditor extends React.Component {
onSelection={handleSelection} onSelection={handleSelection}
closeOnSelection={false} closeOnSelection={false}
> >
<Button className='more-menu__button'> <Button id="skip-target-layer-editor" className='more-menu__button' title="Layer options">
<MdMoreVert className="more-menu__button__svg" /> <MdMoreVert className="more-menu__button__svg" />
</Button> </Button>
<Menu> <Menu>
@@ -316,6 +332,6 @@ export default class LayerEditor extends React.Component {
> >
{groups} {groups}
</Accordion> </Accordion>
</div> </section>
} }
} }

View File

@@ -5,7 +5,7 @@ import lodash from 'lodash';
import LayerListGroup from './LayerListGroup' import LayerListGroup from './LayerListGroup'
import LayerListItem from './LayerListItem' import LayerListItem from './LayerListItem'
import AddModal from '../modals/AddModal' import ModalAdd from './ModalAdd'
import {SortableContainer} from 'react-sortable-hoc'; import {SortableContainer} from 'react-sortable-hoc';
@@ -35,6 +35,8 @@ function findClosestCommonPrefix(layers, idx) {
return closestIdx return closestIdx
} }
let UID = 0;
// List of collapsible layer editors // List of collapsible layer editors
class LayerListContainer extends React.Component { class LayerListContainer extends React.Component {
static propTypes = {...layerListPropTypes} static propTypes = {...layerListPropTypes}
@@ -42,16 +44,28 @@ class LayerListContainer extends React.Component {
onLayerSelect: () => {}, onLayerSelect: () => {},
} }
state = { constructor(props) {
collapsedGroups: {}, super(props);
areAllGroupsExpanded: false, this.selectedItemRef = React.createRef();
isOpen: { this.scrollContainerRef = React.createRef();
add: false, this.state = {
collapsedGroups: {},
areAllGroupsExpanded: false,
keys: {
add: UID++,
},
isOpen: {
add: false,
}
} }
} }
toggleModal(modalName) { toggleModal(modalName) {
this.setState({ this.setState({
keys: {
...this.state.keys,
[modalName]: UID++,
},
isOpen: { isOpen: {
...this.state.isOpen, ...this.state.isOpen,
[modalName]: !this.state.isOpen[modalName] [modalName]: !this.state.isOpen[modalName]
@@ -86,9 +100,18 @@ class LayerListContainer extends React.Component {
groupedLayers() { groupedLayers() {
const groups = [] const groups = []
const layerIdCount = new Map();
for (let i = 0; i < this.props.layers.length; i++) { for (let i = 0; i < this.props.layers.length; i++) {
const origLayer = this.props.layers[i];
const previousLayer = this.props.layers[i-1] const previousLayer = this.props.layers[i-1]
const layer = this.props.layers[i] layerIdCount.set(origLayer.id,
layerIdCount.has(origLayer.id) ? layerIdCount.get(origLayer.id) + 1 : 0
);
const layer = {
...origLayer,
key: `layers-list-${origLayer.id}-${layerIdCount.get(origLayer.id)}`,
}
if(previousLayer && layerPrefix(previousLayer.id) == layerPrefix(layer.id)) { if(previousLayer && layerPrefix(previousLayer.id) == layerPrefix(layer.id)) {
const lastGroup = groups[groups.length - 1] const lastGroup = groups[groups.length - 1]
lastGroup.push(layer) lastGroup.push(layer)
@@ -161,16 +184,39 @@ class LayerListContainer extends React.Component {
return propsChanged; return propsChanged;
} }
componentDidUpdate (prevProps) {
if (prevProps.selectedLayerIndex !== this.props.selectedLayerIndex) {
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() { render() {
const listItems = [] const listItems = []
let idx = 0 let idx = 0
this.groupedLayers().forEach(layers => { const layersByGroup = this.groupedLayers();
layersByGroup.forEach(layers => {
const groupPrefix = layerPrefix(layers[0].id) const groupPrefix = layerPrefix(layers[0].id)
if(layers.length > 1) { if(layers.length > 1) {
const grp = <LayerListGroup const grp = <LayerListGroup
data-wd-key={[groupPrefix, idx].join('-')} data-wd-key={[groupPrefix, idx].join('-')}
key={[groupPrefix, idx].join('-')} aria-controls={layers.map(l => l.key).join(" ")}
key={`group-${groupPrefix}-${idx}`}
title={groupPrefix} title={groupPrefix}
isActive={!this.isCollapsed(groupPrefix, idx) || idx === this.props.selectedLayerIndex} isActive={!this.isCollapsed(groupPrefix, idx) || idx === this.props.selectedLayerIndex}
onActiveToggle={this.toggleLayerGroup.bind(this, groupPrefix, idx)} onActiveToggle={this.toggleLayerGroup.bind(this, groupPrefix, idx)}
@@ -189,6 +235,11 @@ class LayerListContainer extends React.Component {
); );
}); });
const additionalProps = {};
if (idx === this.props.selectedLayerIndex) {
additionalProps.ref = this.selectedItemRef;
}
const listItem = <LayerListItem const listItem = <LayerListItem
className={classnames({ className={classnames({
'maputnik-layer-list-item-collapsed': layers.length > 1 && this.isCollapsed(groupPrefix, groupIdx) && idx !== this.props.selectedLayerIndex, 'maputnik-layer-list-item-collapsed': layers.length > 1 && this.isCollapsed(groupPrefix, groupIdx) && idx !== this.props.selectedLayerIndex,
@@ -196,8 +247,10 @@ class LayerListContainer extends React.Component {
'maputnik-layer-list-item--error': !!layerError 'maputnik-layer-list-item--error': !!layerError
})} })}
index={idx} index={idx}
key={layer.id} key={layer.key}
id={layer.key}
layerId={layer.id} layerId={layer.id}
layerIndex={idx}
layerType={layer.type} layerType={layer.type}
visibility={(layer.layout || {}).visibility} visibility={(layer.layout || {}).visibility}
isSelected={idx === this.props.selectedLayerIndex} isSelected={idx === this.props.selectedLayerIndex}
@@ -205,14 +258,21 @@ class LayerListContainer extends React.Component {
onLayerDestroy={this.props.onLayerDestroy.bind(this)} onLayerDestroy={this.props.onLayerDestroy.bind(this)}
onLayerCopy={this.props.onLayerCopy.bind(this)} onLayerCopy={this.props.onLayerCopy.bind(this)}
onLayerVisibilityToggle={this.props.onLayerVisibilityToggle.bind(this)} onLayerVisibilityToggle={this.props.onLayerVisibilityToggle.bind(this)}
{...additionalProps}
/> />
listItems.push(listItem) listItems.push(listItem)
idx += 1 idx += 1
}) })
}) })
return <div className="maputnik-layer-list"> return <section
<AddModal className="maputnik-layer-list"
role="complementary"
aria-label="Layers list"
ref={this.scrollContainerRef}
>
<ModalAdd
key={this.state.keys.add}
layers={this.props.layers} layers={this.props.layers}
sources={this.props.sources} sources={this.props.sources}
isOpen={this.state.isOpen.add} isOpen={this.state.isOpen.add}
@@ -225,7 +285,7 @@ class LayerListContainer extends React.Component {
<div className="maputnik-default-property"> <div className="maputnik-default-property">
<div className="maputnik-multibutton"> <div className="maputnik-multibutton">
<button <button
id="skip-menu" id="skip-target-layer-list"
onClick={this.toggleLayers} onClick={this.toggleLayers}
className="maputnik-button"> className="maputnik-button">
{this.state.areAllGroupsExpanded === true ? "Collapse" : "Expand"} {this.state.areAllGroupsExpanded === true ? "Collapse" : "Expand"}
@@ -243,10 +303,15 @@ class LayerListContainer extends React.Component {
</div> </div>
</div> </div>
</header> </header>
<ul className="maputnik-layer-list-container"> <div
{listItems} role="navigation"
</ul> aria-label="Layers list"
</div> >
<ul className="maputnik-layer-list-container">
{listItems}
</ul>
</div>
</section>
} }
} }
@@ -261,6 +326,7 @@ export default class LayerList extends React.Component {
helperClass='sortableHelper' helperClass='sortableHelper'
onSortEnd={this.props.onMoveLayer.bind(this)} onSortEnd={this.props.onMoveLayer.bind(this)}
useDragHandle={true} useDragHandle={true}
shouldCancelStart={() => false}
/> />
} }
} }

View File

@@ -7,7 +7,8 @@ export default class LayerListGroup extends React.Component {
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
"data-wd-key": PropTypes.string, "data-wd-key": PropTypes.string,
isActive: PropTypes.bool.isRequired, isActive: PropTypes.bool.isRequired,
onActiveToggle: PropTypes.func.isRequired onActiveToggle: PropTypes.func.isRequired,
'aria-controls': PropTypes.string,
} }
render() { render() {
@@ -16,7 +17,13 @@ export default class LayerListGroup extends React.Component {
data-wd-key={"layer-list-group:"+this.props["data-wd-key"]} data-wd-key={"layer-list-group:"+this.props["data-wd-key"]}
onClick={e => this.props.onActiveToggle(!this.props.isActive)} onClick={e => this.props.onActiveToggle(!this.props.isActive)}
> >
<span className="maputnik-layer-list-group-title">{this.props.title}</span> <button
className="maputnik-layer-list-group-title"
aria-controls={this.props['aria-controls']}
aria-expanded={this.props.isActive}
>
{this.props.title}
</button>
<span className="maputnik-space" /> <span className="maputnik-space" />
<Collapser <Collapser
style={{ height: 14, width: 14 }} style={{ height: 14, width: 14 }}

View File

@@ -4,17 +4,19 @@ import classnames from 'classnames'
import {MdContentCopy, MdVisibility, MdVisibilityOff, MdDelete} from 'react-icons/md' import {MdContentCopy, MdVisibility, MdVisibilityOff, MdDelete} from 'react-icons/md'
import LayerIcon from '../icons/LayerIcon' import IconLayer from './IconLayer'
import {SortableElement, SortableHandle} from 'react-sortable-hoc' import {SortableElement, SortableHandle} from 'react-sortable-hoc'
const DraggableLabel = SortableHandle((props) => { const DraggableLabel = SortableHandle((props) => {
return <div className="maputnik-layer-list-item-handle"> return <div className="maputnik-layer-list-item-handle">
<LayerIcon <IconLayer
className="layer-handle__icon" className="layer-handle__icon"
type={props.layerType} type={props.layerType}
/> />
<span className="maputnik-layer-list-item-id">{props.layerId}</span> <button className="maputnik-layer-list-item-id">
{props.layerId}
</button>
</div> </div>
}); });
@@ -54,6 +56,7 @@ class IconAction extends React.Component {
className={`maputnik-layer-list-icon-action ${classAdditions}`} className={`maputnik-layer-list-icon-action ${classAdditions}`}
data-wd-key={this.props.wdKey} data-wd-key={this.props.wdKey}
onClick={this.props.onClick} onClick={this.props.onClick}
aria-hidden="true"
> >
{this.renderIcon()} {this.renderIcon()}
</button> </button>
@@ -62,6 +65,7 @@ class IconAction extends React.Component {
class LayerListItem extends React.Component { class LayerListItem extends React.Component {
static propTypes = { static propTypes = {
layerIndex: PropTypes.number.isRequired,
layerId: PropTypes.string.isRequired, layerId: PropTypes.string.isRequired,
layerType: PropTypes.string.isRequired, layerType: PropTypes.string.isRequired,
isSelected: PropTypes.bool, isSelected: PropTypes.bool,
@@ -96,8 +100,9 @@ class LayerListItem extends React.Component {
const visibilityAction = this.props.visibility === 'visible' ? 'show' : 'hide'; const visibilityAction = this.props.visibility === 'visible' ? 'show' : 'hide';
return <li return <li
id={this.props.id}
key={this.props.layerId} key={this.props.layerId}
onClick={e => this.props.onLayerSelect(this.props.layerId)} onClick={e => this.props.onLayerSelect(this.props.layerIndex)}
data-wd-key={"layer-list-item:"+this.props.layerId} data-wd-key={"layer-list-item:"+this.props.layerId}
className={classnames({ className={classnames({
"maputnik-layer-list-item": true, "maputnik-layer-list-item": true,
@@ -110,20 +115,20 @@ class LayerListItem extends React.Component {
wdKey={"layer-list-item:"+this.props.layerId+":delete"} wdKey={"layer-list-item:"+this.props.layerId+":delete"}
action={'delete'} action={'delete'}
classBlockName="delete" classBlockName="delete"
onClick={e => this.props.onLayerDestroy(this.props.layerId)} onClick={e => this.props.onLayerDestroy(this.props.layerIndex)}
/> />
<IconAction <IconAction
wdKey={"layer-list-item:"+this.props.layerId+":copy"} wdKey={"layer-list-item:"+this.props.layerId+":copy"}
action={'duplicate'} action={'duplicate'}
classBlockName="duplicate" classBlockName="duplicate"
onClick={e => this.props.onLayerCopy(this.props.layerId)} onClick={e => this.props.onLayerCopy(this.props.layerIndex)}
/> />
<IconAction <IconAction
wdKey={"layer-list-item:"+this.props.layerId+":toggle-visibility"} wdKey={"layer-list-item:"+this.props.layerId+":toggle-visibility"}
action={visibilityAction} action={visibilityAction}
classBlockName="visibility" classBlockName="visibility"
classBlockModifier={visibilityAction} classBlockModifier={visibilityAction}
onClick={e => this.props.onLayerVisibilityToggle(this.props.layerId)} onClick={e => this.props.onLayerVisibilityToggle(this.props.layerIndex)}
/> />
</li> </li>
} }

View File

@@ -1,21 +1,21 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import MapboxGl from 'mapbox-gl' import MapLibreGl from 'maplibre-gl'
import MapboxInspect from 'mapbox-gl-inspect' import MapboxInspect from 'mapbox-gl-inspect'
import FeatureLayerPopup from './FeatureLayerPopup' import MapMapboxGlLayerPopup from './MapMapboxGlLayerPopup'
import FeaturePropertyPopup from './FeaturePropertyPopup' import MapMapboxGlFeaturePropertyPopup from './MapMapboxGlFeaturePropertyPopup'
import tokens from '../../config/tokens.json' import tokens from '../config/tokens.json'
import colors from 'mapbox-gl-inspect/lib/colors' import colors from 'mapbox-gl-inspect/lib/colors'
import Color from 'color' import Color from 'color'
import ZoomControl from '../../libs/zoomcontrol' import ZoomControl from '../libs/zoomcontrol'
import { colorHighlightedLayer } from '../../libs/highlight' import { colorHighlightedLayer } from '../libs/highlight'
import 'mapbox-gl/dist/mapbox-gl.css' import 'maplibre-gl/dist/maplibre-gl.css'
import '../../mapboxgl.css' import '../mapboxgl.css'
import '../../libs/mapbox-rtl' import '../libs/mapbox-rtl'
const IS_SUPPORTED = MapboxGl.supported(); const IS_SUPPORTED = MapLibreGl.supported();
function renderPopup(popup, mountNode) { function renderPopup(popup, mountNode) {
ReactDOM.render(popup, mountNode); ReactDOM.render(popup, mountNode);
@@ -52,7 +52,7 @@ function buildInspectStyle(originalMapStyle, coloredLayers, highlightedLayer) {
return inspectStyle return inspectStyle
} }
export default class MapboxGlMap extends React.Component { export default class MapMapboxGl extends React.Component {
static propTypes = { static propTypes = {
onDataChange: PropTypes.func, onDataChange: PropTypes.func,
onLayerSelect: PropTypes.func.isRequired, onLayerSelect: PropTypes.func.isRequired,
@@ -75,7 +75,6 @@ export default class MapboxGlMap extends React.Component {
constructor(props) { constructor(props) {
super(props) super(props)
MapboxGl.accessToken = tokens.mapbox
this.state = { this.state = {
map: null, map: null,
inspect: null, inspect: null,
@@ -86,8 +85,6 @@ export default class MapboxGlMap extends React.Component {
if(!IS_SUPPORTED) return; if(!IS_SUPPORTED) return;
if(!this.state.map) return if(!this.state.map) return
const metadata = props.mapStyle.metadata || {}
MapboxGl.accessToken = metadata['maputnik:mapbox_access_token'] || tokens.mapbox
//Mapbox GL now does diffing natively so we don't need to calculate //Mapbox GL now does diffing natively so we don't need to calculate
//the necessary operations ourselves! //the necessary operations ourselves!
@@ -97,24 +94,41 @@ export default class MapboxGlMap extends React.Component {
) )
} }
componentDidUpdate(prevProps) { shouldComponentUpdate(nextProps, nextState) {
let should = false;
try {
should = JSON.stringify(this.props) !== JSON.stringify(nextProps) || JSON.stringify(this.state) !== JSON.stringify(nextState);
} catch(e) {
// no biggie, carry on
}
return should;
}
componentDidUpdate(prevProps, prevState, snapshot) {
if(!IS_SUPPORTED) return; if(!IS_SUPPORTED) return;
const map = this.state.map; const map = this.state.map;
this.updateMapFromProps(this.props); this.updateMapFromProps(this.props);
if(this.props.inspectModeEnabled !== prevProps.inspectModeEnabled) { if(this.state.inspect && this.props.inspectModeEnabled !== this.state.inspect._showInspectMap) {
// HACK: Fix for <https://github.com/maputnik/editor/issues/576>, while we wait for a proper fix. // HACK: Fix for <https://github.com/maputnik/editor/issues/576>, while we wait for a proper fix.
// eslint-disable-next-line // eslint-disable-next-line
this.state.inspect._popupBlocked = false; this.state.inspect._popupBlocked = false;
this.state.inspect.toggleInspector() this.state.inspect.toggleInspector()
} }
if(this.props.inspectModeEnabled) {
this.state.inspect.render()
}
if (map) { if (map) {
if (this.props.inspectModeEnabled) {
// HACK: We need to work out why we need to do this and what's causing
// this error. I'm assuming an issue with mapbox-gl update and
// mapbox-gl-inspect.
try {
this.state.inspect.render();
} catch(err) {
console.error("FIXME: Caught error", err);
}
}
map.showTileBoundaries = this.props.options.showTileBoundaries; map.showTileBoundaries = this.props.options.showTileBoundaries;
map.showCollisionBoxes = this.props.options.showCollisionBoxes; map.showCollisionBoxes = this.props.options.showCollisionBoxes;
map.showOverdrawInspector = this.props.options.showOverdrawInspector; map.showOverdrawInspector = this.props.options.showOverdrawInspector;
@@ -132,7 +146,7 @@ export default class MapboxGlMap extends React.Component {
maxZoom: 24 maxZoom: 24
} }
const map = new MapboxGl.Map(mapOpts); const map = new MapLibreGl.Map(mapOpts);
const mapViewChange = () => { const mapViewChange = () => {
const center = map.getCenter(); const center = map.getCenter();
@@ -148,13 +162,13 @@ export default class MapboxGlMap extends React.Component {
const zoomControl = new ZoomControl; const zoomControl = new ZoomControl;
map.addControl(zoomControl, 'top-right'); map.addControl(zoomControl, 'top-right');
const nav = new MapboxGl.NavigationControl({visualizePitch:true}); const nav = new MapLibreGl.NavigationControl({visualizePitch:true});
map.addControl(nav, 'top-right'); map.addControl(nav, 'top-right');
const tmpNode = document.createElement('div'); const tmpNode = document.createElement('div');
const inspect = new MapboxInspect({ const inspect = new MapboxInspect({
popup: new MapboxGl.Popup({ popup: new MapLibreGl.Popup({
closeOnClick: false closeOnClick: false
}), }),
showMapPopup: true, showMapPopup: true,
@@ -168,9 +182,9 @@ export default class MapboxGlMap extends React.Component {
buildInspectStyle: (originalMapStyle, coloredLayers) => buildInspectStyle(originalMapStyle, coloredLayers, this.props.highlightedLayer), buildInspectStyle: (originalMapStyle, coloredLayers) => buildInspectStyle(originalMapStyle, coloredLayers, this.props.highlightedLayer),
renderPopup: features => { renderPopup: features => {
if(this.props.inspectModeEnabled) { if(this.props.inspectModeEnabled) {
return renderPopup(<FeaturePropertyPopup features={features} />, tmpNode); return renderPopup(<MapMapboxGlFeaturePropertyPopup features={features} />, tmpNode);
} else { } else {
return renderPopup(<FeatureLayerPopup features={features} onLayerSelect={this.props.onLayerSelect} zoom={this.state.zoom} />, tmpNode); return renderPopup(<MapMapboxGlLayerPopup features={features} onLayerSelect={this.onLayerSelectById} zoom={this.state.zoom} />, tmpNode);
} }
} }
}) })
@@ -182,9 +196,6 @@ export default class MapboxGlMap extends React.Component {
inspect, inspect,
zoom: map.getZoom() zoom: map.getZoom()
}); });
if(this.props.inspectModeEnabled) {
inspect.toggleInspector();
}
}) })
map.on("data", e => { map.on("data", e => {
@@ -208,10 +219,17 @@ export default class MapboxGlMap extends React.Component {
map.on("zoomend", mapViewChange); map.on("zoomend", mapViewChange);
} }
onLayerSelectById = (id) => {
const index = this.props.mapStyle.layers.findIndex(layer => layer.id === id);
this.props.onLayerSelect(index);
}
render() { render() {
if(IS_SUPPORTED) { if(IS_SUPPORTED) {
return <div return <div
className="maputnik-map__map" className="maputnik-map__map"
role="region"
aria-label="Map view"
ref={x => this.container = x} ref={x => this.container = x}
></div> ></div>
} }
@@ -226,3 +244,4 @@ export default class MapboxGlMap extends React.Component {
} }
} }
} }

View File

@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import InputBlock from '../inputs/InputBlock' import Block from './Block'
import StringInput from '../inputs/StringInput' import FieldString from './FieldString'
function displayValue(value) { function displayValue(value) {
if (typeof value === 'undefined' || value === null) return value; if (typeof value === 'undefined' || value === null) return value;
@@ -15,18 +15,25 @@ function displayValue(value) {
function renderProperties(feature) { function renderProperties(feature) {
return Object.keys(feature.properties).map(propertyName => { return Object.keys(feature.properties).map(propertyName => {
const property = feature.properties[propertyName] const property = feature.properties[propertyName]
return <InputBlock key={propertyName} label={propertyName}> return <Block key={propertyName} label={propertyName}>
<StringInput value={displayValue(property)} style={{backgroundColor: 'transparent'}}/> <FieldString value={displayValue(property)} style={{backgroundColor: 'transparent'}}/>
</InputBlock> </Block>
}) })
} }
function renderFeatureId(feature) {
return <Block key={"feature-id"} label={"feature_id"}>
<FieldString value={displayValue(feature.id)} style={{backgroundColor: 'transparent'}} />
</Block>
}
function renderFeature(feature, idx) { function renderFeature(feature, idx) {
return <div key={`${feature.sourceLayer}-${idx}`}> return <div key={`${feature.sourceLayer}-${idx}`}>
<div className="maputnik-popup-layer-id">{feature.layer['source-layer']}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</div> <div className="maputnik-popup-layer-id">{feature.layer['source']}: {feature.layer['source-layer']}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</div>
<InputBlock key={"property-type"} label={"$type"}> <Block key={"property-type"} label={"$type"}>
<StringInput value={feature.geometry.type} style={{backgroundColor: 'transparent'}} /> <FieldString value={feature.geometry.type} style={{backgroundColor: 'transparent'}} />
</InputBlock> </Block>
{renderFeatureId(feature)}
{renderProperties(feature)} {renderProperties(feature)}
</div> </div>
} }
@@ -36,7 +43,7 @@ function removeDuplicatedFeatures(features) {
features.forEach(feature => { features.forEach(feature => {
const featureIndex = uniqueFeatures.findIndex(feature2 => { const featureIndex = uniqueFeatures.findIndex(feature2 => {
return feature.layer['source-layer'] === feature2.layer['source-layer'] return feature.layer['source-layer'] === feature2.layer['source-layer']
&& JSON.stringify(feature.properties) === JSON.stringify(feature2.properties) && JSON.stringify(feature.properties) === JSON.stringify(feature2.properties)
}) })

View File

@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import LayerIcon from '../icons/LayerIcon' import IconLayer from './IconLayer'
import {latest, expression, function as styleFunction} from '@mapbox/mapbox-gl-style-spec' import {latest} from '@maplibre/maplibre-gl-style-spec'
function groupFeaturesBySourceLayer(features) { function groupFeaturesBySourceLayer(features) {
const sources = {} const sources = {}
@@ -11,7 +11,7 @@ function groupFeaturesBySourceLayer(features) {
features.forEach(feature => { features.forEach(feature => {
if(returnedFeatures.hasOwnProperty(feature.layer.id)) { if(returnedFeatures.hasOwnProperty(feature.layer.id)) {
returnedFeatures[feature.layer.id]++ returnedFeatures[feature.layer.id]++
const featureObject = sources[feature.layer['source-layer']].find(f => f.layer.id === feature.layer.id) const featureObject = sources[feature.layer['source-layer']].find(f => f.layer.id === feature.layer.id)
featureObject.counter = returnedFeatures[feature.layer.id] featureObject.counter = returnedFeatures[feature.layer.id]
@@ -58,23 +58,8 @@ class FeatureLayerPopup extends React.Component {
if(propName) { if(propName) {
const propertySpec = latest["paint_"+feature.layer.type][propName]; const propertySpec = latest["paint_"+feature.layer.type][propName];
let color = feature.layer.paint[propName]; let color = feature.layer.paint[propName];
return String(color);
if(typeof(color) === "object") {
if(color.stops) {
color = styleFunction.convertFunction(color, propertySpec);
}
const exprResult = expression.createExpression(color, propertySpec);
const val = exprResult.value.evaluate({
zoom: zoom
}, feature);
return val.toString();
}
else {
return color;
}
} }
else { else {
// Default color // Default color
@@ -84,7 +69,7 @@ class FeatureLayerPopup extends React.Component {
// This is quite complex, just incase there's an edgecase we're missing // This is quite complex, just incase there's an edgecase we're missing
// always return black if we get an unexpected error. // always return black if we get an unexpected error.
catch (err) { catch (err) {
console.error("Unable to get feature color, error:", err); console.warn("Unable to get feature color, error:", err);
return "black"; return "black";
} }
} }
@@ -101,7 +86,7 @@ class FeatureLayerPopup extends React.Component {
className="maputnik-popup-layer" className="maputnik-popup-layer"
> >
<div <div
className="maputnik-popup-layer__swatch" className="maputnik-popup-layer__swatch"
style={{background: featureColor}} style={{background: featureColor}}
></div> ></div>
<label <label
@@ -110,8 +95,8 @@ class FeatureLayerPopup extends React.Component {
this.props.onLayerSelect(feature.layer.id) this.props.onLayerSelect(feature.layer.id)
}} }}
> >
{feature.layer.type && {feature.layer.type &&
<LayerIcon type={feature.layer.type} style={{ <IconLayer type={feature.layer.type} style={{
width: 14, width: 14,
height: 14, height: 14,
paddingRight: 3 paddingRight: 3

View File

@@ -1,9 +1,9 @@
import React from 'react' import React from 'react'
import {throttle} from 'lodash'; import {throttle} from 'lodash';
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { loadJSON } from '../../libs/urlopen' import { loadJSON } from '../libs/urlopen'
import FeatureLayerPopup from './FeatureLayerPopup'; import MapMapboxGlLayerPopup from './MapMapboxGlLayerPopup';
import 'ol/ol.css' import 'ol/ol.css'
import {apply} from 'ol-mapbox-style'; import {apply} from 'ol-mapbox-style';
@@ -24,7 +24,7 @@ function renderCoords (coords) {
} }
} }
export default class OpenLayersMap extends React.Component { export default class MapOpenLayers extends React.Component {
static propTypes = { static propTypes = {
onDataChange: PropTypes.func, onDataChange: PropTypes.func,
mapStyle: PropTypes.object.isRequired, mapStyle: PropTypes.object.isRequired,
@@ -152,7 +152,7 @@ export default class OpenLayersMap extends React.Component {
> >
× ×
</button> </button>
<FeatureLayerPopup <MapMapboxGlLayerPopup
features={this.state.selectedFeatures || []} features={this.state.selectedFeatures || []}
onLayerSelect={this.props.onLayerSelect} onLayerSelect={this.props.onLayerSelect}
/> />
@@ -179,6 +179,8 @@ export default class OpenLayersMap extends React.Component {
<div <div
className="maputnik-ol" className="maputnik-ol"
ref={x => this.container = x} ref={x => this.container = x}
role="region"
aria-label="Map view"
style={{ style={{
...this.props.style, ...this.props.style,
}}> }}>

View File

@@ -5,7 +5,7 @@ import AriaModal from 'react-aria-modal'
import classnames from 'classnames'; import classnames from 'classnames';
class Modal extends React.Component { export default class Modal extends React.Component {
static propTypes = { static propTypes = {
"data-wd-key": PropTypes.string, "data-wd-key": PropTypes.string,
isOpen: PropTypes.bool.isRequired, isOpen: PropTypes.bool.isRequired,
@@ -32,17 +32,12 @@ class Modal extends React.Component {
}); });
} }
getApplicationNode() {
return document.getElementById('app');
}
render() { render() {
if(this.props.isOpen) { if(this.props.isOpen) {
return <AriaModal return <AriaModal
titleText={this.props.title} titleText={this.props.title}
underlayClickExits={this.props.underlayClickExits} underlayClickExits={this.props.underlayClickExits}
underlayProps={this.props.underlayProps} underlayProps={this.props.underlayProps}
getApplicationNode={this.getApplicationNode}
data-wd-key={this.props["data-wd-key"]} data-wd-key={this.props["data-wd-key"]}
verticallyCenter={true} verticallyCenter={true}
onExit={this.onClose} onExit={this.onClose}
@@ -54,6 +49,7 @@ class Modal extends React.Component {
<h1 className="maputnik-modal-header-title">{this.props.title}</h1> <h1 className="maputnik-modal-header-title">{this.props.title}</h1>
<span className="maputnik-modal-header-space"></span> <span className="maputnik-modal-header-space"></span>
<button className="maputnik-modal-header-toggle" <button className="maputnik-modal-header-toggle"
title="Close modal"
onClick={this.onClose} onClick={this.onClose}
data-wd-key={this.props["data-wd-key"]+".close-modal"} data-wd-key={this.props["data-wd-key"]+".close-modal"}
> >
@@ -72,4 +68,3 @@ class Modal extends React.Component {
} }
} }
export default Modal

View File

@@ -1,15 +1,16 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import Button from '../Button' import {latest} from '@maplibre/maplibre-gl-style-spec'
import InputButton from './InputButton'
import Modal from './Modal' import Modal from './Modal'
import LayerTypeBlock from '../layers/LayerTypeBlock' import FieldType from './FieldType'
import LayerIdBlock from '../layers/LayerIdBlock' import FieldId from './FieldId'
import LayerSourceBlock from '../layers/LayerSourceBlock' import FieldSource from './FieldSource'
import LayerSourceLayerBlock from '../layers/LayerSourceLayerBlock' import FieldSourceLayer from './FieldSourceLayer'
class AddModal extends React.Component { export default class ModalAdd extends React.Component {
static propTypes = { static propTypes = {
layers: PropTypes.array.isRequired, layers: PropTypes.array.isRequired,
onLayersChange: PropTypes.func.isRequired, onLayersChange: PropTypes.func.isRequired,
@@ -129,20 +130,22 @@ class AddModal extends React.Component {
className="maputnik-add-modal" className="maputnik-add-modal"
> >
<div className="maputnik-add-layer"> <div className="maputnik-add-layer">
<LayerIdBlock <FieldId
label="ID"
fieldSpec={latest.layer.id}
value={this.state.id} value={this.state.id}
wdKey="add-layer.layer-id" wdKey="add-layer.layer-id"
onChange={v => { onChange={v => {
this.setState({ id: v }) this.setState({ id: v })
}} }}
/> />
<LayerTypeBlock <FieldType
value={this.state.type} value={this.state.type}
wdKey="add-layer.layer-type" wdKey="add-layer.layer-type"
onChange={v => this.setState({ type: v })} onChange={v => this.setState({ type: v })}
/> />
{this.state.type !== 'background' && {this.state.type !== 'background' &&
<LayerSourceBlock <FieldSource
sourceIds={sources} sourceIds={sources}
wdKey="add-layer.layer-source-block" wdKey="add-layer.layer-source-block"
value={this.state.source} value={this.state.source}
@@ -150,23 +153,22 @@ class AddModal extends React.Component {
/> />
} }
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.state.type) < 0 && {['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.state.type) < 0 &&
<LayerSourceLayerBlock <FieldSourceLayer
isFixed={true} isFixed={true}
sourceLayerIds={layers} sourceLayerIds={layers}
value={this.state['source-layer']} value={this.state['source-layer']}
onChange={v => this.setState({ 'source-layer': v })} onChange={v => this.setState({ 'source-layer': v })}
/> />
} }
<Button <InputButton
className="maputnik-add-layer-button" className="maputnik-add-layer-button"
onClick={this.addLayer} onClick={this.addLayer}
data-wd-key="add-layer" data-wd-key="add-layer"
> >
Add Layer Add Layer
</Button> </InputButton>
</div> </div>
</Modal> </Modal>
} }
} }
export default AddModal

View File

@@ -4,7 +4,7 @@ import PropTypes from 'prop-types'
import Modal from './Modal' import Modal from './Modal'
class DebugModal extends React.Component { export default class ModalDebug extends React.Component {
static propTypes = { static propTypes = {
isOpen: PropTypes.bool.isRequired, isOpen: PropTypes.bool.isRequired,
renderer: PropTypes.string.isRequired, renderer: PropTypes.string.isRequired,
@@ -24,13 +24,13 @@ class DebugModal extends React.Component {
const osmLat = Number.parseFloat(mapView.center.lat).toFixed(5); const osmLat = Number.parseFloat(mapView.center.lat).toFixed(5);
return <Modal return <Modal
data-wd-key="debug-modal" data-wd-key="modal:debug"
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle} onOpenToggle={this.props.onOpenToggle}
title={'Debug'} title={'Debug'}
> >
<div className="maputnik-modal-section maputnik-modal-shortcuts"> <section className="maputnik-modal-section maputnik-modal-shortcuts">
<h4>Options</h4> <h1>Options</h1>
{this.props.renderer === 'mbgljs' && {this.props.renderer === 'mbgljs' &&
<ul> <ul>
{Object.entries(this.props.mapboxGlDebugOptions).map(([key, val]) => { {Object.entries(this.props.mapboxGlDebugOptions).map(([key, val]) => {
@@ -53,9 +53,9 @@ class DebugModal extends React.Component {
})} })}
</ul> </ul>
} }
</div> </section>
<div className="maputnik-modal-section"> <section className="maputnik-modal-section">
<h4>Links</h4> <h1>Links</h1>
<p> <p>
<a <a
target="_blank" target="_blank"
@@ -65,9 +65,8 @@ class DebugModal extends React.Component {
Open in OSM Open in OSM
</a> &mdash; Opens the current view on openstreetmap.org </a> &mdash; Opens the current view on openstreetmap.org
</p> </p>
</div> </section>
</Modal> </Modal>
} }
} }
export default DebugModal;

View File

@@ -0,0 +1,162 @@
import React from 'react'
import PropTypes from 'prop-types'
import Slugify from 'slugify'
import { saveAs } from 'file-saver'
import pkgLockJson from '../../package-lock.json'
import {format} from '@maplibre/maplibre-gl-style-spec'
import FieldString from './FieldString'
import FieldCheckbox from './FieldCheckbox'
import InputButton from './InputButton'
import Modal from './Modal'
import {MdFileDownload} from 'react-icons/md'
import style from '../libs/style'
import fieldSpecAdditional from '../libs/field-spec-additional'
const MAPBOX_GL_VERSION = pkgLockJson.dependencies["mapbox-gl"].version;
export default class ModalExport extends React.Component {
static propTypes = {
mapStyle: PropTypes.object.isRequired,
onStyleChanged: PropTypes.func.isRequired,
isOpen: PropTypes.bool.isRequired,
onOpenToggle: PropTypes.func.isRequired,
}
constructor(props) {
super(props);
}
tokenizedStyle () {
return format(
style.stripAccessTokens(
style.replaceAccessTokens(this.props.mapStyle)
)
);
}
exportName () {
if(this.props.mapStyle.name) {
return Slugify(this.props.mapStyle.name, {
replacement: '_',
remove: /[*\-+~.()'"!:]/g,
lower: true
});
} else {
return this.props.mapStyle.id
}
}
downloadHtml() {
const tokenStyle = this.tokenizedStyle();
const htmlTitle = this.props.mapStyle.name || "Map";
const html = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>${htmlTitle}</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
<script src="https://api.mapbox.com/mapbox-gl-js/v${MAPBOX_GL_VERSION}/mapbox-gl.js"></script>
<link href="https://api.mapbox.com/mapbox-gl-js/v${MAPBOX_GL_VERSION}/mapbox-gl.css" rel="stylesheet" />
<style>
body { margin: 0; padding: 0; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
</style>
</head>
<body>
<div id="map"></div>
<script>
mapboxgl.accessToken = 'access_token';
const map = new mapboxgl.Map({
container: 'map',
style: ${tokenStyle},
});
map.addControl(new mapboxgl.NavigationControl());
</script>
</body>
</html>
`;
const blob = new Blob([html], {type: "text/html;charset=utf-8"});
const exportName = this.exportName();
saveAs(blob, exportName + ".html");
}
downloadStyle() {
const tokenStyle = this.tokenizedStyle();
const blob = new Blob([tokenStyle], {type: "application/json;charset=utf-8"});
const exportName = this.exportName();
saveAs(blob, exportName + ".json");
}
changeMetadataProperty(property, value) {
const changedStyle = {
...this.props.mapStyle,
metadata: {
...this.props.mapStyle.metadata,
[property]: value
}
}
this.props.onStyleChanged(changedStyle)
}
render() {
return <Modal
data-wd-key="modal:export"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={'Export Style'}
className="maputnik-export-modal"
>
<section className="maputnik-modal-section">
<h1>Download Style</h1>
<p>
Download a JSON style to your computer.
</p>
<div>
<FieldString
label={fieldSpecAdditional.maputnik.mapbox_access_token.label}
fieldSpec={fieldSpecAdditional.maputnik.mapbox_access_token}
value={(this.props.mapStyle.metadata || {})['maputnik:mapbox_access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:mapbox_access_token")}
/>
<FieldString
label={fieldSpecAdditional.maputnik.maptiler_access_token.label}
fieldSpec={fieldSpecAdditional.maputnik.maptiler_access_token}
value={(this.props.mapStyle.metadata || {})['maputnik:openmaptiles_access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
/>
<FieldString
label={fieldSpecAdditional.maputnik.thunderforest_access_token.label}
fieldSpec={fieldSpecAdditional.maputnik.thunderforest_access_token}
value={(this.props.mapStyle.metadata || {})['maputnik:thunderforest_access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
/>
</div>
<div className="maputnik-modal-export-buttons">
<InputButton
onClick={this.downloadStyle.bind(this)}
>
<MdFileDownload />
Download Style
</InputButton>
<InputButton
onClick={this.downloadHtml.bind(this)}
>
<MdFileDownload />
Download HTML
</InputButton>
</div>
</section>
</Modal>
}
}

View File

@@ -1,11 +1,11 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import Button from '../Button' import InputButton from './InputButton'
import Modal from './Modal' import Modal from './Modal'
class LoadingModal extends React.Component { export default class ModalLoading extends React.Component {
static propTypes = { static propTypes = {
isOpen: PropTypes.bool.isRequired, isOpen: PropTypes.bool.isRequired,
onCancel: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired,
@@ -20,7 +20,7 @@ class LoadingModal extends React.Component {
render() { render() {
return <Modal return <Modal
data-wd-key="loading-modal" data-wd-key="modal:loading"
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
underlayClickExits={false} underlayClickExits={false}
underlayProps={{ underlayProps={{
@@ -34,12 +34,11 @@ class LoadingModal extends React.Component {
{this.props.message} {this.props.message}
</p> </p>
<p className="maputnik-dialog__buttons"> <p className="maputnik-dialog__buttons">
<Button onClick={(e) => this.props.onCancel(e)}> <InputButton onClick={(e) => this.props.onCancel(e)}>
Cancel Cancel
</Button> </InputButton>
</p> </p>
</Modal> </Modal>
} }
} }
export default LoadingModal

View File

@@ -1,16 +1,16 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import LoadingModal from './LoadingModal' import ModalLoading from './ModalLoading'
import Modal from './Modal' import Modal from './Modal'
import Button from '../Button' import InputButton from './InputButton'
import FileReaderInput from 'react-file-reader-input' import FileReaderInput from 'react-file-reader-input'
import UrlInput from '../inputs/UrlInput' import InputUrl from './InputUrl'
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 style from '../../libs/style.js' import style from '../libs/style.js'
import publicStyles from '../../config/styles.json' import publicStyles from '../config/styles.json'
class PublicStyle extends React.Component { class PublicStyle extends React.Component {
static propTypes = { static propTypes = {
@@ -22,28 +22,28 @@ class PublicStyle extends React.Component {
render() { render() {
return <div className="maputnik-public-style"> return <div className="maputnik-public-style">
<Button <InputButton
className="maputnik-public-style-button" className="maputnik-public-style-button"
aria-label={this.props.title} aria-label={this.props.title}
onClick={() => this.props.onSelect(this.props.url)} onClick={() => this.props.onSelect(this.props.url)}
> >
<header className="maputnik-public-style-header"> <div className="maputnik-public-style-header">
<h4>{this.props.title}</h4> <div>{this.props.title}</div>
<span className="maputnik-space" /> <span className="maputnik-space" />
<MdAddCircleOutline /> <MdAddCircleOutline />
</header> </div>
<div <div
className="maputnik-public-style-thumbnail" className="maputnik-public-style-thumbnail"
style={{ style={{
backgroundImage: `url(${this.props.thumbnailUrl})` backgroundImage: `url(${this.props.thumbnailUrl})`
}} }}
></div> ></div>
</Button> </InputButton>
</div> </div>
} }
} }
class OpenModal extends React.Component { export default class ModalOpen extends React.Component {
static propTypes = { static propTypes = {
isOpen: PropTypes.bool.isRequired, isOpen: PropTypes.bool.isRequired,
onOpenToggle: PropTypes.func.isRequired, onOpenToggle: PropTypes.func.isRequired,
@@ -123,7 +123,8 @@ class OpenModal extends React.Component {
}) })
} }
onOpenUrl = (url) => { onSubmitUrl = (e) => {
e.preventDefault();
this.onStyleSelect(this.state.styleUrl); this.onStyleSelect(this.state.styleUrl);
} }
@@ -190,45 +191,49 @@ class OpenModal extends React.Component {
return ( return (
<div> <div>
<Modal <Modal
data-wd-key="open-modal" data-wd-key="modal:open"
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
onOpenToggle={() => this.onOpenToggle()} onOpenToggle={() => this.onOpenToggle()}
title={'Open Style'} title={'Open Style'}
> >
{errorElement} {errorElement}
<section className="maputnik-modal-section"> <section className="maputnik-modal-section">
<h2>Upload Style</h2> <h1>Upload Style</h1>
<p>Upload a JSON style from your computer.</p> <p>Upload a JSON style from your computer.</p>
<FileReaderInput onChange={this.onUpload} tabIndex="-1"> <FileReaderInput onChange={this.onUpload} tabIndex="-1" aria-label="Style file">
<Button className="maputnik-upload-button"><MdFileUpload /> Upload</Button> <InputButton className="maputnik-upload-button"><MdFileUpload /> Upload</InputButton>
</FileReaderInput> </FileReaderInput>
</section> </section>
<section className="maputnik-modal-section"> <section className="maputnik-modal-section">
<h2>Load from URL</h2> <form onSubmit={this.onSubmitUrl}>
<p> <h1>Load from URL</h1>
Load from a URL. Note that the URL must have <a href="https://enable-cors.org" target="_blank" rel="noopener noreferrer">CORS enabled</a>. <p>
</p> Load from a URL. Note that the URL must have <a href="https://enable-cors.org" target="_blank" rel="noopener noreferrer">CORS enabled</a>.
<UrlInput </p>
data-wd-key="open-modal.url.input" <InputUrl
type="text" aria-label="Style URL"
className="maputnik-input" data-wd-key="modal:open.url.input"
default="Enter URL..." type="text"
value={this.state.styleUrl} className="maputnik-input"
onInput={this.onChangeUrl} default="Enter URL..."
/> value={this.state.styleUrl}
<div> onInput={this.onChangeUrl}
<Button onChange={this.onChangeUrl}
data-wd-key="open-modal.url.button" />
className="maputnik-big-button" <div>
onClick={this.onOpenUrl} <InputButton
disabled={this.state.styleUrl.length < 1} data-wd-key="modal:open.url.button"
>Open URL</Button> type="submit"
</div> className="maputnik-big-button"
disabled={this.state.styleUrl.length < 1}
>Load from URL</InputButton>
</div>
</form>
</section> </section>
<section className="maputnik-modal-section maputnik-modal-section--shrink"> <section className="maputnik-modal-section maputnik-modal-section--shrink">
<h2>Gallery Styles</h2> <h1>Gallery Styles</h1>
<p> <p>
Open one of the publicly available styles to start from. Open one of the publicly available styles to start from.
</p> </p>
@@ -238,7 +243,7 @@ class OpenModal extends React.Component {
</section> </section>
</Modal> </Modal>
<LoadingModal <ModalLoading
isOpen={!!this.state.activeRequest} isOpen={!!this.state.activeRequest}
title={'Loading style'} title={'Loading style'}
onCancel={(e) => this.onCancelActiveRequest(e)} onCancel={(e) => this.onCancelActiveRequest(e)}
@@ -249,4 +254,3 @@ class OpenModal extends React.Component {
} }
} }
export default OpenModal

View File

@@ -1,19 +1,19 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import {latest} from '@mapbox/mapbox-gl-style-spec' import {latest} from '@maplibre/maplibre-gl-style-spec'
import InputBlock from '../inputs/InputBlock' import Block from './Block'
import ArrayInput from '../inputs/ArrayInput' import FieldArray from './FieldArray'
import NumberInput from '../inputs/NumberInput' import FieldNumber from './FieldNumber'
import StringInput from '../inputs/StringInput' import FieldString from './FieldString'
import UrlInput from '../inputs/UrlInput' import FieldUrl from './FieldUrl'
import SelectInput from '../inputs/SelectInput' import FieldSelect from './FieldSelect'
import EnumInput from '../inputs/EnumInput' import FieldEnum from './FieldEnum'
import ColorField from '../fields/ColorField' import FieldColor from './FieldColor'
import Modal from './Modal' import Modal from './Modal'
import fieldSpecAdditional from '../../libs/field-spec-additional' import fieldSpecAdditional from '../libs/field-spec-additional'
class SettingsModal extends React.Component { export default class ModalSettings extends React.Component {
static propTypes = { static propTypes = {
mapStyle: PropTypes.object.isRequired, mapStyle: PropTypes.object.isRequired,
onStyleChanged: PropTypes.func.isRequired, onStyleChanged: PropTypes.func.isRequired,
@@ -81,175 +81,165 @@ class SettingsModal extends React.Component {
const transition = this.props.mapStyle.transition || {}; const transition = this.props.mapStyle.transition || {};
return <Modal return <Modal
data-wd-key="modal-settings" data-wd-key="modal:settings"
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle} onOpenToggle={this.props.onOpenToggle}
title={'Style Settings'} title={'Style Settings'}
> >
<div className="modal-settings"> <div className="modal:settings">
<InputBlock label={"Name"} fieldSpec={latest.$root.name}> <FieldString {...inputProps}
<StringInput {...inputProps} label={"Name"}
data-wd-key="modal-settings.name" fieldSpec={latest.$root.name}
data-wd-key="modal:settings.name"
value={this.props.mapStyle.name} value={this.props.mapStyle.name}
onChange={this.changeStyleProperty.bind(this, "name")} onChange={this.changeStyleProperty.bind(this, "name")}
/> />
</InputBlock> <FieldString {...inputProps}
<InputBlock label={"Owner"} fieldSpec={{doc: "Owner ID of the style. Used by Mapbox or future style APIs."}}> label={"Owner"}
<StringInput {...inputProps} fieldSpec={{doc: "Owner ID of the style. Used by Mapbox or future style APIs."}}
data-wd-key="modal-settings.owner" data-wd-key="modal:settings.owner"
value={this.props.mapStyle.owner} value={this.props.mapStyle.owner}
onChange={this.changeStyleProperty.bind(this, "owner")} onChange={this.changeStyleProperty.bind(this, "owner")}
/> />
</InputBlock> <FieldUrl {...inputProps}
<InputBlock label={"Sprite URL"} fieldSpec={latest.$root.sprite}> fieldSpec={latest.$root.sprite}
<UrlInput {...inputProps} label="Sprite URL"
data-wd-key="modal-settings.sprite" data-wd-key="modal:settings.sprite"
value={this.props.mapStyle.sprite} value={this.props.mapStyle.sprite}
onChange={this.changeStyleProperty.bind(this, "sprite")} onChange={this.changeStyleProperty.bind(this, "sprite")}
/> />
</InputBlock>
<InputBlock label={"Glyphs URL"} fieldSpec={latest.$root.glyphs}> <FieldUrl {...inputProps}
<UrlInput {...inputProps} label="Glyphs URL"
data-wd-key="modal-settings.glyphs" fieldSpec={latest.$root.glyphs}
data-wd-key="modal:settings.glyphs"
value={this.props.mapStyle.glyphs} value={this.props.mapStyle.glyphs}
onChange={this.changeStyleProperty.bind(this, "glyphs")} onChange={this.changeStyleProperty.bind(this, "glyphs")}
/> />
</InputBlock>
<InputBlock <FieldString {...inputProps}
label={fieldSpecAdditional.maputnik.mapbox_access_token.label} label={fieldSpecAdditional.maputnik.mapbox_access_token.label}
fieldSpec={fieldSpecAdditional.maputnik.mapbox_access_token} fieldSpec={fieldSpecAdditional.maputnik.mapbox_access_token}
> data-wd-key="modal:settings.maputnik:mapbox_access_token"
<StringInput {...inputProps}
data-wd-key="modal-settings.maputnik:mapbox_access_token"
value={metadata['maputnik:mapbox_access_token']} value={metadata['maputnik:mapbox_access_token']}
onChange={onChangeMetadataProperty.bind(this, "maputnik:mapbox_access_token")} onChange={onChangeMetadataProperty.bind(this, "maputnik:mapbox_access_token")}
/> />
</InputBlock>
<InputBlock <FieldString {...inputProps}
label={fieldSpecAdditional.maputnik.maptiler_access_token.label} label={fieldSpecAdditional.maputnik.maptiler_access_token.label}
fieldSpec={fieldSpecAdditional.maputnik.maptiler_access_token} fieldSpec={fieldSpecAdditional.maputnik.maptiler_access_token}
> data-wd-key="modal:settings.maputnik:openmaptiles_access_token"
<StringInput {...inputProps}
data-wd-key="modal-settings.maputnik:openmaptiles_access_token"
value={metadata['maputnik:openmaptiles_access_token']} value={metadata['maputnik:openmaptiles_access_token']}
onChange={onChangeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")} onChange={onChangeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
/> />
</InputBlock>
<InputBlock <FieldString {...inputProps}
label={fieldSpecAdditional.maputnik.thunderforest_access_token.label} label={fieldSpecAdditional.maputnik.thunderforest_access_token.label}
fieldSpec={fieldSpecAdditional.maputnik.thunderforest_access_token} fieldSpec={fieldSpecAdditional.maputnik.thunderforest_access_token}
> data-wd-key="modal:settings.maputnik:thunderforest_access_token"
<StringInput {...inputProps}
data-wd-key="modal-settings.maputnik:thunderforest_access_token"
value={metadata['maputnik:thunderforest_access_token']} value={metadata['maputnik:thunderforest_access_token']}
onChange={onChangeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")} onChange={onChangeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
/> />
</InputBlock>
<InputBlock label={"Center"} fieldSpec={latest.$root.center}> <FieldArray
<ArrayInput label={"Center"}
fieldSpec={latest.$root.center}
length={2} length={2}
type="number" type="number"
value={mapStyle.center} value={mapStyle.center}
default={latest.$root.center.default || [0, 0]} default={latest.$root.center.default || [0, 0]}
onChange={this.changeStyleProperty.bind(this, "center")} onChange={this.changeStyleProperty.bind(this, "center")}
/> />
</InputBlock>
<InputBlock label={"Zoom"} fieldSpec={latest.$root.zoom}> <FieldNumber
<NumberInput
{...inputProps} {...inputProps}
label={"Zoom"}
fieldSpec={latest.$root.zoom}
value={mapStyle.zoom} value={mapStyle.zoom}
default={latest.$root.zoom.default || 0} default={latest.$root.zoom.default || 0}
onChange={this.changeStyleProperty.bind(this, "zoom")} onChange={this.changeStyleProperty.bind(this, "zoom")}
/> />
</InputBlock>
<InputBlock label={"Bearing"} fieldSpec={latest.$root.bearing}> <FieldNumber
<NumberInput
{...inputProps} {...inputProps}
label={"Bearing"}
fieldSpec={latest.$root.bearing}
value={mapStyle.bearing} value={mapStyle.bearing}
default={latest.$root.bearing.default} default={latest.$root.bearing.default}
onChange={this.changeStyleProperty.bind(this, "bearing")} onChange={this.changeStyleProperty.bind(this, "bearing")}
/> />
</InputBlock>
<InputBlock label={"Pitch"} fieldSpec={latest.$root.pitch}> <FieldNumber
<NumberInput
{...inputProps} {...inputProps}
label={"Pitch"}
fieldSpec={latest.$root.pitch}
value={mapStyle.pitch} value={mapStyle.pitch}
default={latest.$root.pitch.default} default={latest.$root.pitch.default}
onChange={this.changeStyleProperty.bind(this, "pitch")} onChange={this.changeStyleProperty.bind(this, "pitch")}
/> />
</InputBlock>
<InputBlock label={"Light anchor"} fieldSpec={latest.light.anchor}> <FieldEnum
<EnumInput
{...inputProps} {...inputProps}
label={"Light anchor"}
fieldSpec={latest.light.anchor}
name="light-anchor"
value={light.anchor} value={light.anchor}
options={Object.keys(latest.light.anchor.values)} options={Object.keys(latest.light.anchor.values)}
default={latest.light.anchor.default} default={latest.light.anchor.default}
onChange={this.changeLightProperty.bind(this, "anchor")} onChange={this.changeLightProperty.bind(this, "anchor")}
/> />
</InputBlock>
<InputBlock label={"Light color"} fieldSpec={latest.light.color}> <FieldColor
<ColorField
{...inputProps} {...inputProps}
label={"Light color"}
fieldSpec={latest.light.color}
value={light.color} value={light.color}
default={latest.light.color.default} default={latest.light.color.default}
onChange={this.changeLightProperty.bind(this, "color")} onChange={this.changeLightProperty.bind(this, "color")}
/> />
</InputBlock>
<InputBlock label={"Light intensity"} fieldSpec={latest.light.intensity}> <FieldNumber
<NumberInput
{...inputProps} {...inputProps}
label={"Light intensity"}
fieldSpec={latest.light.intensity}
value={light.intensity} value={light.intensity}
default={latest.light.intensity.default} default={latest.light.intensity.default}
onChange={this.changeLightProperty.bind(this, "intensity")} onChange={this.changeLightProperty.bind(this, "intensity")}
/> />
</InputBlock>
<InputBlock label={"Light position"} fieldSpec={latest.light.position}> <FieldArray
<ArrayInput
{...inputProps} {...inputProps}
label={"Light position"}
fieldSpec={latest.light.position}
type="number" type="number"
length={latest.light.position.length} length={latest.light.position.length}
value={light.position} value={light.position}
default={latest.light.position.default} default={latest.light.position.default}
onChange={this.changeLightProperty.bind(this, "position")} onChange={this.changeLightProperty.bind(this, "position")}
/> />
</InputBlock>
<InputBlock label={"Transition delay"} fieldSpec={latest.transition.delay}> <FieldNumber
<NumberInput
{...inputProps} {...inputProps}
label={"Transition delay"}
fieldSpec={latest.transition.delay}
value={transition.delay} value={transition.delay}
default={latest.transition.delay.default} default={latest.transition.delay.default}
onChange={this.changeTransitionProperty.bind(this, "delay")} onChange={this.changeTransitionProperty.bind(this, "delay")}
/> />
</InputBlock>
<InputBlock label={"Transition duration"} fieldSpec={latest.transition.duration}> <FieldNumber
<NumberInput
{...inputProps} {...inputProps}
label={"Transition duration"}
fieldSpec={latest.transition.duration}
value={transition.duration} value={transition.duration}
default={latest.transition.duration.default} default={latest.transition.duration.default}
onChange={this.changeTransitionProperty.bind(this, "duration")} onChange={this.changeTransitionProperty.bind(this, "duration")}
/> />
</InputBlock>
<InputBlock <FieldSelect {...inputProps}
label={fieldSpecAdditional.maputnik.style_renderer.label} label={fieldSpecAdditional.maputnik.style_renderer.label}
fieldSpec={fieldSpecAdditional.maputnik.style_renderer} fieldSpec={fieldSpecAdditional.maputnik.style_renderer}
> data-wd-key="modal:settings.maputnik:renderer"
<SelectInput {...inputProps}
data-wd-key="modal-settings.maputnik:renderer"
options={[ options={[
['mbgljs', 'MapboxGL JS'], ['mbgljs', 'MapboxGL JS'],
['ol', 'Open Layers (experimental)'], ['ol', 'Open Layers (experimental)'],
@@ -257,13 +247,8 @@ class SettingsModal extends React.Component {
value={metadata['maputnik:renderer'] || 'mbgljs'} value={metadata['maputnik:renderer'] || 'mbgljs'}
onChange={onChangeMetadataProperty.bind(this, 'maputnik:renderer')} onChange={onChangeMetadataProperty.bind(this, 'maputnik:renderer')}
/> />
</InputBlock>
</div> </div>
</Modal> </Modal>
} }
} }
export default SettingsModal

View File

@@ -0,0 +1,132 @@
import React from 'react'
import PropTypes from 'prop-types'
import Modal from './Modal'
export default class ModalShortcuts extends React.Component {
static propTypes = {
isOpen: PropTypes.bool.isRequired,
onOpenToggle: PropTypes.func.isRequired,
}
render() {
const help = [
{
key: <kbd>?</kbd>,
text: "Shortcuts menu"
},
{
key: <kbd>o</kbd>,
text: "Open modal"
},
{
key: <kbd>e</kbd>,
text: "Export modal"
},
{
key: <kbd>d</kbd>,
text: "Data Sources modal"
},
{
key: <kbd>s</kbd>,
text: "Style Settings modal"
},
{
key: <kbd>i</kbd>,
text: "Toggle inspect"
},
{
key: <kbd>m</kbd>,
text: "Focus map"
},
{
key: <kbd>!</kbd>,
text: "Debug modal"
},
]
const mapShortcuts = [
{
key: <kbd>+</kbd>,
text: "Increase the zoom level by 1.",
},
{
key: <><kbd>Shift</kbd> + <kbd>+</kbd></>,
text: "Increase the zoom level by 2.",
},
{
key: <kbd>-</kbd>,
text: "Decrease the zoom level by 1.",
},
{
key: <><kbd>Shift</kbd> + <kbd>-</kbd></>,
text: "Decrease the zoom level by 2.",
},
{
key: <kbd>Up</kbd>,
text: "Pan up by 100 pixels.",
},
{
key: <kbd>Down</kbd>,
text: "Pan down by 100 pixels.",
},
{
key: <kbd>Left</kbd>,
text: "Pan left by 100 pixels.",
},
{
key: <kbd>Right</kbd>,
text: "Pan right by 100 pixels.",
},
{
key: <><kbd>Shift</kbd> + <kbd>Right</kbd></>,
text: "Increase the rotation by 15 degrees.",
},
{
key: <><kbd>Shift</kbd> + <kbd>Left</kbd></>,
text: "Decrease the rotation by 15 degrees."
},
{
key: <><kbd>Shift</kbd> + <kbd>Up</kbd></>,
text: "Increase the pitch by 10 degrees."
},
{
key: <><kbd>Shift</kbd> + <kbd>Down</kbd></>,
text: "Decrease the pitch by 10 degrees."
},
]
return <Modal
data-wd-key="modal:shortcuts"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={'Shortcuts'}
>
<section className="maputnik-modal-section maputnik-modal-shortcuts">
<p>
Press <code>ESC</code> to lose focus of any active elements, then press one of:
</p>
<dl>
{help.map((item, idx) => {
return <div key={idx} className="maputnik-modal-shortcuts__shortcut">
<dt key={"dt"+idx}>{item.key}</dt>
<dd key={"dd"+idx}>{item.text}</dd>
</div>
})}
</dl>
<p>If the Map is in focused you can use the following shortcuts</p>
<ul>
{mapShortcuts.map((item, idx) => {
return <li key={idx}>
<span>{item.key}</span> {item.text}
</li>
})}
</ul>
</section>
</Modal>
}
}

View File

@@ -1,16 +1,16 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import {latest} from '@mapbox/mapbox-gl-style-spec' import {latest} from '@maplibre/maplibre-gl-style-spec'
import Modal from './Modal' import Modal from './Modal'
import Button from '../Button' import InputButton from './InputButton'
import InputBlock from '../inputs/InputBlock' import Block from './Block'
import StringInput from '../inputs/StringInput' import FieldString from './FieldString'
import SelectInput from '../inputs/SelectInput' import FieldSelect from './FieldSelect'
import SourceTypeEditor from '../sources/SourceTypeEditor' import ModalSourcesTypeEditor from './ModalSourcesTypeEditor'
import style from '../../libs/style' import style from '../libs/style'
import { deleteSource, addSource, changeSource } from '../../libs/source' import { deleteSource, addSource, changeSource } from '../libs/source'
import publicSources from '../../config/tilesets.json' import publicSources from '../config/tilesets.json'
import {MdAddCircleOutline, MdDelete} from 'react-icons/md' import {MdAddCircleOutline, MdDelete} from 'react-icons/md'
@@ -24,7 +24,7 @@ class PublicSource extends React.Component {
render() { render() {
return <div className="maputnik-public-source"> return <div className="maputnik-public-source">
<Button <InputButton
className="maputnik-public-source-select" className="maputnik-public-source-select"
onClick={() => this.props.onSelect(this.props.id)} onClick={() => this.props.onSelect(this.props.id)}
> >
@@ -34,7 +34,7 @@ class PublicSource extends React.Component {
</div> </div>
<span className="maputnik-space" /> <span className="maputnik-space" />
<MdAddCircleOutline /> <MdAddCircleOutline />
</Button> </InputButton>
</div> </div>
} }
} }
@@ -69,7 +69,7 @@ function editorMode(source) {
return null return null
} }
class ActiveSourceTypeEditor extends React.Component { class ActiveModalSourcesTypeEditor extends React.Component {
static propTypes = { static propTypes = {
sourceId: PropTypes.string.isRequired, sourceId: PropTypes.string.isRequired,
source: PropTypes.object.isRequired, source: PropTypes.object.isRequired,
@@ -83,16 +83,17 @@ class ActiveSourceTypeEditor extends React.Component {
<div className="maputnik-active-source-type-editor-header"> <div className="maputnik-active-source-type-editor-header">
<span className="maputnik-active-source-type-editor-header-id">#{this.props.sourceId}</span> <span className="maputnik-active-source-type-editor-header-id">#{this.props.sourceId}</span>
<span className="maputnik-space" /> <span className="maputnik-space" />
<Button <InputButton
aria-label={`Remove '${this.props.sourceId}' source`}
className="maputnik-active-source-type-editor-header-delete" className="maputnik-active-source-type-editor-header-delete"
onClick={()=> this.props.onDelete(this.props.sourceId)} onClick={()=> this.props.onDelete(this.props.sourceId)}
style={{backgroundColor: 'transparent'}} style={{backgroundColor: 'transparent'}}
> >
<MdDelete /> <MdDelete />
</Button> </InputButton>
</div> </div>
<div className="maputnik-active-source-type-editor-content"> <div className="maputnik-active-source-type-editor-content">
<SourceTypeEditor <ModalSourcesTypeEditor
onChange={this.props.onChange} onChange={this.props.onChange}
mode={editorMode(this.props.source)} mode={editorMode(this.props.source)}
source={this.props.source} source={this.props.source}
@@ -196,7 +197,7 @@ class AddSource extends React.Component {
render() { render() {
// Kind of a hack because the type changes, however maputnik has 1..n // Kind of a hack because the type changes, however maputnik has 1..n
// options per type, for example // options per type, for example
// //
// - 'geojson' - 'GeoJSON (URL)' and 'GeoJSON (JSON)' // - 'geojson' - 'GeoJSON (URL)' and 'GeoJSON (JSON)'
// - 'raster' - 'Raster (TileJSON URL)' and 'Raster (XYZ URL)' // - 'raster' - 'Raster (TileJSON URL)' and 'Raster (XYZ URL)'
@@ -207,46 +208,46 @@ class AddSource extends React.Component {
}; };
return <div className="maputnik-add-source"> return <div className="maputnik-add-source">
<InputBlock label={"Source ID"} fieldSpec={{doc: "Unique ID that identifies the source and is used in the layer to reference the source."}}> <FieldString
<StringInput label={"Source ID"}
value={this.state.sourceId} fieldSpec={{doc: "Unique ID that identifies the source and is used in the layer to reference the source."}}
onChange={v => this.setState({ sourceId: v})} value={this.state.sourceId}
/> onChange={v => this.setState({ sourceId: v})}
</InputBlock> />
<InputBlock label={"Source Type"} fieldSpec={sourceTypeFieldSpec}> <FieldSelect
<SelectInput label={"Source Type"}
options={[ fieldSpec={sourceTypeFieldSpec}
['geojson_json', 'GeoJSON (JSON)'], options={[
['geojson_url', 'GeoJSON (URL)'], ['geojson_json', 'GeoJSON (JSON)'],
['tilejson_vector', 'Vector (TileJSON URL)'], ['geojson_url', 'GeoJSON (URL)'],
['tilexyz_vector', 'Vector (XYZ URLs)'], ['tilejson_vector', 'Vector (TileJSON URL)'],
['tilejson_raster', 'Raster (TileJSON URL)'], ['tilexyz_vector', 'Vector (XYZ URLs)'],
['tilexyz_raster', 'Raster (XYZ URL)'], ['tilejson_raster', 'Raster (TileJSON URL)'],
['tilejson_raster-dem', 'Raster DEM (TileJSON URL)'], ['tilexyz_raster', 'Raster (XYZ URL)'],
['tilexyz_raster-dem', 'Raster DEM (XYZ URLs)'], ['tilejson_raster-dem', 'Raster DEM (TileJSON URL)'],
['image', 'Image'], ['tilexyz_raster-dem', 'Raster DEM (XYZ URLs)'],
['video', 'Video'], ['image', 'Image'],
]} ['video', 'Video'],
onChange={mode => this.setState({mode: mode, source: this.defaultSource(mode)})} ]}
value={this.state.mode} onChange={mode => this.setState({mode: mode, source: this.defaultSource(mode)})}
/> value={this.state.mode}
</InputBlock> />
<SourceTypeEditor <ModalSourcesTypeEditor
onChange={this.onChangeSource} onChange={this.onChangeSource}
mode={this.state.mode} mode={this.state.mode}
source={this.state.source} source={this.state.source}
/> />
<Button <InputButton
className="maputnik-add-source-button" className="maputnik-add-source-button"
onClick={this.onAdd} onClick={this.onAdd}
> >
Add Source Add Source
</Button> </InputButton>
</div> </div>
} }
} }
class SourcesModal extends React.Component { export default class ModalSources extends React.Component {
static propTypes = { static propTypes = {
mapStyle: PropTypes.object.isRequired, mapStyle: PropTypes.object.isRequired,
isOpen: PropTypes.bool.isRequired, isOpen: PropTypes.bool.isRequired,
@@ -264,7 +265,7 @@ class SourcesModal extends React.Component {
const mapStyle = this.props.mapStyle const mapStyle = this.props.mapStyle
const activeSources = Object.keys(mapStyle.sources).map(sourceId => { const activeSources = Object.keys(mapStyle.sources).map(sourceId => {
const source = mapStyle.sources[sourceId] const source = mapStyle.sources[sourceId]
return <ActiveSourceTypeEditor return <ActiveModalSourcesTypeEditor
key={sourceId} key={sourceId}
sourceId={sourceId} sourceId={sourceId}
source={source} source={source}
@@ -286,37 +287,34 @@ class SourcesModal extends React.Component {
const inputProps = { } const inputProps = { }
return <Modal return <Modal
data-wd-key="modal:sources"
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle} onOpenToggle={this.props.onOpenToggle}
title={'Sources'} title={'Sources'}
> >
<div className="maputnik-modal-section"> <section className="maputnik-modal-section">
<h4>Active Sources</h4> <h1>Active Sources</h1>
{activeSources} {activeSources}
</div> </section>
<div className="maputnik-modal-section"> <section className="maputnik-modal-section">
<h4>Choose Public Source</h4> <h1>Choose Public Source</h1>
<p> <p>
Add one of the publicly available sources to your style. Add one of the publicly available sources to your style.
</p> </p>
<div className="maputnik-public-sources" style={{maxwidth: 500}}> <div className="maputnik-public-sources" style={{maxwidth: 500}}>
{tilesetOptions} {tilesetOptions}
</div> </div>
<p> </section>
<strong>Note:</strong> Some of the tilesets are not optimised for online use, and as a result the file sizes of the tiles can be quite large (heavy) for online vector rendering. Please review any tilesets before use.
</p>
</div>
<div className="maputnik-modal-section"> <section className="maputnik-modal-section">
<h4>Add New Source</h4> <h1>Add New Source</h1>
<p>Add a new source to your style. You can only choose the source type and id at creation time!</p> <p>Add a new source to your style. You can only choose the source type and id at creation time!</p>
<AddSource <AddSource
onAdd={(sourceId, source) => this.props.onStyleChanged(addSource(mapStyle, sourceId, source))} onAdd={(sourceId, source) => this.props.onStyleChanged(addSource(mapStyle, sourceId, source))}
/> />
</div> </section>
</Modal> </Modal>
} }
} }
export default SourcesModal

View File

@@ -0,0 +1,266 @@
import React from 'react'
import PropTypes from 'prop-types'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import Block from './Block'
import FieldUrl from './FieldUrl'
import FieldNumber from './FieldNumber'
import FieldSelect from './FieldSelect'
import FieldDynamicArray from './FieldDynamicArray'
import FieldArray from './FieldArray'
import FieldJson from './FieldJson'
class TileJSONSourceEditor extends React.Component {
static propTypes = {
source: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
children: PropTypes.node,
}
render() {
return <div>
<FieldUrl
label={"TileJSON URL"}
fieldSpec={latest.source_vector.url}
value={this.props.source.url}
onChange={url => this.props.onChange({
...this.props.source,
url: url
})}
/>
{this.props.children}
</div>
}
}
class TileURLSourceEditor extends React.Component {
static propTypes = {
source: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
children: PropTypes.node,
}
changeTileUrls(tiles) {
this.props.onChange({
...this.props.source,
tiles,
})
}
renderTileUrls() {
const tiles = this.props.source.tiles || [];
return <FieldDynamicArray
label={"Tile URL"}
fieldSpec={latest.source_vector.tiles}
type="url"
value={tiles}
onChange={this.changeTileUrls.bind(this)}
/>
}
render() {
return <div>
{this.renderTileUrls()}
<FieldNumber
label={"Min Zoom"}
fieldSpec={latest.source_vector.minzoom}
value={this.props.source.minzoom || 0}
onChange={minzoom => this.props.onChange({
...this.props.source,
minzoom: minzoom
})}
/>
<FieldNumber
label={"Max Zoom"}
fieldSpec={latest.source_vector.maxzoom}
value={this.props.source.maxzoom || 22}
onChange={maxzoom => this.props.onChange({
...this.props.source,
maxzoom: maxzoom
})}
/>
{this.props.children}
</div>
}
}
class ImageSourceEditor extends React.Component {
static propTypes = {
source: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
}
render() {
const changeCoord = (idx, val) => {
const coordinates = this.props.source.coordinates.slice(0);
coordinates[idx] = val;
this.props.onChange({
...this.props.source,
coordinates,
});
}
return <div>
<FieldUrl
label={"Image URL"}
fieldSpec={latest.source_image.url}
value={this.props.source.url}
onChange={url => this.props.onChange({
...this.props.source,
url,
})}
/>
{["top left", "top right", "bottom right", "bottom left"].map((label, idx) => {
return (
<FieldArray
label={`Coord ${label}`}
key={label}
length={2}
type="number"
value={this.props.source.coordinates[idx]}
default={[0, 0]}
onChange={(val) => changeCoord(idx, val)}
/>
);
})}
</div>
}
}
class VideoSourceEditor extends React.Component {
static propTypes = {
source: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
}
render() {
const changeCoord = (idx, val) => {
const coordinates = this.props.source.coordinates.slice(0);
coordinates[idx] = val;
this.props.onChange({
...this.props.source,
coordinates,
});
}
const changeUrls = (urls) => {
this.props.onChange({
...this.props.source,
urls,
});
}
return <div>
<FieldDynamicArray
label={"Video URL"}
fieldSpec={latest.source_video.urls}
type="string"
value={this.props.source.urls}
default={""}
onChange={changeUrls}
/>
{["top left", "top right", "bottom right", "bottom left"].map((label, idx) => {
return (
<FieldArray
label={`Coord ${label}`}
key={label}
length={2}
type="number"
value={this.props.source.coordinates[idx]}
default={[0, 0]}
onChange={val => changeCoord(idx, val)}
/>
);
})}
</div>
}
}
class GeoJSONSourceUrlEditor extends React.Component {
static propTypes = {
source: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
}
render() {
return <FieldUrl
label={"GeoJSON URL"}
fieldSpec={latest.source_geojson.data}
value={this.props.source.data}
onChange={data => this.props.onChange({
...this.props.source,
data: data
})}
/>
}
}
class GeoJSONSourceFieldJsonEditor extends React.Component {
static propTypes = {
source: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
}
render() {
return <Block label={"GeoJSON"} fieldSpec={latest.source_geojson.data}>
<FieldJson
layer={this.props.source.data}
maxHeight={200}
mode={{
name: "javascript",
json: true
}}
lint={true}
onChange={data => {
this.props.onChange({
...this.props.source,
data,
})
}}
/>
</Block>
}
}
export default class ModalSourcesTypeEditor extends React.Component {
static propTypes = {
mode: PropTypes.string.isRequired,
source: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
}
render() {
const commonProps = {
source: this.props.source,
onChange: this.props.onChange,
}
switch(this.props.mode) {
case 'geojson_url': return <GeoJSONSourceUrlEditor {...commonProps} />
case 'geojson_json': return <GeoJSONSourceFieldJsonEditor {...commonProps} />
case 'tilejson_vector': return <TileJSONSourceEditor {...commonProps} />
case 'tilexyz_vector': return <TileURLSourceEditor {...commonProps} />
case 'tilejson_raster': return <TileJSONSourceEditor {...commonProps} />
case 'tilexyz_raster': return <TileURLSourceEditor {...commonProps} />
case 'tilejson_raster-dem': return <TileJSONSourceEditor {...commonProps} />
case 'tilexyz_raster-dem': return <TileURLSourceEditor {...commonProps}>
<FieldSelect
label={"Encoding"}
fieldSpec={latest.source_raster_dem.encoding}
options={Object.keys(latest.source_raster_dem.encoding.values)}
onChange={encoding => this.props.onChange({
...this.props.source,
encoding: encoding
})}
value={this.props.source.encoding || latest.source_raster_dem.encoding.default}
/>
</TileURLSourceEditor>
case 'image': return <ImageSourceEditor {...commonProps} />
case 'video': return <VideoSourceEditor {...commonProps} />
default: return null
}
}
}

View File

@@ -1,12 +1,12 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import Button from '../Button' import InputButton from './InputButton'
import Modal from './Modal' import Modal from './Modal'
import logoImage from 'maputnik-design/logos/logo-color.svg' import logoImage from 'maputnik-design/logos/logo-color.svg'
class SurveyModal extends React.Component { export default class ModalSurvey extends React.Component {
static propTypes = { static propTypes = {
isOpen: PropTypes.bool.isRequired, isOpen: PropTypes.bool.isRequired,
onOpenToggle: PropTypes.func.isRequired, onOpenToggle: PropTypes.func.isRequired,
@@ -20,20 +20,19 @@ class SurveyModal extends React.Component {
render() { render() {
return <Modal return <Modal
data-wd-key="modal-survey" data-wd-key="modal:survey"
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle} onOpenToggle={this.props.onOpenToggle}
title="Maputnik Survey" title="Maputnik Survey"
> >
<div className="maputnik-modal-survey"> <div className="maputnik-modal-survey">
<div className="maputnik-modal-survey__logo" dangerouslySetInnerHTML={{__html: logoImage}} /> <img src={logoImage} className="maputnik-modal-survey__logo" />
<h1>You + Maputnik = Maputnik better for you</h1> <h1>You + Maputnik = Maputnik better for you</h1>
<p className="maputnik-modal-survey__description">We dont track you, so we dont know how you use Maputnik. Help us make Maputnik better for you by completing a 7minute survey carried out by our contributing designer.</p> <p className="maputnik-modal-survey__description">We dont track you, so we dont know how you use Maputnik. Help us make Maputnik better for you by completing a 7minute survey carried out by our contributing designer.</p>
<Button onClick={this.onClick} className="maputnik-big-button maputnik-white-button maputnik-wide-button">Take the Maputnik Survey</Button> <InputButton onClick={this.onClick} className="maputnik-big-button maputnik-white-button maputnik-wide-button">Take the Maputnik Survey</InputButton>
<p className="maputnik-modal-survey__footnote">It takes 7 minutes, tops! Every question is optional.</p> <p className="maputnik-modal-survey__footnote">It takes 7 minutes, tops! Every question is optional.</p>
</div> </div>
</Modal> </Modal>
} }
} }
export default SurveyModal

View File

@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import FunctionSpecField from './FunctionSpecField' import FieldFunction from './FieldFunction'
const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image'] const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image']
/** Extract field spec by {@fieldName} from the {@layerType} in the /** Extract field spec by {@fieldName} from the {@layerType} in the
@@ -58,7 +58,7 @@ export default class PropertyGroup extends React.Component {
const fieldValue = fieldName in paint ? paint[fieldName] : layout[fieldName] const fieldValue = fieldName in paint ? paint[fieldName] : layout[fieldName]
const fieldType = fieldName in paint ? 'paint' : 'layout'; const fieldType = fieldName in paint ? 'paint' : 'layout';
return <FunctionSpecField return <FieldFunction
errors={errors} errors={errors}
onChange={this.onPropertyChange} onChange={this.onPropertyChange}
key={fieldName} key={fieldName}

View File

@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
class ScrollContainer extends React.Component { export default class ScrollContainer extends React.Component {
static propTypes = { static propTypes = {
children: PropTypes.node children: PropTypes.node
} }
@@ -13,4 +13,3 @@ class ScrollContainer extends React.Component {
} }
} }
export default ScrollContainer

View File

@@ -1,10 +1,10 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { otherFilterOps } from '../../libs/filterops.js' import { otherFilterOps } from '../libs/filterops.js'
import StringInput from '../inputs/StringInput' import InputString from './InputString'
import AutocompleteInput from '../inputs/AutocompleteInput' import InputAutocomplete from './InputAutocomplete'
import SelectInput from '../inputs/SelectInput' import InputSelect from './InputSelect'
function tryParseInt(v) { function tryParseInt(v) {
if (v === '') return v if (v === '') return v
@@ -35,7 +35,7 @@ function parseFilter(v) {
return v; return v;
} }
class SingleFilterEditor extends React.Component { export default class SingleFilterEditor extends React.Component {
static propTypes = { static propTypes = {
filter: PropTypes.array.isRequired, filter: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
@@ -64,14 +64,16 @@ class SingleFilterEditor extends React.Component {
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">
<AutocompleteInput <InputAutocomplete
aria-label="key"
value={propertyName} value={propertyName}
options={Object.keys(this.props.properties).map(propName => [propName, propName])} options={Object.keys(this.props.properties).map(propName => [propName, propName])}
onChange={newPropertyName => this.onFilterPartChanged(filterOp, newPropertyName, filterArgs)} onChange={newPropertyName => this.onFilterPartChanged(filterOp, newPropertyName, filterArgs)}
/> />
</div> </div>
<div className="maputnik-filter-editor-operator"> <div className="maputnik-filter-editor-operator">
<SelectInput <InputSelect
aria-label="function"
value={filterOp} value={filterOp}
onChange={newFilterOp => this.onFilterPartChanged(newFilterOp, propertyName, filterArgs)} onChange={newFilterOp => this.onFilterPartChanged(newFilterOp, propertyName, filterArgs)}
options={otherFilterOps} options={otherFilterOps}
@@ -79,7 +81,8 @@ class SingleFilterEditor extends React.Component {
</div> </div>
{filterArgs.length > 0 && {filterArgs.length > 0 &&
<div className="maputnik-filter-editor-args"> <div className="maputnik-filter-editor-args">
<StringInput <InputString
aria-label="value"
value={filterArgs.join(',')} value={filterArgs.join(',')}
onChange={ v=> this.onFilterPartChanged(filterOp, propertyName, v.split(','))} onChange={ v=> this.onFilterPartChanged(filterOp, propertyName, v.split(','))}
/> />
@@ -90,4 +93,3 @@ class SingleFilterEditor extends React.Component {
} }
export default SingleFilterEditor

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types'
import './SmallError.scss'; import './SmallError.scss';
class SmallError extends React.Component { export default class SmallError extends React.Component {
static propTypes = { static propTypes = {
children: PropTypes.node, children: PropTypes.node,
} }
@@ -17,4 +17,3 @@ class SmallError extends React.Component {
} }
} }
export default SmallError

View File

@@ -1,4 +1,4 @@
@import '../../styles/vars'; @import '../styles/vars';
.SmallError { .SmallError {
color: #E57373; color: #E57373;

View File

@@ -0,0 +1,51 @@
import React from 'react'
import PropTypes from 'prop-types'
import Block from './Block'
import InputSpec from './InputSpec'
import Fieldset from './Fieldset'
const typeMap = {
color: () => Block,
enum: ({fieldSpec}) => (Object.keys(fieldSpec.values).length <= 3 ? Fieldset : Block),
number: () => Block,
boolean: () => Block,
array: () => Fieldset,
resolvedImage: () => Block,
number: () => Block,
string: () => Block,
formatted: () => Block,
};
export default class SpecField extends React.Component {
static propTypes = {
...InputSpec.propTypes,
name: PropTypes.string,
}
render() {
const {props} = this;
const fieldType = props.fieldSpec.type;
const typeBlockFn = typeMap[fieldType];
let TypeBlock;
if (typeBlockFn) {
TypeBlock = typeBlockFn(props);
}
else {
console.warn("No such type for '%s'", fieldType);
TypeBlock = Block;
}
return <TypeBlock
label={props.label}
action={props.action}
fieldSpec={this.props.fieldSpec}
>
<InputSpec {...props} />
</TypeBlock>
}
}

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