diff --git a/.circleci/config.yml b/.circleci/config.yml index 7e27134..6450a6e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -50,11 +50,6 @@ templates: path: /tmp/artifacts destination: /artifacts jobs: - build-linux-node-v8: - docker: - - image: node:8 - working_directory: ~/repo-linux-node-v8 - steps: *build-steps build-linux-node-v10: docker: - image: node:10 @@ -71,14 +66,6 @@ jobs: - image: node:13 working_directory: ~/repo-linux-node-v13 steps: *build-steps - build-osx-node-v8: - macos: - xcode: "9.0" - dependencies: - override: - - brew install node@8 - working_directory: ~/repo-osx-node-v8 - steps: *build-steps build-osx-node-v10: macos: xcode: "9.0" @@ -108,11 +95,9 @@ workflows: version: 2 build: jobs: - - build-linux-node-v8 - build-linux-node-v10 - build-linux-node-v12 - build-linux-node-v13 - - build-osx-node-v8 - build-osx-node-v10 - build-osx-node-v12 - build-osx-node-v13 diff --git a/appveyor.yml b/appveyor.yml index 4bfc59e..5c6a57d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,7 +1,6 @@ -image: Visual Studio 2015 +image: Visual Studio 2019 environment: matrix: - - nodejs_version: "8" - nodejs_version: "10" - nodejs_version: "12" - nodejs_version: "13" @@ -18,7 +17,7 @@ install: Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version) $env:platform } - md public - - npm --vs2015 install --global windows-build-tools + - npm install --global windows-build-tools - npm install build_script: - npm run build diff --git a/package-lock.json b/package-lock.json index 0fc459b..223a485 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9070,12 +9070,9 @@ } }, "react-collapse": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/react-collapse/-/react-collapse-4.0.3.tgz", - "integrity": "sha512-OO4NhtEqFtz+1ma31J1B7+ezdRnzHCZiTGSSd/Pxoks9hxrZYhzFEddeYt05A/1477xTtdrwo7xEa2FLJyWGCQ==", - "requires": { - "prop-types": "^15.5.8" - } + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/react-collapse/-/react-collapse-5.0.1.tgz", + "integrity": "sha512-cN2tkxBWizhPQ2JHfe0aUSJtmMthKA17NZkTElpiQ2snQAAi1hssXZ2fv88rAPNNvG5ss4t0PbOZT0TIl9Lk3Q==" }, "react-color": { "version": "2.17.3", diff --git a/package.json b/package.json index c08f087..a8f02d0 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "react-aria-modal": "^4.0.0", "react-autobind": "^1.0.6", "react-autocomplete": "^1.8.1", - "react-collapse": "^4.0.3", + "react-collapse": "^5.0.1", "react-color": "^2.17.3", "react-dom": "^16.10.2", "react-file-reader-input": "^2.0.0", diff --git a/src/codemirror-maputnik.css b/src/codemirror-maputnik.css index 83b653b..b43c6c0 100644 --- a/src/codemirror-maputnik.css +++ b/src/codemirror-maputnik.css @@ -17,7 +17,7 @@ } .cm-s-maputnik .CodeMirror-cursor { - border-left: solid thin #8e8e8e !important; + border-left: solid thin #f0f0f0 !important; } .cm-s-maputnik.CodeMirror-focused div.CodeMirror-selected { @@ -47,5 +47,11 @@ } .cm-s-maputnik .CodeMirror-matchingbracket { - text-decoration: underline; color: white !important; + background-color: #f0f0f0; + color: #565659 !important; +} + +.cm-s-maputnik .CodeMirror-nonmatchingbracket { + background-color: #bb0000; + color: white !important; } diff --git a/src/components/fields/FunctionSpecField.jsx b/src/components/fields/FunctionSpecField.jsx index 36d7930..e2542c5 100644 --- a/src/components/fields/FunctionSpecField.jsx +++ b/src/components/fields/FunctionSpecField.jsx @@ -14,6 +14,25 @@ function isDataField(value) { return typeof value === 'object' && value.stops && typeof value.property !== 'undefined' } +/** + * 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 * https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property */ @@ -82,8 +101,8 @@ export default class FunctionSpecProperty extends React.Component { makeZoomFunction = () => { const zoomFunc = { stops: [ - [6, this.props.value], - [10, this.props.value] + [6, this.props.value || findDefaultFromSpec(this.props.fieldSpec)], + [10, this.props.value || findDefaultFromSpec(this.props.fieldSpec)] ] } this.props.onChange(this.props.fieldName, zoomFunc) @@ -96,8 +115,8 @@ export default class FunctionSpecProperty extends React.Component { property: "", type: functionType, stops: [ - [{zoom: 6, value: stopValue}, this.props.value || stopValue], - [{zoom: 10, value: stopValue}, this.props.value || stopValue] + [{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) diff --git a/src/components/fields/SpecField.jsx b/src/components/fields/SpecField.jsx index 1f2aac6..92c7fc3 100644 --- a/src/components/fields/SpecField.jsx +++ b/src/components/fields/SpecField.jsx @@ -11,7 +11,7 @@ import ArrayInput from '../inputs/ArrayInput' import DynamicArrayInput from '../inputs/DynamicArrayInput' import FontInput from '../inputs/FontInput' import IconInput from '../inputs/IconInput' -import EnumInput from '../inputs/SelectInput' +import EnumInput from '../inputs/EnumInput' import capitalize from 'lodash.capitalize' const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image'] @@ -75,6 +75,7 @@ export default class SpecField extends React.Component { {...commonProps} options={options} /> + case 'resolvedImage': case 'formatted': case 'string': if(iconProperties.indexOf(this.props.fieldName) >= 0) { diff --git a/src/components/fields/_DataProperty.jsx b/src/components/fields/_DataProperty.jsx index 4116031..acfa2f6 100644 --- a/src/components/fields/_DataProperty.jsx +++ b/src/components/fields/_DataProperty.jsx @@ -8,11 +8,32 @@ import StringInput from '../inputs/StringInput' import SelectInput from '../inputs/SelectInput' import DocLabel from './DocLabel' import InputBlock from '../inputs/InputBlock' +import docUid from '../../libs/document-uid' +import sortNumerically from '../../libs/sort-numerically' import labelFromFieldName from './_labelFromFieldName' import DeleteStopButton from './_DeleteStopButton' + +function setStopRefs(props, state) { + // This is initialsed below only if required to improved performance. + let newRefs; + + if(props.value && props.value.stops) { + props.value.stops.forEach((val, idx) => { + if(!state.refs.hasOwnProperty(idx)) { + if(!newRefs) { + newRefs = {...state}; + } + newRefs[idx] = docUid("stop-"); + } + }) + } + + return newRefs; +} + export default class DataProperty extends React.Component { static propTypes = { onChange: PropTypes.func, @@ -29,6 +50,30 @@ export default class DataProperty extends React.Component { ]), } + state = { + refs: {} + } + + componentDidMount() { + const newRefs = setStopRefs(this.props, this.state); + + if(newRefs) { + this.setState({ + refs: newRefs + }) + } + } + + static getDerivedStateFromProps(props, state) { + const newRefs = setStopRefs(props, state); + if(newRefs) { + return { + refs: newRefs + }; + } + return null; + } + getFieldFunctionType(fieldSpec) { if (fieldSpec.expression.interpolated) { return "exponential" @@ -48,14 +93,42 @@ export default class DataProperty extends React.Component { } } + // Order the stops altering the refs to reflect their new position. + orderStopsByZoom(stops) { + const mappedWithRef = stops + .map((stop, idx) => { + return { + ref: this.state.refs[idx], + data: stop + } + }) + // Sort by zoom + .sort((a, b) => sortNumerically(a.data[0].zoom, b.data[0].zoom)); + + // Fetch the new position of the stops + const newRefs = {}; + mappedWithRef + .forEach((stop, idx) =>{ + newRefs[idx] = stop.ref; + }) + + this.setState({ + refs: newRefs + }); + + return mappedWithRef.map((item) => item.data); + } changeStop(changeIdx, stopData, value) { const stops = this.props.value.stops.slice(0) const changedStop = stopData.zoom === undefined ? stopData.value : stopData stops[changeIdx] = [changedStop, value] + + const orderedStops = this.orderStopsByZoom(stops); + const changedValue = { ...this.props.value, - stops: stops, + stops: orderedStops, } this.props.onChange(this.props.fieldName, changedValue) } @@ -77,6 +150,7 @@ export default class DataProperty extends React.Component { const dataFields = this.props.value.stops.map((stop, idx) => { const zoomLevel = typeof stop[0] === 'object' ? stop[0].zoom : undefined; + const key = this.state.refs[idx]; const dataLevel = typeof stop[0] === 'object' ? stop[0].value : stop[0]; const value = stop[1] const deleteStopBtn = @@ -107,7 +181,7 @@ export default class DataProperty extends React.Component { } - return + return {zoomInput}
{dataInput} diff --git a/src/components/inputs/StringInput.jsx b/src/components/inputs/StringInput.jsx index e160d69..87bdd31 100644 --- a/src/components/inputs/StringInput.jsx +++ b/src/components/inputs/StringInput.jsx @@ -8,10 +8,15 @@ class StringInput extends React.Component { style: PropTypes.object, default: PropTypes.string, onChange: PropTypes.func, + onInput: PropTypes.func, multi: PropTypes.bool, required: PropTypes.bool, } + static defaultProps = { + onInput: () => {}, + } + constructor(props) { super(props) this.state = { @@ -57,7 +62,9 @@ class StringInput extends React.Component { this.setState({ editing: true, value: e.target.value - }) + }, () => { + this.props.onInput(this.state.value); + }); }, onBlur: () => { if(this.state.value!==this.props.value) { diff --git a/src/components/inputs/UrlInput.jsx b/src/components/inputs/UrlInput.jsx new file mode 100644 index 0000000..335d3ff --- /dev/null +++ b/src/components/inputs/UrlInput.jsx @@ -0,0 +1,77 @@ +import React from 'react' +import PropTypes from 'prop-types' +import StringInput from './StringInput' +import SmallError from '../util/SmallError' + + +function validate (url) { + let error; + const getProtocol = (url) => { + try { + const urlObj = new URL(url); + return urlObj.protocol; + } + catch (err) { + return undefined; + } + }; + const protocol = getProtocol(url); + if ( + protocol && + protocol === "http:" && + window.location.protocol === "https:" + ) { + error = ( + + CORS policy won't allow fetching resources served over http from https, use a https:// domain + + ); + } + + return error; +} + +class UrlInput extends React.Component { + static propTypes = { + "data-wd-key": PropTypes.string, + value: PropTypes.string, + style: PropTypes.object, + default: PropTypes.string, + onChange: PropTypes.func, + onInput: PropTypes.func, + multi: PropTypes.bool, + required: PropTypes.bool, + } + + static defaultProps = { + onInput: () => {}, + } + + constructor (props) { + super(props); + this.state = { + error: validate(props.value) + }; + } + + onInput = (url) => { + this.setState({ + error: validate(url) + }); + this.props.onInput(url); + } + + render () { + return ( +
+ + {this.state.error} +
+ ); + } +} + +export default UrlInput diff --git a/src/components/layers/JSONEditor.jsx b/src/components/layers/JSONEditor.jsx index a8c0ce5..e0a99ec 100644 --- a/src/components/layers/JSONEditor.jsx +++ b/src/components/layers/JSONEditor.jsx @@ -7,6 +7,7 @@ import CodeMirror from 'codemirror'; import 'codemirror/mode/javascript/javascript' import 'codemirror/addon/lint/lint' +import 'codemirror/addon/edit/matchbrackets' import 'codemirror/lib/codemirror.css' import 'codemirror/addon/lint/lint.css' import '../../codemirror-maputnik.css' @@ -47,6 +48,7 @@ class JSONEditor extends React.Component { viewportMargin: Infinity, lineNumbers: true, lint: true, + matchBrackets: true, gutters: ["CodeMirror-lint-markers"], scrollbarStyle: "null", }); @@ -105,17 +107,6 @@ class JSONEditor extends React.Component { } render() { - const codeMirrorOptions = { - mode: {name: "javascript", json: true}, - tabSize: 2, - theme: 'maputnik', - viewportMargin: Infinity, - lineNumbers: true, - lint: true, - gutters: ["CodeMirror-lint-markers"], - scrollbarStyle: "null", - } - const style = {}; if (this.props.maxHeight) { style.maxHeight = this.props.maxHeight; diff --git a/src/components/map/MapboxGlMap.jsx b/src/components/map/MapboxGlMap.jsx index d5b221f..794dbc7 100644 --- a/src/components/map/MapboxGlMap.jsx +++ b/src/components/map/MapboxGlMap.jsx @@ -19,8 +19,7 @@ const IS_SUPPORTED = MapboxGl.supported(); function renderPopup(popup, mountNode) { ReactDOM.render(popup, mountNode); - var content = mountNode.innerHTML; - return content; + return mountNode; } function buildInspectStyle(originalMapStyle, coloredLayers, highlightedLayer) { @@ -87,11 +86,9 @@ export default class MapboxGlMap extends React.Component { const metadata = props.mapStyle.metadata || {} MapboxGl.accessToken = metadata['maputnik:mapbox_access_token'] || tokens.mapbox - if(!props.inspectModeEnabled) { - //Mapbox GL now does diffing natively so we don't need to calculate - //the necessary operations ourselves! - this.state.map.setStyle(props.mapStyle, { diff: true}) - } + //Mapbox GL now does diffing natively so we don't need to calculate + //the necessary operations ourselves! + this.state.map.setStyle(props.mapStyle, {diff: true}) } componentDidUpdate(prevProps) { @@ -102,6 +99,9 @@ export default class MapboxGlMap extends React.Component { this.updateMapFromProps(this.props); if(this.props.inspectModeEnabled !== prevProps.inspectModeEnabled) { + // HACK: Fix for , while we wait for a proper fix. + // eslint-disable-next-line + this.state.inspect._popupBlocked = false; this.state.inspect.toggleInspector() } if(this.props.inspectModeEnabled) { @@ -123,6 +123,7 @@ export default class MapboxGlMap extends React.Component { container: this.container, style: this.props.mapStyle, hash: true, + maxZoom: 24 } const map = new MapboxGl.Map(mapOpts); @@ -180,6 +181,10 @@ export default class MapboxGlMap extends React.Component { }) }) + map.on("error", e => { + console.log("ERROR", e); + }) + map.on("zoom", e => { this.setState({ zoom: map.getZoom() diff --git a/src/components/modals/OpenModal.jsx b/src/components/modals/OpenModal.jsx index d2c6477..67f289d 100644 --- a/src/components/modals/OpenModal.jsx +++ b/src/components/modals/OpenModal.jsx @@ -4,6 +4,7 @@ import LoadingModal from './LoadingModal' import Modal from './Modal' import Button from '../Button' import FileReaderInput from 'react-file-reader-input' +import UrlInput from '../inputs/UrlInput' import {MdFileUpload} from 'react-icons/md' import {MdAddCircleOutline} from 'react-icons/md' @@ -122,9 +123,8 @@ class OpenModal extends React.Component { }) } - onOpenUrl = () => { - const url = this.styleUrlElement.value; - this.onStyleSelect(url); + onOpenUrl = (url) => { + this.onStyleSelect(this.state.styleUrl); } onUpload = (_, files) => { @@ -160,9 +160,9 @@ class OpenModal extends React.Component { this.props.onOpenToggle(); } - onChangeUrl = () => { + onChangeUrl = (url) => { this.setState({ - styleUrl: this.styleUrlElement.value + styleUrl: url, }); } @@ -209,14 +209,13 @@ class OpenModal extends React.Component {

Load from a URL. Note that the URL must have CORS enabled.

- this.styleUrlElement = input} className="maputnik-input" - placeholder="Enter URL..." + default="Enter URL..." value={this.state.styleUrl} - onChange={this.onChangeUrl} + onInput={this.onChangeUrl} />
diff --git a/src/components/sources/SourceTypeEditor.jsx b/src/components/sources/SourceTypeEditor.jsx index 83672e1..8ed4672 100644 --- a/src/components/sources/SourceTypeEditor.jsx +++ b/src/components/sources/SourceTypeEditor.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types' import {latest} from '@mapbox/mapbox-gl-style-spec' import InputBlock from '../inputs/InputBlock' import StringInput from '../inputs/StringInput' +import UrlInput from '../inputs/UrlInput' import NumberInput from '../inputs/NumberInput' import SelectInput from '../inputs/SelectInput' import JSONEditor from '../layers/JSONEditor' @@ -18,7 +19,7 @@ class TileJSONSourceEditor extends React.Component { render() { return
- this.props.onChange({ ...this.props.source, @@ -52,7 +53,7 @@ class TileURLSourceEditor extends React.Component { const tiles = this.props.source.tiles || [] return tiles.map((tileUrl, tileIndex) => { return - @@ -95,7 +96,7 @@ class GeoJSONSourceUrlEditor extends React.Component { render() { return - this.props.onChange({ ...this.props.source, diff --git a/src/components/util/SmallError.jsx b/src/components/util/SmallError.jsx new file mode 100644 index 0000000..03d9c78 --- /dev/null +++ b/src/components/util/SmallError.jsx @@ -0,0 +1,20 @@ +import React from 'react' +import PropTypes from 'prop-types' +import './SmallError.scss'; + + +class SmallError extends React.Component { + static propTypes = { + children: PropTypes.node, + } + + render () { + return ( +
+ Error: {this.props.children} +
+ ); + } +} + +export default SmallError diff --git a/src/components/util/SmallError.scss b/src/components/util/SmallError.scss new file mode 100644 index 0000000..1111282 --- /dev/null +++ b/src/components/util/SmallError.scss @@ -0,0 +1,7 @@ +@import '../../styles/vars'; + +.SmallError { + color: #E57373; + font-size: $font-size-6; + margin-top: $margin-2 +} diff --git a/src/config/styles.json b/src/config/styles.json index c0a6595..ff0e407 100644 --- a/src/config/styles.json +++ b/src/config/styles.json @@ -1,40 +1,40 @@ [ - { - "id": "klokantech-basic", - "title": "Klokantech Basic", - "url": "https://cdn.jsdelivr.net/gh/openmaptiles/klokantech-basic-gl-style@e142f83/style.json", - "thumbnail": "https://maputnik.github.io/thumbnails/klokantech-basic.png" - }, - { - "id": "dark-matter", - "title": "Dark Matter", - "url": "https://cdn.jsdelivr.net/gh/openmaptiles/dark-matter-gl-style@1dcc1d3/style.json", - "thumbnail": "https://maputnik.github.io/thumbnails/dark-matter.png" - }, - { - "id": "positron", - "title": "Positron", - "url": "https://cdn.jsdelivr.net/gh/openmaptiles/positron-gl-style@2877814/style.json", - "thumbnail": "https://maputnik.github.io/thumbnails/positron.png" - }, - { - "id": "osm-bright", - "title": "OSM Bright", - "url": "https://cdn.jsdelivr.net/gh/openmaptiles/osm-bright-gl-style@500e26e/style.json", - "thumbnail": "https://maputnik.github.io/thumbnails/osm-bright.png" - }, - { - "id": "toner-gl-style", - "title": "Toner", - "url": "https://cdn.jsdelivr.net/gh/openmaptiles/toner-gl-style@bb49571/style.json", - "thumbnail": "https://maputnik.github.io/thumbnails/toner.png" - }, { "id": "osm-liberty", "title": "OSM Liberty", "url": "https://maputnik.github.io/osm-liberty/style.json", "thumbnail": "https://maputnik.github.io/thumbnails/osm-liberty.png" }, + { + "id": "klokantech-basic", + "title": "Klokantech Basic", + "url": "https://cdn.jsdelivr.net/gh/openmaptiles/klokantech-basic-gl-style@v1.9/style.json", + "thumbnail": "https://maputnik.github.io/thumbnails/klokantech-basic.png" + }, + { + "id": "dark-matter", + "title": "Dark Matter", + "url": "https://cdn.jsdelivr.net/gh/openmaptiles/dark-matter-gl-style@v1.8/style.json", + "thumbnail": "https://maputnik.github.io/thumbnails/dark-matter.png" + }, + { + "id": "positron", + "title": "Positron", + "url": "https://cdn.jsdelivr.net/gh/openmaptiles/positron-gl-style@v1.8/style.json", + "thumbnail": "https://maputnik.github.io/thumbnails/positron.png" + }, + { + "id": "osm-bright", + "title": "OSM Bright", + "url": "https://cdn.jsdelivr.net/gh/openmaptiles/osm-bright-gl-style@v1.9/style.json", + "thumbnail": "https://maputnik.github.io/thumbnails/osm-bright.png" + }, + { + "id": "toner-gl-style", + "title": "Toner", + "url": "https://cdn.jsdelivr.net/gh/openmaptiles/toner-gl-style@dcb6e64/style.json", + "thumbnail": "https://maputnik.github.io/thumbnails/toner.png" + }, { "id": "os-zoomstack-outdoor", "title": "Zoomstack Outdoor", diff --git a/src/styles/_modal.scss b/src/styles/_modal.scss index 2f6b1b7..f15cf79 100644 --- a/src/styles/_modal.scss +++ b/src/styles/_modal.scss @@ -280,3 +280,7 @@ color: $color-green; margin-top: 16px; } + +.modal-settings { + width: 400px; +} diff --git a/src/styles/_react-collapse.scss b/src/styles/_react-collapse.scss index 3ee0cae..ce8db33 100644 --- a/src/styles/_react-collapse.scss +++ b/src/styles/_react-collapse.scss @@ -7,3 +7,7 @@ flex: 1; } } + +.ReactCollapse--collapse { + transition: height 180ms; +} diff --git a/src/styles/_vars.scss b/src/styles/_vars.scss new file mode 100644 index 0000000..7cb3d17 --- /dev/null +++ b/src/styles/_vars.scss @@ -0,0 +1,23 @@ +$color-black: #191b20; +$color-gray: #222429; +$color-midgray: #303237; +$color-lowgray: #a4a4a4; +$color-white: #f0f0f0; +$color-red: #cf4a4a; +$color-green: #53b972; +$margin-1: 3px; +$margin-2: 5px; +$margin-3: 10px; +$margin-4: 30px; +$margin-5: 40px; +$font-size-1: 24px; +$font-size-2: 20px; +$font-size-3: 18px; +$font-size-4: 16px; +$font-size-5: 14px; +$font-size-6: 12px; +$font-family: Roboto, sans-serif; + +$toolbar-height: 40px; +$toolbar-offset: 0; + diff --git a/src/styles/index.scss b/src/styles/index.scss index 49af915..759b73c 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -1,26 +1,4 @@ -$color-black: #191b20; -$color-gray: #222429; -$color-midgray: #303237; -$color-lowgray: #a4a4a4; -$color-white: #f0f0f0; -$color-red: #cf4a4a; -$color-green: #53b972; -$margin-1: 3px; -$margin-2: 5px; -$margin-3: 10px; -$margin-4: 30px; -$margin-5: 40px; -$font-size-1: 24px; -$font-size-2: 20px; -$font-size-3: 18px; -$font-size-4: 16px; -$font-size-5: 14px; -$font-size-6: 12px; -$font-family: Roboto, sans-serif; - -$toolbar-height: 40px; -$toolbar-offset: 0; - +@import 'vars'; @import 'mixins'; @import 'reset'; @import 'base';