diff --git a/package.json b/package.json index 5471a5dd..aed9b823 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,8 @@ "license": "MIT", "homepage": "https://github.com/maputnik/editor#readme", "dependencies": { - "@mapbox/mapbox-gl-style-spec": "^9.0.0", "@mapbox/mapbox-gl-rtl-text": "^0.1.0", + "@mapbox/mapbox-gl-style-spec": "^9.0.1", "classnames": "^2.2.5", "codemirror": "^5.18.2", "color": "^1.0.3", @@ -32,8 +32,9 @@ "lodash.clonedeep": "^4.5.0", "lodash.isequal": "^4.4.0", "lodash.throttle": "^4.1.1", - "mapbox-gl": "^0.34.0", + "mapbox-gl": "^0.40.1", "mapbox-gl-inspect": "^1.2.3", + "maputnik-design": "github:maputnik/design", "mousetrap": "^1.6.0", "ol-mapbox-style": "1.0.1", "openlayers": "^3.19.1", @@ -41,7 +42,7 @@ "react-addons-pure-render-mixin": "^15.4.0", "react-autocomplete": "^1.4.0", "react-codemirror": "^0.3.0", - "react-collapse": "^4.0.2", + "react-collapse": "^4.0.3", "react-color": "^2.10.0", "react-dom": "^15.4.0", "react-file-reader-input": "^1.1.0", diff --git a/src/components/Toolbar.jsx b/src/components/Toolbar.jsx index bcb60f8d..9bf7ad68 100644 --- a/src/components/Toolbar.jsx +++ b/src/components/Toolbar.jsx @@ -16,7 +16,7 @@ import MdFontDownload from 'react-icons/lib/md/font-download' import HelpIcon from 'react-icons/lib/md/help-outline' import InspectionIcon from 'react-icons/lib/md/find-in-page' -import logoImage from '../img/maputnik.png' +import logoImage from 'maputnik-design/logos/logo-color.svg' import SettingsModal from './modals/SettingsModal' import ExportModal from './modals/ExportModal' import SourcesModal from './modals/SourcesModal' diff --git a/src/components/fields/FunctionSpecField.jsx b/src/components/fields/FunctionSpecField.jsx new file mode 100644 index 00000000..2b7383b9 --- /dev/null +++ b/src/components/fields/FunctionSpecField.jsx @@ -0,0 +1,352 @@ +import React from 'react' +import Color from 'color' + +import Button from '../Button' +import SpecField from './SpecField' +import NumberInput from '../inputs/NumberInput' +import StringInput from '../inputs/StringInput' +import SelectInput from '../inputs/SelectInput' +import DocLabel from './DocLabel' +import InputBlock from '../inputs/InputBlock' + +import AddIcon from 'react-icons/lib/md/add-circle-outline' +import DeleteIcon from 'react-icons/lib/md/delete' +import FunctionIcon from 'react-icons/lib/md/functions' +import MdInsertChart from 'react-icons/lib/md/insert-chart' + +import capitalize from 'lodash.capitalize' + +function isZoomField(value) { + return typeof value === 'object' && value.stops && typeof value.property === 'undefined' +} + +function isDataField(value) { + return typeof value === 'object' && value.stops && typeof value.property !== 'undefined' +} + +/** Supports displaying spec field for zoom function objects + * https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property + */ +export default class FunctionSpecProperty extends React.Component { + static propTypes = { + onChange: React.PropTypes.func.isRequired, + fieldName: React.PropTypes.string.isRequired, + fieldSpec: React.PropTypes.object.isRequired, + + value: React.PropTypes.oneOfType([ + React.PropTypes.object, + React.PropTypes.string, + React.PropTypes.number, + React.PropTypes.bool, + React.PropTypes.array + ]), + } + + addStop() { + const stops = this.props.value.stops.slice(0) + const lastStop = stops[stops.length - 1] + if (typeof lastStop[0] === "object") { + stops.push([ + {zoom: lastStop[0].zoom + 1, value: lastStop[0].value}, + lastStop[1] + ]) + } + else { + stops.push([lastStop[0] + 1, lastStop[1]]) + } + + const changedValue = { + ...this.props.value, + stops: stops, + } + + this.props.onChange(this.props.fieldName, changedValue) + } + + deleteStop(stopIdx) { + const stops = this.props.value.stops.slice(0) + stops.splice(stopIdx, 1) + + let changedValue = { + ...this.props.value, + stops: stops, + } + + if(stops.length === 1) { + changedValue = stops[0][1] + } + + this.props.onChange(this.props.fieldName, changedValue) + } + + makeZoomFunction() { + const zoomFunc = { + stops: [ + [6, this.props.value], + [10, this.props.value] + ] + } + this.props.onChange(this.props.fieldName, zoomFunc) + } + + getDataFunctionTypes(functionType) { + if (functionType === "interpolated") { + return ["categorical", "interval", "exponential"] + } + else { + return ["categorical", "interval"] + } + } + + makeDataFunction() { + const dataFunc = { + property: "", + type: "categorical", + stops: [ + [{zoom: 6, value: 0}, this.props.value], + [{zoom: 10, value: 0}, this.props.value] + ] + } + this.props.onChange(this.props.fieldName, dataFunc) + } + + changeStop(changeIdx, stopData, value) { + const stops = this.props.value.stops.slice(0) + stops[changeIdx] = [stopData, value] + const changedValue = { + ...this.props.value, + stops: stops, + } + this.props.onChange(this.props.fieldName, changedValue) + } + + changeDataProperty(propName, propVal) { + if (propVal) { + this.props.value[propName] = propVal + } + else { + delete this.props.value[propName] + } + this.props.onChange(this.props.fieldName, this.props.value) + } + + renderDataProperty() { + const dataFields = this.props.value.stops.map((stop, idx) => { + const zoomLevel = stop[0].zoom + const dataLevel = stop[0].value + const value = stop[1] + const deleteStopBtn = + + const dataProps = { + label: "Data value", + value: dataLevel, + onChange: newData => this.changeStop(idx, { zoom: zoomLevel, value: newData }, value) + } + const dataInput = this.props.value.type === "categorical" ? : + + return +
+ this.changeStop(idx, {zoom: newZoom, value: dataLevel}, value)} + min={0} + max={22} + /> +
+
+ {dataInput} +
+
+ this.changeStop(idx, {zoom: zoomLevel, value: dataLevel}, newValue)} + /> +
+
+ }) + + return
+
+ +
+ +
+ this.changeDataProperty("property", propVal)} + /> +
+
+
+ +
+ this.changeDataProperty("type", propVal)} + options={this.getDataFunctionTypes(this.props.fieldSpec.function)} + /> +
+
+
+ +
+ this.changeDataProperty("default", propVal)} + /> +
+
+
+
+ {dataFields} + +
+ } + + renderZoomProperty() { + const zoomFields = this.props.value.stops.map((stop, idx) => { + const zoomLevel = stop[0] + const value = stop[1] + const deleteStopBtn= + + return +
+
+ this.changeStop(idx, changedStop, value)} + min={0} + max={22} + /> +
+
+ this.changeStop(idx, zoomLevel, newValue)} + /> +
+
+
+ }) + + return
+ {zoomFields} + +
+ } + + renderProperty() { + const functionBtn = + return + + + } + + render() { + const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property" + let specField + if (isZoomField(this.props.value)) { + specField = this.renderZoomProperty() + } + else if (isDataField(this.props.value)) { + specField = this.renderDataProperty() + } + else { + specField = this.renderProperty() + } + return
+ {specField} +
+ } +} + +function MakeFunctionButtons(props) { + let makeZoomButton, makeDataButton + if (props.fieldSpec['zoom-function']) { + makeZoomButton = + + if (props.fieldSpec['property-function'] && ['piecewise-constant', 'interpolated'].indexOf(props.fieldSpec['function']) !== -1) { + makeDataButton = + } + return
{makeDataButton}{makeZoomButton}
+ } + else { + return null + } +} + +function DeleteStopButton(props) { + return +} + +function labelFromFieldName(fieldName) { + let label = fieldName.split('-').slice(1).join(' ') + return capitalize(label) +} diff --git a/src/components/fields/PropertyGroup.jsx b/src/components/fields/PropertyGroup.jsx index 8de8b342..fc178929 100644 --- a/src/components/fields/PropertyGroup.jsx +++ b/src/components/fields/PropertyGroup.jsx @@ -1,6 +1,6 @@ import React from 'react' -import ZoomSpecField from './ZoomSpecField' +import FunctionSpecField from './FunctionSpecField' const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image'] /** Extract field spec by {@fieldName} from the {@layerType} in the @@ -54,7 +54,7 @@ export default class PropertyGroup extends React.Component { const layout = this.props.layer.layout || {} const fieldValue = fieldName in paint ? paint[fieldName] : layout[fieldName] - return { - const zoomLevel = stop[0] - const value = stop[1] - const deleteStopBtn= - - return -
-
- this.changeStop(idx, changedStop, value)} - min={0} - max={22} - /> -
-
- this.changeStop(idx, zoomLevel, newValue)} - /> -
-
-
- }) - - return
- {zoomFields} - -
- } - - renderProperty() { - let zoomBtn = null - if(this.props.fieldSpec['zoom-function']) { - zoomBtn = - } - return - - - } - - render() { - const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property" - return
- {isZoomField(this.props.value) ? this.renderZoomProperty() : this.renderProperty()} -
- } -} - -function MakeZoomFunctionButton(props) { - return -} - -function DeleteStopButton(props) { - return -} - -function labelFromFieldName(fieldName) { - let label = fieldName.split('-').slice(1).join(' ') - return capitalize(label) -} diff --git a/src/components/filter/SingleFilterEditor.jsx b/src/components/filter/SingleFilterEditor.jsx index 8902625b..f7af98d4 100644 --- a/src/components/filter/SingleFilterEditor.jsx +++ b/src/components/filter/SingleFilterEditor.jsx @@ -11,6 +11,29 @@ function tryParseInt(v) { return parseFloat(v) } +function tryParseBool(v) { + const isString = (typeof(v) === "string"); + if(!isString) { + return v; + } + + if(v.match(/^\s*true\s*$/)) { + return true; + } + else if(v.match(/^\s*false\s*$/)) { + return false; + } + else { + return v; + } +} + +function parseFilter(v) { + v = tryParseInt(v); + v = tryParseBool(v); + return v; +} + class SingleFilterEditor extends React.Component { static propTypes = { filter: React.PropTypes.array.isRequired, @@ -23,7 +46,7 @@ class SingleFilterEditor extends React.Component { } onFilterPartChanged(filterOp, propertyName, filterArgs) { - let newFilter = [filterOp, propertyName, ...filterArgs.map(tryParseInt)] + let newFilter = [filterOp, propertyName, ...filterArgs.map(parseFilter)] if(filterOp === 'has' || filterOp === '!has') { newFilter = [filterOp, propertyName] } else if(filterArgs.length === 0) { diff --git a/src/components/layers/CommentBlock.jsx b/src/components/layers/CommentBlock.jsx index 996ca1ca..ac8bee61 100644 --- a/src/components/layers/CommentBlock.jsx +++ b/src/components/layers/CommentBlock.jsx @@ -10,7 +10,7 @@ class MetadataBlock extends React.Component { } render() { - return + return -
{this.props.children}
+
+
{this.props.children}
+
} diff --git a/src/styles/_components.scss b/src/styles/_components.scss index b4032a5d..12415faa 100644 --- a/src/styles/_components.scss +++ b/src/styles/_components.scss @@ -40,6 +40,7 @@ .maputnik-doc-target:hover .maputnik-doc-popup { display: block; + text-align: left; } // BUTTON @@ -104,13 +105,17 @@ .maputnik-action-block { .maputnik-input-block-label { display: inline-block; - width: 43%; + width: 35%; } .maputnik-input-block-action { vertical-align: top; display: inline-block; - width: 7%; + width: 15%; + } + + .maputnik-input-block-action > div { + text-align: right; } } diff --git a/src/styles/_modal.scss b/src/styles/_modal.scss index 69ee2e49..56740608 100644 --- a/src/styles/_modal.scss +++ b/src/styles/_modal.scss @@ -2,6 +2,7 @@ .maputnik-modal { min-width: 350px; max-width: 600px; + overflow: hidden; background-color: $color-black; box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.3); z-index: 3; @@ -39,9 +40,13 @@ cursor: pointer; } +.maputnik-modal-scroller { + max-height: calc(100vh - 35px); + overflow-y: auto; +} + .maputnik-modal-content { padding: $margin-3; - max-height: 90vh; @include flex-column; } @@ -80,7 +85,6 @@ } .maputnik-style-gallery-container { - overflow-y: scroll; flex-shrink: 1; } diff --git a/src/styles/_toolbar.scss b/src/styles/_toolbar.scss index e9459645..77dbea87 100644 --- a/src/styles/_toolbar.scss +++ b/src/styles/_toolbar.scss @@ -22,9 +22,8 @@ img { width: 30px; - height: 30px; padding-right: $margin-2; - vertical-align: middle; + vertical-align: top; } } diff --git a/src/styles/_zoomproperty.scss b/src/styles/_zoomproperty.scss index 22984961..d6510c9b 100644 --- a/src/styles/_zoomproperty.scss +++ b/src/styles/_zoomproperty.scss @@ -67,3 +67,68 @@ .maputnik-zoom-spec-property .maputnik-input-block:not(:first-child) .maputnik-input-block-label { visibility: hidden; } + +// DATA FUNC +.maputnik-make-data-function { + background-color: transparent; + display: inline-block; + padding-bottom: 0; + padding-top: 0; + vertical-align: middle; + + @extend .maputnik-icon-button; +} + +// DATA PROPERTY +.maputnik-data-spec-block { + overflow: auto; +} + +.maputnik-data-spec-property { + .maputnik-input-block-label { + width: 30%; + } + + .maputnik-input-block-content { + width: 70%; + } + + .maputnik-data-spec-property-group { + margin-bottom: 3%; + + .maputnik-doc-wrapper { + width: 25%; + color: $color-lowgray; + } + + .maputnik-doc-wrapper:hover { + color: inherit; + } + + .maputnik-data-spec-property-input { + width: 75%; + display: inline-block; + + .maputnik-string { + margin-bottom: 3%; + } + } + } +} + +.maputnik-data-spec-block { + .maputnik-data-spec-property-stop-edit, + .maputnik-data-spec-property-stop-data { + display: inline-block; + margin-bottom: 3%; + } + + .maputnik-data-spec-property-stop-edit { + width: 18%; + margin-right: 3%; + } + + .maputnik-data-spec-property-stop-data { + width: 78%; + } +} diff --git a/test/specs/simple.js b/test/specs/simple.js index 56b27695..fb6ca499 100644 --- a/test/specs/simple.js +++ b/test/specs/simple.js @@ -9,7 +9,7 @@ describe('maputnik', function() { browser.waitForExist(".maputnik-toolbar-link"); var src = browser.getAttribute(".maputnik-toolbar-link img", "src"); - assert.equal(src, config.baseUrl+'/img/maputnik.png'); + assert.equal(src, config.baseUrl+'/img/logo-color.svg'); }); });