Add lint to CI and fix errors (#853)

Adds lint to CI and fixes errors.
I'm not sure I'm fully proud of all the solutions there.
But there's no lint issues and the lint is being checked as part of the
CI.

---------

Co-authored-by: Yuri Astrakhan <yuriastrakhan@gmail.com>
This commit is contained in:
Harel M
2023-12-26 23:13:22 +02:00
committed by GitHub
parent a324ddb654
commit a22476cab2
55 changed files with 831 additions and 691 deletions

45
.eslintrc Normal file
View File

@@ -0,0 +1,45 @@
{
"root": true,
"env": {
"browser": true,
"es2020": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:react-hooks/recommended",
],
"ignorePatterns": [
"dist"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"settings": {
"react": { "version": "16.4" }
},
"plugins": [
"@typescript-eslint",
"react-refresh"],
"rules": {
"react-refresh/only-export-components": [
"warn",
{ "allowConstantExport": true }
],
"@typescript-eslint/no-unused-vars": [
"warn",
{ "argsIgnorePattern": "^_" }
],
"no-unused-vars": "off",
"react/prop-types": ["off"],
// Disable no-undef. It's covered by @typescript-eslint
"no-undef": "off",
"indent": ["error", 2]
},
"globals": {
"global": "readonly"
}
}

View File

@@ -1,20 +0,0 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: '18.2' } },
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

View File

@@ -42,6 +42,9 @@ jobs:
${{ runner.os }}-node-
- run: npm ci
- run: npm run build
- run: npm run lint
- run: npm run lint-css
build-artifacts:

383
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@
"scripts": {
"start": "vite",
"build": "tsc && vite build",
"lint": "eslint ./src --ext ts,tsx,js,jsx --report-unused-disable-directives --max-warnings 0 && npm run lint-css",
"lint": "eslint ./src --ext ts,tsx,js,jsx --report-unused-disable-directives --max-warnings 0",
"test": "cypress run",
"cy:open": "cypress open",
"lint-css": "stylelint \"src/styles/*.scss\"",
@@ -26,6 +26,8 @@
"@maplibre/maplibre-gl-style-spec": "^17.0.1",
"@mdi/js": "^6.6.96",
"@mdi/react": "^1.5.0",
"@typescript-eslint/eslint-plugin": "^6.16.0",
"@typescript-eslint/parser": "^6.16.0",
"array-move": "^4.0.0",
"buffer": "^6.0.3",
"classnames": "^2.3.1",
@@ -103,6 +105,7 @@
"@types/color": "^3.0.6",
"@types/cors": "^2.8.17",
"@types/file-saver": "^2.0.7",
"@types/geojson": "^7946.0.13",
"@types/json-to-ast": "^2.1.4",
"@types/lodash.capitalize": "^4.2.9",
"@types/lodash.clamp": "^4.0.9",
@@ -124,10 +127,10 @@
"@vitejs/plugin-react": "^4.2.0",
"cors": "^2.8.5",
"cypress": "^13.6.1",
"eslint": "^8.53.0",
"eslint": "^8.56.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.4",
"eslint-plugin-react-refresh": "^0.4.5",
"express": "^4.17.3",
"istanbul": "^0.4.5",
"istanbul-lib-coverage": "^3.2.0",

View File

@@ -334,7 +334,7 @@ export default class App extends React.Component<any, AppState> {
const metadata: {[key: string]: string} = this.state.mapStyle.metadata || {} as any
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
let glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate;
const glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate;
downloadGlyphsMetadata(glyphUrl, fonts => {
this.setState({ spec: updateRootSpec(this.state.spec, 'glyphs', fonts)})
})
@@ -402,7 +402,7 @@ export default class App extends React.Component<any, AppState> {
// Special case: Duplicate layer id
const dupMatch = error.message.match(/layers\[(\d+)\]: (duplicate layer id "?(.*)"?, previously used)/);
if (dupMatch) {
const [_matchStr, index, message] = dupMatch;
const [, index, message] = dupMatch;
return {
message: error.message,
parsed: {
@@ -419,7 +419,7 @@ export default class App extends React.Component<any, AppState> {
// Special case: Invalid source
const invalidSourceMatch = error.message.match(/layers\[(\d+)\]: (source "(?:.*)" not found)/);
if (invalidSourceMatch) {
const [_matchStr, index, message] = invalidSourceMatch;
const [, index, message] = invalidSourceMatch;
return {
message: error.message,
parsed: {
@@ -435,7 +435,7 @@ export default class App extends React.Component<any, AppState> {
const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/);
if (layerMatch) {
const [_matchStr, index, group, property, message] = layerMatch;
const [, index, group, property, message] = layerMatch;
const key = (group && property) ? [group, property].join(".") : property;
return {
message: error.message,
@@ -466,7 +466,7 @@ export default class App extends React.Component<any, AppState> {
try {
const objPath = message.split(":")[0];
// Errors can be deply nested for example 'layers[0].filter[1][1][0]' we only care upto the property 'layers[0].filter'
const unsetPath = objPath.match(/^\S+?\[\d+\]\.[^\[]+/)![0];
const unsetPath = objPath.match(/^\S+?\[\d+\]\.[^[]+/)![0];
unset(dirtyMapStyle, unsetPath);
}
catch (err) {
@@ -547,14 +547,14 @@ export default class App extends React.Component<any, AppState> {
}
onLayerDestroy = (index: number) => {
let layers = this.state.mapStyle.layers;
const layers = this.state.mapStyle.layers;
const remainingLayers = layers.slice(0);
remainingLayers.splice(index, 1);
this.onLayersChange(remainingLayers);
}
onLayerCopy = (index: number) => {
let layers = this.state.mapStyle.layers;
const layers = this.state.mapStyle.layers;
const changedLayers = layers.slice(0)
const clonedLayer = cloneDeep(changedLayers[index])
@@ -564,7 +564,7 @@ export default class App extends React.Component<any, AppState> {
}
onLayerVisibilityToggle = (index: number) => {
let layers = this.state.mapStyle.layers;
const layers = this.state.mapStyle.layers;
const changedLayers = layers.slice(0)
const layer = { ...changedLayers[index] }
@@ -624,11 +624,11 @@ export default class App extends React.Component<any, AppState> {
fetchSources() {
const sourceList: {[key: string]: any} = {};
for(let [key, val] of Object.entries(this.state.mapStyle.sources)) {
for(const [key, val] of Object.entries(this.state.mapStyle.sources)) {
if(
!this.state.sources.hasOwnProperty(key) &&
!Object.prototype.hasOwnProperty.call(this.state.sources, key) &&
val.type === "vector" &&
val.hasOwnProperty("url")
Object.prototype.hasOwnProperty.call(val, "url")
) {
sourceList[key] = {
type: val.type,
@@ -646,30 +646,30 @@ export default class App extends React.Component<any, AppState> {
fetch(url!, {
mode: 'cors',
})
.then(response => response.json())
.then(json => {
.then(response => response.json())
.then(json => {
if(!json.hasOwnProperty("vector_layers")) {
return;
}
if(!Object.prototype.hasOwnProperty.call(json, "vector_layers")) {
return;
}
// Create new objects before setState
const sources = Object.assign({}, {
[key]: this.state.sources[key],
// Create new objects before setState
const sources = Object.assign({}, {
[key]: this.state.sources[key],
});
for(const layer of json.vector_layers) {
(sources[key] as any).layers.push(layer.id)
}
console.debug("Updating source: "+key);
this.setState({
sources: sources
});
})
.catch(err => {
console.error("Failed to process sources for '%s'", url, err);
});
for(let layer of json.vector_layers) {
(sources[key] as any).layers.push(layer.id)
}
console.debug("Updating source: "+key);
this.setState({
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];
@@ -760,8 +760,8 @@ export default class App extends React.Component<any, AppState> {
url.searchParams.set("layer", `${hashVal}~${selectedLayerIndex}`);
const openModals = Object.entries(isOpen)
.map(([key, val]) => (val === true ? key : null))
.filter(val => val !== null);
.map(([key, val]) => (val === true ? key : null))
.filter(val => val !== null);
if (openModals.length > 0) {
url.searchParams.set("modal", openModals.join(","));

View File

@@ -35,8 +35,8 @@ class AppLayout extends React.Component<AppLayoutProps> {
</div>
{this.props.map}
{this.props.bottom && <div className="maputnik-layout-bottom">
{this.props.bottom}
</div>
{this.props.bottom}
</div>
}
{this.props.modals}
</div>

View File

@@ -56,7 +56,7 @@ export default class Block extends React.Component<BlockProps, BlockState> {
if (event.nativeEvent.target.nodeName !== "INPUT" && !contains) {
event.stopPropagation();
}
event.preventDefault();
event.preventDefault();
}
render() {

View File

@@ -71,7 +71,7 @@ export default class Doc extends React.Component<DocProps> {
<tr key={key}>
<td>{key}</td>
{Object.keys(headers).map((k) => {
if (supportObj.hasOwnProperty(k)) {
if (Object.prototype.hasOwnProperty.call(supportObj, k)) {
return <td key={k}>{supportObj[k as keyof typeof headers]}</td>;
}
else {

View File

@@ -40,7 +40,7 @@ function isIdentityProperty(value: any) {
return (
typeof(value) === 'object' &&
value.type === "identity" &&
value.hasOwnProperty("property")
Object.prototype.hasOwnProperty.call(value, "property")
);
}

View File

@@ -1,6 +1,7 @@
import React, { ReactElement } from 'react'
import FieldDocLabel from './FieldDocLabel'
import Doc from './Doc'
import generateUniqueId from '../libs/document-uid';
type FieldsetProps = {
label?: string,
@@ -12,14 +13,12 @@ type FieldsetState = {
showDoc: boolean
};
let IDX = 0;
export default class Fieldset extends React.Component<FieldsetProps, FieldsetState> {
_labelId: string;
constructor (props: FieldsetProps) {
super(props);
this._labelId = `fieldset_label_${(IDX++)}`;
this._labelId = generateUniqueId(`fieldset_label_`);
this.state = {
showDoc: false,
}

View File

@@ -16,7 +16,7 @@ import ExpressionProperty from './_ExpressionProperty';
function combiningFilter(props: FilterEditorProps): LegacyFilterSpecification | ExpressionSpecification {
let filter = props.filter || ['all'];
const filter = props.filter || ['all'];
if (!Array.isArray(filter)) {
return filter;
@@ -148,7 +148,7 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
}
makeExpression = () => {
let filter = combiningFilter(this.props);
const filter = combiningFilter(this.props);
this.props.onChange(migrateFilter(filter));
this.setState({
displaySimpleFilter: false,
@@ -205,8 +205,8 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
}
else if (displaySimpleFilter) {
const filter = combiningFilter(this.props);
let combiningOp = filter[0];
let filters = filter.slice(1) as (LegacyFilterSpecification | ExpressionSpecification)[];
const combiningOp = filter[0];
const filters = filter.slice(1) as (LegacyFilterSpecification | ExpressionSpecification)[];
const actions = (
<div>
@@ -282,7 +282,7 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
);
}
else {
let {filter} = this.props;
const {filter} = this.props;
return (
<>

View File

@@ -17,16 +17,16 @@ export default class IconLayer extends React.Component<IconLayerProps> {
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} />
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

@@ -91,7 +91,7 @@ export default class InputAutocomplete extends React.Component<InputAutocomplete
"maputnik-autocomplete-menu-item-selected": isHighlighted,
})}
>
{item[1]}
{item[1]}
</div>
)}
/>

View File

@@ -27,8 +27,8 @@ export default class InputCheckbox extends React.Component<InputCheckboxProps> {
/>
<div className="maputnik-checkbox-box">
<svg style={{
display: this.props.value ? 'inline' : 'none'
}} className="maputnik-checkbox-icon" viewBox='0 0 32 32'>
display: this.props.value ? 'inline' : 'none'
}} className="maputnik-checkbox-icon" viewBox='0 0 32 32'>
<path d='M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z' />
</svg>
</div>

View File

@@ -87,8 +87,8 @@ export default class InputColor extends React.Component<InputColorProps> {
const picker = <div
className="maputnik-color-picker-offset"
style={{
position: 'fixed',
zIndex: 1,
position: 'fixed',
zIndex: 1,
left: offset.left,
top: offset.top,
}}>
@@ -110,7 +110,7 @@ export default class InputColor extends React.Component<InputColorProps> {
/>
</div>
var swatchStyle = {
const swatchStyle = {
backgroundColor: this.props.value
};

View File

@@ -100,7 +100,7 @@ export default class FieldDynamicArray extends React.Component<FieldDynamicArray
style={this.props.style}
key={i}
className="maputnik-array-block"
>
>
<div className="maputnik-array-block-action">
{deleteValueBtn}
</div>

View File

@@ -1,6 +1,5 @@
import React, { BaseSyntheticEvent } from 'react'
let IDX = 0;
import generateUniqueId from '../libs/document-uid';
export type InputNumberProps = {
value?: number
@@ -32,7 +31,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
constructor(props: InputNumberProps) {
super(props)
this.state = {
uuid: IDX++,
uuid: +generateUniqueId(),
editing: false,
value: props.value,
dirtyValue: props.value,
@@ -140,7 +139,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
}
else {
value = value + (step - snap);
};
}
}
}
@@ -155,7 +154,8 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
render() {
if(
this.props.hasOwnProperty("min") && this.props.hasOwnProperty("max") &&
Object.prototype.hasOwnProperty.call(this.props, "min") &&
Object.prototype.hasOwnProperty.call(this.props, "max") &&
this.props.min !== undefined && this.props.max !== undefined &&
this.props.allowRange
) {

View File

@@ -53,66 +53,67 @@ export default class SpecField extends React.Component<SpecFieldProps> {
'aria-label': this.props['aria-label'],
}
switch(this.props.fieldSpec?.type) {
case 'number': return (
<InputNumber
{...commonProps as InputNumberProps}
min={this.props.fieldSpec.minimum}
max={this.props.fieldSpec.maximum}
/>
)
case 'enum':
const options = Object.keys(this.props.fieldSpec.values || []).map(v => [v, capitalize(v)])
case 'number': return (
<InputNumber
{...commonProps as InputNumberProps}
min={this.props.fieldSpec.minimum}
max={this.props.fieldSpec.maximum}
/>
)
case 'enum': {
const options = Object.keys(this.props.fieldSpec.values || []).map(v => [v, capitalize(v)])
return <InputEnum
{...commonProps as Omit<InputEnumProps, "options">}
options={options}
return <InputEnum
{...commonProps as Omit<InputEnumProps, "options">}
options={options}
/>
}
case 'resolvedImage':
case 'formatted':
case 'string':
if (iconProperties.indexOf(this.props.fieldName!) >= 0) {
const options = this.props.fieldSpec.values || [];
return <InputAutocomplete
{...commonProps as Omit<InputAutocompleteProps, "options">}
options={options.map(f => [f, f])}
/>
case 'resolvedImage':
case 'formatted':
case 'string':
if (iconProperties.indexOf(this.props.fieldName!) >= 0) {
const options = this.props.fieldSpec.values || [];
return <InputAutocomplete
{...commonProps as Omit<InputAutocompleteProps, "options">}
options={options.map(f => [f, f])}
} else {
return <InputString
{...commonProps as InputStringProps}
/>
}
case 'color': return (
<InputColor
{...commonProps as InputColorProps}
/>
)
case 'boolean': return (
<InputCheckbox
{...commonProps as InputCheckboxProps}
/>
)
case 'array':
if(this.props.fieldName === 'text-font') {
return <InputFont
{...commonProps as FieldFontProps}
fonts={this.props.fieldSpec.values}
/>
} else {
if (this.props.fieldSpec.length) {
return <InputArray
{...commonProps as FieldArrayProps}
type={this.props.fieldSpec.value}
length={this.props.fieldSpec.length}
/>
} else {
return <InputString
{...commonProps as InputStringProps}
return <InputDynamicArray
{...commonProps as FieldDynamicArrayProps}
fieldSpec={this.props.fieldSpec}
type={this.props.fieldSpec.value as FieldDynamicArrayProps['type']}
/>
}
case 'color': return (
<InputColor
{...commonProps as InputColorProps}
/>
)
case 'boolean': return (
<InputCheckbox
{...commonProps as InputCheckboxProps}
/>
)
case 'array':
if(this.props.fieldName === 'text-font') {
return <InputFont
{...commonProps as FieldFontProps}
fonts={this.props.fieldSpec.values}
/>
} else {
if (this.props.fieldSpec.length) {
return <InputArray
{...commonProps as FieldArrayProps}
type={this.props.fieldSpec.value}
length={this.props.fieldSpec.length}
/>
} else {
return <InputDynamicArray
{...commonProps as FieldDynamicArrayProps}
fieldSpec={this.props.fieldSpec}
type={this.props.fieldSpec.value as FieldDynamicArrayProps['type']}
/>
}
}
default: return null
}
default: return null
}
}

View File

@@ -46,7 +46,7 @@ export default class InputString extends React.Component<InputStringProps, Input
let tag;
let classes;
if(!!this.props.multi) {
if(this.props.multi) {
tag = "textarea"
classes = [
"maputnik-string",
@@ -60,14 +60,14 @@ export default class InputString extends React.Component<InputStringProps, Input
]
}
if(!!this.props.disabled) {
if(this.props.disabled) {
classes.push("maputnik-string--disabled");
}
return React.createElement(tag, {
"aria-label": this.props["aria-label"],
"data-wd-key": this.props["data-wd-key"],
spellCheck: this.props.hasOwnProperty("spellCheck") ? this.props.spellCheck : !(tag === "input"),
spellCheck: Object.prototype.hasOwnProperty.call(this.props, "spellCheck") ? this.props.spellCheck : !(tag === "input"),
disabled: this.props.disabled,
className: classes.join(" "),
style: this.props.style,

View File

@@ -26,8 +26,8 @@ function validate(url: string) {
<SmallError>
Must provide protocol {
isSsl
? <code>https://</code>
: <><code>http://</code> or <code>https://</code></>
? <code>https://</code>
: <><code>http://</code> or <code>https://</code></>
}
</SmallError>
);

View File

@@ -1,4 +1,4 @@
import React from 'react'
import React, {type JSX} from 'react'
import PropTypes from 'prop-types'
import { Wrapper, Button, Menu, MenuItem } from 'react-aria-menubutton'
import {Accordion} from 'react-accessible-accordion';
@@ -150,87 +150,87 @@ export default class LayerEditor extends React.Component<LayerEditorProps, Layer
let sourceLayerIds;
const layer = this.props.layer as Exclude<LayerSpecification, BackgroundLayerSpecification>;
if(this.props.sources.hasOwnProperty(layer.source)) {
if(Object.prototype.hasOwnProperty.call(this.props.sources, layer.source)) {
sourceLayerIds = (this.props.sources[layer.source] as any).layers;
}
switch(type) {
case 'layer': return <div>
<FieldId
value={this.props.layer.id}
wdKey="layer-editor.layer-id"
error={errorData.id}
onChange={newId => this.props.onLayerIdChange(this.props.layerIndex, this.props.layer.id, newId)}
/>
<FieldType
disabled={true}
error={errorData.type}
value={this.props.layer.type}
onChange={newType => this.props.onLayerChanged(
this.props.layerIndex,
changeType(this.props.layer, newType)
)}
/>
{this.props.layer.type !== 'background' && <FieldSource
error={errorData.source}
sourceIds={Object.keys(this.props.sources!)}
value={this.props.layer.source}
onChange={v => this.changeProperty(null, 'source', v)}
/>
}
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.props.layer.type) < 0 &&
case 'layer': return <div>
<FieldId
value={this.props.layer.id}
wdKey="layer-editor.layer-id"
error={errorData.id}
onChange={newId => this.props.onLayerIdChange(this.props.layerIndex, this.props.layer.id, newId)}
/>
<FieldType
disabled={true}
error={errorData.type}
value={this.props.layer.type}
onChange={newType => this.props.onLayerChanged(
this.props.layerIndex,
changeType(this.props.layer, newType)
)}
/>
{this.props.layer.type !== 'background' && <FieldSource
error={errorData.source}
sourceIds={Object.keys(this.props.sources!)}
value={this.props.layer.source}
onChange={v => this.changeProperty(null, 'source', v)}
/>
}
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.props.layer.type) < 0 &&
<FieldSourceLayer
error={errorData['source-layer']}
sourceLayerIds={sourceLayerIds}
value={(this.props.layer as any)['source-layer']}
onChange={v => this.changeProperty(null, 'source-layer', v)}
/>
}
<FieldMinZoom
error={errorData.minzoom}
value={this.props.layer.minzoom}
onChange={v => this.changeProperty(null, 'minzoom', v)}
/>
<FieldMaxZoom
error={errorData.maxzoom}
value={this.props.layer.maxzoom}
onChange={v => this.changeProperty(null, 'maxzoom', v)}
/>
<FieldComment
error={errorData.comment}
value={comment}
onChange={v => this.changeProperty('metadata', 'maputnik:comment', v == "" ? undefined : v)}
/>
</div>
case 'filter': return <div>
<div className="maputnik-filter-editor-wrapper">
<FilterEditor
errors={errorData}
filter={(this.props.layer as any).filter}
properties={this.props.vectorLayers[(this.props.layer as any)['source-layer']]}
onChange={f => this.changeProperty(null, 'filter', f)}
/>
</div>
</div>
case 'properties':
return <PropertyGroup
}
<FieldMinZoom
error={errorData.minzoom}
value={this.props.layer.minzoom}
onChange={v => this.changeProperty(null, 'minzoom', v)}
/>
<FieldMaxZoom
error={errorData.maxzoom}
value={this.props.layer.maxzoom}
onChange={v => this.changeProperty(null, 'maxzoom', v)}
/>
<FieldComment
error={errorData.comment}
value={comment}
onChange={v => this.changeProperty('metadata', 'maputnik:comment', v == "" ? undefined : v)}
/>
</div>
case 'filter': return <div>
<div className="maputnik-filter-editor-wrapper">
<FilterEditor
errors={errorData}
layer={this.props.layer}
groupFields={fields!}
spec={this.props.spec}
onChange={this.changeProperty.bind(this)}
filter={(this.props.layer as any).filter}
properties={this.props.vectorLayers[(this.props.layer as any)['source-layer']]}
onChange={f => this.changeProperty(null, 'filter', f)}
/>
case 'jsoneditor':
return <FieldJson
layer={this.props.layer}
onChange={(layer) => {
this.props.onLayerChanged(
this.props.layerIndex,
layer
);
}}
/>
default: return <></>
</div>
</div>
case 'properties':
return <PropertyGroup
errors={errorData}
layer={this.props.layer}
groupFields={fields!}
spec={this.props.spec}
onChange={this.changeProperty.bind(this)}
/>
case 'jsoneditor':
return <FieldJson
layer={this.props.layer}
onChange={(layer) => {
this.props.onLayerChanged(
this.props.layerIndex,
layer
);
}}
/>
default: return <></>
}
}

View File

@@ -5,10 +5,10 @@ import {
mdiMenuUp
} from '@mdi/js';
import {
AccordionItem,
AccordionItemHeading,
AccordionItemButton,
AccordionItemPanel,
AccordionItem,
AccordionItemHeading,
AccordionItemButton,
AccordionItemPanel,
} from 'react-accessible-accordion';

View File

@@ -1,4 +1,4 @@
import React from 'react'
import React, {type JSX} from 'react'
import classnames from 'classnames'
import lodash from 'lodash';
@@ -8,26 +8,8 @@ import ModalAdd from './ModalAdd'
import {SortEndHandler, SortableContainer} from 'react-sortable-hoc';
import type {LayerSpecification} from 'maplibre-gl';
function layerPrefix(name: string) {
return name.replace(' ', '-').replace('_', '-').split('-')[0]
}
function findClosestCommonPrefix(layers: LayerSpecification[], idx: number) {
const currentLayerPrefix = layerPrefix(layers[idx].id)
let closestIdx = idx
for (let i = idx; i > 0; i--) {
const previousLayerPrefix = layerPrefix(layers[i-1].id)
if(previousLayerPrefix === currentLayerPrefix) {
closestIdx = i - 1
} else {
return closestIdx
}
}
return closestIdx
}
let UID = 0;
import generateUniqueId from '../libs/document-uid';
import { findClosestCommonPrefix, layerPrefix } from '../libs/layer';
type LayerListContainerProps = {
layers: LayerSpecification[]
@@ -64,7 +46,7 @@ class LayerListContainer extends React.Component<LayerListContainerProps, LayerL
collapsedGroups: {},
areAllGroupsExpanded: false,
keys: {
add: UID++,
add: +generateUniqueId(),
},
isOpen: {
add: false,
@@ -76,7 +58,7 @@ class LayerListContainer extends React.Component<LayerListContainerProps, LayerL
this.setState({
keys: {
...this.state.keys,
[modalName]: UID++,
[modalName]: +generateUniqueId(),
},
isOpen: {
...this.state.isOpen,
@@ -88,7 +70,7 @@ class LayerListContainer extends React.Component<LayerListContainerProps, LayerL
toggleLayers = () => {
let idx = 0
let newGroups: {[key:string]: boolean} = {}
const newGroups: {[key:string]: boolean} = {}
this.groupedLayers().forEach(layers => {
const groupPrefix = layerPrefix(layers[0].id)
@@ -284,12 +266,12 @@ class LayerListContainer extends React.Component<LayerListContainerProps, LayerL
ref={this.scrollContainerRef}
>
<ModalAdd
key={this.state.keys.add}
layers={this.props.layers}
sources={this.props.sources}
isOpen={this.state.isOpen.add}
onOpenToggle={this.toggleModal.bind(this, 'add')}
onLayersChange={this.props.onLayersChange}
key={this.state.keys.add}
layers={this.props.layers}
sources={this.props.sources}
isOpen={this.state.isOpen.add}
onOpenToggle={this.toggleModal.bind(this, 'add')}
onLayersChange={this.props.onLayersChange}
/>
<header className="maputnik-layer-list-header">
<span className="maputnik-layer-list-header-title">Layers</span>
@@ -328,6 +310,7 @@ class LayerListContainer extends React.Component<LayerListContainerProps, LayerL
}
}
// eslint-disable-next-line react-refresh/only-export-components
const LayerListContainerSortable = SortableContainer((props: LayerListContainerProps) => <LayerListContainer {...props} />)
type LayerListProps = LayerListContainerProps & {

View File

@@ -36,10 +36,10 @@ type IconActionProps = {
class IconAction extends React.Component<IconActionProps> {
renderIcon() {
switch(this.props.action) {
case 'duplicate': return <MdContentCopy />
case 'show': return <MdVisibility />
case 'hide': return <MdVisibilityOff />
case 'delete': return <MdDelete />
case 'duplicate': return <MdContentCopy />
case 'show': return <MdVisibility />
case 'hide': return <MdVisibilityOff />
case 'delete': return <MdDelete />
}
}
@@ -114,27 +114,27 @@ class LayerListItem extends React.Component<LayerListItemProps> {
"maputnik-layer-list-item-selected": this.props.isSelected,
[this.props.className!]: true,
})}>
<DraggableLabel {...this.props} />
<span style={{flexGrow: 1}} />
<IconAction
wdKey={"layer-list-item:"+this.props.layerId+":delete"}
action={'delete'}
classBlockName="delete"
onClick={_e => this.props.onLayerDestroy!(this.props.layerIndex)}
/>
<IconAction
wdKey={"layer-list-item:"+this.props.layerId+":copy"}
action={'duplicate'}
classBlockName="duplicate"
onClick={_e => this.props.onLayerCopy!(this.props.layerIndex)}
/>
<IconAction
wdKey={"layer-list-item:"+this.props.layerId+":toggle-visibility"}
action={visibilityAction}
classBlockName="visibility"
classBlockModifier={visibilityAction}
onClick={_e => this.props.onLayerVisibilityToggle!(this.props.layerIndex)}
/>
<DraggableLabel {...this.props} />
<span style={{flexGrow: 1}} />
<IconAction
wdKey={"layer-list-item:"+this.props.layerId+":delete"}
action={'delete'}
classBlockName="delete"
onClick={_e => this.props.onLayerDestroy!(this.props.layerIndex)}
/>
<IconAction
wdKey={"layer-list-item:"+this.props.layerId+":copy"}
action={'duplicate'}
classBlockName="duplicate"
onClick={_e => this.props.onLayerCopy!(this.props.layerIndex)}
/>
<IconAction
wdKey={"layer-list-item:"+this.props.layerId+":toggle-visibility"}
action={visibilityAction}
classBlockName="visibility"
classBlockModifier={visibilityAction}
onClick={_e => this.props.onLayerVisibilityToggle!(this.props.layerIndex)}
/>
</li>
}
}

View File

@@ -1,4 +1,4 @@
import React from 'react'
import React, {type JSX} from 'react'
import ReactDOM from 'react-dom'
import MapLibreGl, {LayerSpecification, LngLat, Map, MapOptions, SourceSpecification, StyleSpecification} from 'maplibre-gl'
// @ts-ignore

View File

@@ -34,7 +34,7 @@ function renderFeatureId(feature: InspectFeature) {
return <Block key={"feature-id"} label={"feature_id"}>
<FieldString value={displayValue(feature.id)} style={{backgroundColor: 'transparent'}} />
</Block>
}
}
function renderFeature(feature: InspectFeature, idx: number) {
return <div key={`${feature.sourceLayer}-${idx}`}>
@@ -48,7 +48,7 @@ function renderFeature(feature: InspectFeature, idx: number) {
}
function removeDuplicatedFeatures(features: InspectFeature[]) {
let uniqueFeatures: InspectFeature[] = [];
const uniqueFeatures: InspectFeature[] = [];
features.forEach(feature => {
const featureIndex = uniqueFeatures.findIndex(feature2 => {

View File

@@ -5,10 +5,10 @@ import type {InspectFeature} from './MapMaplibreGlFeaturePropertyPopup';
function groupFeaturesBySourceLayer(features: InspectFeature[]) {
const sources: {[key: string]: InspectFeature[]} = {}
let returnedFeatures: {[key: string]: number} = {}
const returnedFeatures: {[key: string]: number} = {}
features.forEach(feature => {
if(returnedFeatures.hasOwnProperty(feature.layer.id)) {
if(Object.prototype.hasOwnProperty.call(returnedFeatures, feature.layer.id)) {
returnedFeatures[feature.layer.id]++
const featureObject = sources[feature.layer['source-layer']].find((f: InspectFeature) => f.layer.id === feature.layer.id)
@@ -42,21 +42,21 @@ class FeatureLayerPopup extends React.Component<FeatureLayerPopupProps> {
const paintProps = feature.layer.paint;
let propName;
if(paintProps.hasOwnProperty("text-color") && paintProps["text-color"]) {
if(Object.prototype.hasOwnProperty.call(paintProps, "text-color") && paintProps["text-color"]) {
propName = "text-color";
}
else if (paintProps.hasOwnProperty("fill-color") && paintProps["fill-color"]) {
else if (Object.prototype.hasOwnProperty.call(paintProps, "fill-color") && paintProps["fill-color"]) {
propName = "fill-color";
}
else if (paintProps.hasOwnProperty("line-color") && paintProps["line-color"]) {
else if (Object.prototype.hasOwnProperty.call(paintProps, "line-color") && paintProps["line-color"]) {
propName = "line-color";
}
else if (paintProps.hasOwnProperty("fill-extrusion-color") && paintProps["fill-extrusion-color"]) {
else if (Object.prototype.hasOwnProperty.call(paintProps, "fill-extrusion-color") && paintProps["fill-extrusion-color"]) {
propName = "fill-extrusion-color";
}
if(propName) {
let color = feature.layer.paint[propName];
const color = feature.layer.paint[propName];
return String(color);
}
else {
@@ -80,8 +80,8 @@ class FeatureLayerPopup extends React.Component<FeatureLayerPopupProps> {
const featureColor = this._getFeatureColor(feature, this.props.zoom);
return <div
key={idx}
className="maputnik-popup-layer"
key={idx}
className="maputnik-popup-layer"
>
<div
className="maputnik-popup-layer__swatch"

View File

@@ -99,7 +99,7 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
});
map.on('pointermove', (evt) => {
var coords = toLonLat(evt.coordinate);
const coords = toLonLat(evt.coordinate);
this.setState({
cursor: [
coords[0].toFixed(2),

View File

@@ -68,7 +68,7 @@ export default class ModalAdd extends React.Component<ModalAddProps, ModalAddSta
const availableSourcesNew = this.getSources(newType);
if(
// Type has changed
// Type has changed
oldType !== newType
&& prevState.source !== ""
// Was a valid source previously
@@ -113,7 +113,7 @@ export default class ModalAdd extends React.Component<ModalAddProps, ModalAddSta
]
}
for(let [key, val] of Object.entries(this.props.sources) as any) {
for(const [key, val] of Object.entries(this.props.sources) as any) {
const valType = val.type as keyof typeof types;
if(types[valType] && types[valType].indexOf(type) > -1) {
sources.push(key);
@@ -136,41 +136,41 @@ export default class ModalAdd extends React.Component<ModalAddProps, ModalAddSta
className="maputnik-add-modal"
>
<div className="maputnik-add-layer">
<FieldId
value={this.state.id}
wdKey="add-layer.layer-id"
onChange={(v: string) => {
this.setState({ id: v })
}}
/>
<FieldType
value={this.state.type}
wdKey="add-layer.layer-type"
onChange={(v: LayerSpecification["type"]) => this.setState({ type: v })}
/>
{this.state.type !== 'background' &&
<FieldId
value={this.state.id}
wdKey="add-layer.layer-id"
onChange={(v: string) => {
this.setState({ id: v })
}}
/>
<FieldType
value={this.state.type}
wdKey="add-layer.layer-type"
onChange={(v: LayerSpecification["type"]) => this.setState({ type: v })}
/>
{this.state.type !== 'background' &&
<FieldSource
sourceIds={sources}
wdKey="add-layer.layer-source-block"
value={this.state.source}
onChange={(v: string) => this.setState({ source: v })}
/>
}
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.state.type) < 0 &&
}
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.state.type) < 0 &&
<FieldSourceLayer
isFixed={true}
sourceLayerIds={layers}
value={this.state['source-layer']}
onChange={(v: string) => this.setState({ 'source-layer': v })}
/>
}
<InputButton
className="maputnik-add-layer-button"
onClick={this.addLayer}
data-wd-key="add-layer"
>
}
<InputButton
className="maputnik-add-layer-button"
onClick={this.addLayer}
data-wd-key="add-layer"
>
Add Layer
</InputButton>
</InputButton>
</div>
</Modal>
}

View File

@@ -25,7 +25,7 @@ export default class ModalLoading extends React.Component<ModalLoadingProps> {
underlayClickExits={false}
underlayProps={{
// @ts-ignore
onClick: (e: Event) => underlayProps(e)
onClick: (e: Event) => underlayProps(e)
}}
title={this.props.title}
onOpenToggle={() => this.props.onCancel()}

View File

@@ -91,33 +91,33 @@ export default class ModalOpen extends React.Component<ModalOpenProps, ModalOpen
mode: 'cors',
credentials: "same-origin"
})
.then(function(response) {
return response.json();
})
.then((body) => {
if(canceled) {
return;
}
.then(function(response) {
return response.json();
})
.then((body) => {
if(canceled) {
return;
}
this.setState({
activeRequest: null,
activeRequestUrl: null
});
this.setState({
activeRequest: null,
activeRequestUrl: null
});
const mapStyle = style.ensureStyleValidity(body)
console.log('Loaded style ', mapStyle.id)
this.props.onStyleOpen(mapStyle)
this.onOpenToggle()
})
.catch((err) => {
this.setState({
error: `Failed to load: '${styleUrl}'`,
activeRequest: null,
activeRequestUrl: null
});
console.error(err);
console.warn('Could not open the style URL', styleUrl)
})
const mapStyle = style.ensureStyleValidity(body)
console.log('Loaded style ', mapStyle.id)
this.props.onStyleOpen(mapStyle)
this.onOpenToggle()
})
.catch((err) => {
this.setState({
error: `Failed to load: '${styleUrl}'`,
activeRequest: null,
activeRequestUrl: null
});
console.error(err);
console.warn('Could not open the style URL', styleUrl)
})
this.setState({
activeRequest: {
@@ -135,7 +135,7 @@ export default class ModalOpen extends React.Component<ModalOpenProps, ModalOpen
}
onUpload = (_: any, files: Result[]) => {
const [_e, file] = files[0];
const [, file] = files[0];
const reader = new FileReader();
this.clearError();
@@ -244,7 +244,7 @@ export default class ModalOpen extends React.Component<ModalOpenProps, ModalOpen
Open one of the publicly available styles to start from.
</p>
<div className="maputnik-style-gallery-container">
{styleOptions}
{styleOptions}
</div>
</section>
</Modal>

View File

@@ -24,17 +24,17 @@ type PublicSourceProps = {
class PublicSource extends React.Component<PublicSourceProps> {
render() {
return <div className="maputnik-public-source">
<InputButton
<InputButton
className="maputnik-public-source-select"
onClick={() => this.props.onSelect(this.props.id)}
>
<div className="maputnik-public-source-info">
<p className="maputnik-public-source-name">{this.props.title}</p>
<p className="maputnik-public-source-id">#{this.props.id}</p>
</div>
<span className="maputnik-space" />
<MdAddCircleOutline />
</InputButton>
onClick={() => this.props.onSelect(this.props.id)}
>
<div className="maputnik-public-source-info">
<p className="maputnik-public-source-name">{this.props.title}</p>
<p className="maputnik-public-source-id">#{this.props.id}</p>
</div>
<span className="maputnik-space" />
<MdAddCircleOutline />
</InputButton>
</div>
}
}
@@ -127,68 +127,68 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
const {protocol} = window.location;
switch(mode) {
case 'geojson_url': return {
type: 'geojson',
data: `${protocol}//localhost:3000/geojson.json`
}
case 'geojson_json': return {
type: 'geojson',
cluster: (source as GeoJSONSourceSpecification).cluster || false,
data: {}
}
case 'tilejson_vector': return {
type: 'vector',
url: (source as VectorSourceSpecification).url || `${protocol}//localhost:3000/tilejson.json`
}
case 'tilexyz_vector': return {
type: 'vector',
tiles: (source as VectorSourceSpecification).tiles || [`${protocol}//localhost:3000/{x}/{y}/{z}.pbf`],
minzoom: (source as VectorSourceSpecification).minzoom || 0,
maxzoom: (source as VectorSourceSpecification).maxzoom || 14
}
case 'tilejson_raster': return {
type: 'raster',
url: (source as RasterSourceSpecification).url || `${protocol}//localhost:3000/tilejson.json`
}
case 'tilexyz_raster': return {
type: 'raster',
tiles: (source as RasterSourceSpecification).tiles || [`${protocol}//localhost:3000/{x}/{y}/{z}.pbf`],
minzoom: (source as RasterSourceSpecification).minzoom || 0,
maxzoom: (source as RasterSourceSpecification).maxzoom || 14
}
case 'tilejson_raster-dem': return {
type: 'raster-dem',
url: (source as RasterDEMSourceSpecification).url || `${protocol}//localhost:3000/tilejson.json`
}
case 'tilexyz_raster-dem': return {
type: 'raster-dem',
tiles: (source as RasterDEMSourceSpecification).tiles || [`${protocol}//localhost:3000/{x}/{y}/{z}.pbf`],
minzoom: (source as RasterDEMSourceSpecification).minzoom || 0,
maxzoom: (source as RasterDEMSourceSpecification).maxzoom || 14
}
case 'image': return {
type: 'image',
url: `${protocol}//localhost:3000/image.png`,
coordinates: [
[0,0],
[0,0],
[0,0],
[0,0],
],
}
case 'video': return {
type: 'video',
urls: [
`${protocol}//localhost:3000/movie.mp4`
],
coordinates: [
[0,0],
[0,0],
[0,0],
[0,0],
],
}
default: return {} as any
case 'geojson_url': return {
type: 'geojson',
data: `${protocol}//localhost:3000/geojson.json`
}
case 'geojson_json': return {
type: 'geojson',
cluster: (source as GeoJSONSourceSpecification).cluster || false,
data: {}
}
case 'tilejson_vector': return {
type: 'vector',
url: (source as VectorSourceSpecification).url || `${protocol}//localhost:3000/tilejson.json`
}
case 'tilexyz_vector': return {
type: 'vector',
tiles: (source as VectorSourceSpecification).tiles || [`${protocol}//localhost:3000/{x}/{y}/{z}.pbf`],
minzoom: (source as VectorSourceSpecification).minzoom || 0,
maxzoom: (source as VectorSourceSpecification).maxzoom || 14
}
case 'tilejson_raster': return {
type: 'raster',
url: (source as RasterSourceSpecification).url || `${protocol}//localhost:3000/tilejson.json`
}
case 'tilexyz_raster': return {
type: 'raster',
tiles: (source as RasterSourceSpecification).tiles || [`${protocol}//localhost:3000/{x}/{y}/{z}.pbf`],
minzoom: (source as RasterSourceSpecification).minzoom || 0,
maxzoom: (source as RasterSourceSpecification).maxzoom || 14
}
case 'tilejson_raster-dem': return {
type: 'raster-dem',
url: (source as RasterDEMSourceSpecification).url || `${protocol}//localhost:3000/tilejson.json`
}
case 'tilexyz_raster-dem': return {
type: 'raster-dem',
tiles: (source as RasterDEMSourceSpecification).tiles || [`${protocol}//localhost:3000/{x}/{y}/{z}.pbf`],
minzoom: (source as RasterDEMSourceSpecification).minzoom || 0,
maxzoom: (source as RasterDEMSourceSpecification).maxzoom || 14
}
case 'image': return {
type: 'image',
url: `${protocol}//localhost:3000/image.png`,
coordinates: [
[0,0],
[0,0],
[0,0],
[0,0],
],
}
case 'video': return {
type: 'video',
urls: [
`${protocol}//localhost:3000/movie.mp4`
],
coordinates: [
[0,0],
[0,0],
[0,0],
[0,0],
],
}
default: return {} as any
}
}
@@ -245,7 +245,7 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
/>
<InputButton
className="maputnik-add-source-button"
onClick={this.onAdd}
onClick={this.onAdd}
>
Add Source
</InputButton>
@@ -308,16 +308,16 @@ export default class ModalSources extends React.Component<ModalSourcesProps> {
Add one of the publicly available sources to your style.
</p>
<div className="maputnik-public-sources" style={{maxWidth: 500}}>
{tilesetOptions}
{tilesetOptions}
</div>
</section>
<section className="maputnik-modal-section">
<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>
<AddSource
onAdd={(sourceId: string, source: SourceSpecification) => this.props.onStyleChanged(addSource(mapStyle, sourceId, source))}
/>
<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>
<AddSource
onAdd={(sourceId: string, source: SourceSpecification) => this.props.onStyleChanged(addSource(mapStyle, sourceId, source))}
/>
</section>
</Modal>
}

View File

@@ -269,28 +269,28 @@ export default class ModalSourcesTypeEditor extends React.Component<ModalSources
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
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

@@ -23,7 +23,7 @@ function setStopRefs(props: DataPropertyProps, state: DataPropertyState) {
if(props.value && props.value.stops) {
props.value.stops.forEach((_val, idx) => {
if(!state.refs.hasOwnProperty(idx)) {
if(!Object.prototype.hasOwnProperty.call(state.refs, idx)) {
if(!newRefs) {
newRefs = {...state};
}
@@ -118,7 +118,7 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
data: stop
}
})
// Sort by zoom
// Sort by zoom
.sort((a, b) => sortNumerically(a.data[0].zoom, b.data[0].zoom));
// Fetch the new position of the stops

View File

@@ -96,12 +96,12 @@ export default class ExpressionProperty extends React.Component<ExpressionProper
}
else {
Object.entries(errors!)
.filter(([key, _error]) => {
return key.startsWith(errorKeyStart);
})
.forEach(([_key, error]) => {
return foundErrors.push(error);
})
.filter(([key, _error]) => {
return key.startsWith(errorKeyStart);
})
.forEach(([_key, error]) => {
return foundErrors.push(error);
})
if (fieldError) {
foundErrors.push(fieldError);

View File

@@ -26,7 +26,7 @@ function setStopRefs(props: ZoomPropertyProps, state: ZoomPropertyState) {
if(props.value && (props.value as ZoomWithStops).stops) {
(props.value as ZoomWithStops).stops.forEach((_val, idx: number) => {
if(!state.refs.hasOwnProperty(idx)) {
if(Object.prototype.hasOwnProperty.call(!state.refs, idx)) {
if(!newRefs) {
newRefs = {...state};
}

View File

@@ -1,5 +1,4 @@
import { IconContext } from "react-icons";
import React from 'react';
import ReactDOM from 'react-dom';
import './favicon.ico'

View File

@@ -29,18 +29,18 @@ export class ApiStyleStore {
fetch(this.localUrl + '/styles', {
mode: 'cors',
})
.then((response) => {
return response.json();
})
.then((body) => {
const styleIds = body;
this.latestStyleId = styleIds[0]
this.notifyLocalChanges()
cb(null)
})
.catch(() => {
cb(new Error('Can not connect to style API'))
})
.then((response) => {
return response.json();
})
.then((body) => {
const styleIds = body;
this.latestStyleId = styleIds[0]
this.notifyLocalChanges()
cb(null)
})
.catch(() => {
cb(new Error('Can not connect to style API'))
})
}
notifyLocalChanges() {
@@ -64,12 +64,12 @@ export class ApiStyleStore {
fetch(this.localUrl + '/styles/' + this.latestStyleId, {
mode: 'cors',
})
.then(function(response) {
return response.json();
})
.then(function(body) {
cb(style.ensureStyleValidity(body))
})
.then(function(response) {
return response.json();
})
.then(function(body) {
cb(style.ensureStyleValidity(body))
})
} else {
throw new Error('No latest style available. You need to init the api backend first.')
}
@@ -92,9 +92,9 @@ export class ApiStyleStore {
},
body: styleJSON
})
.catch(function(error) {
if(error) console.error(error)
})
.catch(function(error) {
if(error) console.error(error)
})
return mapStyle
}
}

View File

@@ -33,7 +33,7 @@ function get(namespace: keyof DebugStore, key: string) {
if(!enabled()) {
throw genErr();
}
if(debugStore.hasOwnProperty(namespace)) {
if(Object.prototype.hasOwnProperty.call(debugStore, namespace)) {
return debugStore[namespace][key];
}
}

View File

@@ -70,3 +70,21 @@ export function changeProperty(layer: LayerSpecification, group: keyof LayerSpec
}
}
}
export function layerPrefix(name: string) {
return name.replace(' ', '-').replace('_', '-').split('-')[0]
}
export function findClosestCommonPrefix(layers: LayerSpecification[], idx: number) {
const currentLayerPrefix = layerPrefix(layers[idx].id)
let closestIdx = idx
for (let i = idx; i > 0; i--) {
const previousLayerPrefix = layerPrefix(layers[i-1].id)
if(previousLayerPrefix === currentLayerPrefix) {
closestIdx = i - 1
} else {
return closestIdx
}
}
return closestIdx
}

View File

@@ -5,16 +5,16 @@ function loadJSON(url: string, defaultValue: any, cb: (...args: any[]) => void)
mode: 'cors',
credentials: "same-origin"
})
.then(function(response) {
return response.json();
})
.then(function(body) {
cb(body)
})
.catch(function() {
console.warn('Can not metadata for ' + url)
cb(defaultValue)
})
.then(function(response) {
return response.json();
})
.then(function(body) {
cb(body)
})
.catch(function() {
console.warn('Can not metadata for ' + url)
cb(defaultValue)
})
}
export function downloadGlyphsMetadata(urlTemplate: string, cb: (...args: any[]) => void) {
@@ -22,14 +22,14 @@ export function downloadGlyphsMetadata(urlTemplate: string, cb: (...args: any[])
// Special handling because Tileserver GL serves the fontstacks metadata differently
// https://github.com/klokantech/tileserver-gl/pull/104#issuecomment-274444087
let urlObj = npmurl.parse(urlTemplate);
const urlObj = npmurl.parse(urlTemplate);
const normPathPart = '/%7Bfontstack%7D/%7Brange%7D.pbf';
if(urlObj.pathname === normPathPart) {
urlObj.pathname = '/fontstacks.json';
} else {
urlObj.pathname = urlObj.pathname!.replace(normPathPart, '.json');
}
let url = npmurl.format(urlObj);
const url = npmurl.format(urlObj);
loadJSON(url, [], cb)
}

View File

@@ -21,10 +21,10 @@ export function loadDefaultStyle(cb: (...args: any[]) => void) {
function loadStoredStyles() {
const styles = []
for (let i = 0; i < window.localStorage.length; i++) {
const key = window.localStorage.key(i)
if(isStyleKey(key!)) {
styles.push(fromKey(key!))
}
const key = window.localStorage.key(i)
if(isStyleKey(key!)) {
styles.push(fromKey(key!))
}
}
return styles
}
@@ -70,10 +70,10 @@ export class StyleStore {
// Delete entire style history
purge() {
for (let i = 0; i < window.localStorage.length; i++) {
const key = window.localStorage.key(i) as string;
if(key.startsWith(storagePrefix)) {
window.localStorage.removeItem(key)
}
const key = window.localStorage.key(i) as string;
if(key.startsWith(storagePrefix)) {
window.localStorage.removeItem(key)
}
}
}

View File

@@ -12,16 +12,16 @@ export function loadStyleUrl(styleUrl: string, cb: (...args: any[]) => void) {
mode: 'cors',
credentials: "same-origin"
})
.then(function(response) {
return response.json();
})
.then(function(body) {
cb(style.ensureStyleValidity(body))
})
.catch(function() {
console.warn('Could not fetch default style', styleUrl)
cb(style.emptyStyle)
})
.then(function(response) {
return response.json();
})
.then(function(body) {
cb(style.ensureStyleValidity(body))
})
.catch(function() {
console.warn('Could not fetch default style', styleUrl)
cb(style.emptyStyle)
})
}
export function removeStyleQuerystring() {

View File

@@ -1,3 +1,4 @@
@use 'sass:color';
// MAP
.maputnik-map__container {
background: white;
@@ -110,12 +111,12 @@
text-decoration: none;
&:hover {
background-color: lighten($color-midgray, 12);
background-color: color.adjust($color-midgray, $lightness: 12%);
color: $color-white;
}
&:disabled {
background-color: darken($color-midgray, 5);
background-color: color.adjust($color-midgray, $lightness: -5%);
color: $color-midgray;
cursor: not-allowed;
}

View File

@@ -8,7 +8,7 @@
}
.maputnik-filter-editor {
@extend .clearfix;
@extend .clearfix; /* stylelint-disable-line */
color: $color-lowgray;
}
@@ -62,7 +62,7 @@
}
.maputnik-delete-filter {
@extend .maputnik-icon-button;
@extend .maputnik-icon-button; /* stylelint-disable-line */
}
.maputnik-filter-editor-block-action {
@@ -79,7 +79,7 @@
}
.maputnik-radio-as-button {
@extend .maputnik-button;
@extend .maputnik-button; /* stylelint-disable-line */
border: solid 1px transparent;

View File

@@ -1,3 +1,4 @@
@use 'sass:color';
//INPUT
.maputnik-input {
height: 24px;
@@ -10,7 +11,7 @@
padding-right: $margin-2;
border: none;
background-color: $color-gray;
color: lighten($color-lowgray, 12);
color: color.adjust($color-lowgray, $lightness: 12%);
&:invalid {
border: solid 1px #B71C1C;
@@ -19,7 +20,7 @@
}
.maputnik-string {
@extend .maputnik-input;
@extend .maputnik-input; /* stylelint-disable-line */
&--multi {
resize: vertical;
@@ -43,12 +44,12 @@
}
.maputnik-number {
@extend .maputnik-input;
@extend .maputnik-input; /* stylelint-disable-line */
}
//COLOR PICKER
.maputnik-color {
@extend .maputnik-input;
@extend .maputnik-input; /* stylelint-disable-line */
height: 26px;
}
@@ -95,7 +96,7 @@
// SELECT
.maputnik-select {
@extend .maputnik-input;
@extend .maputnik-input; /* stylelint-disable-line */
-moz-appearance: none;
-webkit-appearance: none;
@@ -117,7 +118,7 @@
}
.maputnik-button-selected {
background-color: lighten($color-midgray, 12);
background-color: color.adjust($color-midgray, $lightness: 12%);
color: white;
}
@@ -133,7 +134,7 @@
outline: none;
&-wrapper {
@extend .maputnik-input;
@extend .maputnik-input; /* stylelint-disable-line */
padding-left: 0;
padding-right: 0;

View File

@@ -1,3 +1,4 @@
@use 'sass:color';
// LAYER LIST
.maputnik-layer-list {
height: 100%;
@@ -48,7 +49,7 @@
font-weight: 400;
color: $color-lowgray;
font-size: $font-size-6;
border-bottom-color: lighten($color-black, 0.1);
border-bottom-color: color.adjust($color-black, $lightness: 0.1%);
user-select: none;
list-style: none;
z-index: 2000;
@@ -87,7 +88,7 @@
height: 15px;
svg {
fill: darken($color-lowgray, 20);
fill: color.adjust($color-lowgray, $lightness: -20%);
&:hover {
fill: $color-white;
@@ -101,13 +102,13 @@
.maputnik-layer-list-item:hover,
.maputnik-layer-list-item-selected {
background-color: lighten($color-black, 2);
background-color: color.adjust($color-black, $lightness: 2%);
.maputnik-layer-list-icon-action {
display: block;
svg {
fill: darken($color-lowgray, 0.5);
fill: color.adjust($color-lowgray, $lightness: -0.5%);
}
}
}
@@ -151,7 +152,7 @@
border: solid 1px transparent;
font-size: $font-size-6;
color: $color-lowgray;
background-color: lighten($color-black, 2);
background-color: color.adjust($color-black, $lightness: 2%);
user-select: none;
padding: $margin-2;
@@ -185,7 +186,7 @@
.maputnik-layer-editor-group {
font-weight: bold;
font-size: $font-size-5;
background-color: lighten($color-black, 2);
background-color: color.adjust($color-black, $lightness: 2%);
color: $color-white;
cursor: pointer;
user-select: none;
@@ -232,7 +233,7 @@
// PROPERTY
.maputnik-default-property {
.maputnik-input-block-label {
color: darken($color-lowgray, 20%);
color: color.adjust($color-lowgray, $lightness: -20%);
}
.maputnik-string,
@@ -240,8 +241,8 @@
.maputnik-color,
.maputnik-select,
.maputnik-checkbox-wrapper {
background-color: darken($color-gray, 2%);
color: darken($color-lowgray, 20%);
background-color: color.adjust($color-gray, $lightness: -2%);
color: color.adjust($color-lowgray, $lightness: -20%);
}
.maputnik-make-zoom-function svg {
@@ -249,17 +250,17 @@
}
.maputnik-multibutton .maputnik-button {
background-color: darken($color-midgray, 10%);
color: darken($color-lowgray, 20%);
background-color: color.adjust($color-midgray, $lightness: -10%);
color: color.adjust($color-lowgray, $lightness: -20%);
&:hover {
background-color: lighten($color-midgray, 12);
background-color: color.adjust($color-midgray, $lightness: 12%);
color: $color-white;
}
}
.maputnik-multibutton .maputnik-button-selected {
background-color: darken($color-midgray, 2%);
background-color: color.adjust($color-midgray, $lightness: -2%);
color: $color-lowgray;
}
}

View File

@@ -72,12 +72,12 @@
}
.maputnik-modal-header-space {
@extend .maputnik-space;
@extend .maputnik-space; /* stylelint-disable-line */
}
//OPEN MODAL
.maputnik-upload-button {
@extend .maputnik-big-button;
@extend .maputnik-big-button; /* stylelint-disable-line */
}
.maputnik-style-gallery-container {
@@ -139,12 +139,12 @@
}
.maputnik-add-layer {
@extend .clearfix;
@extend .clearfix; /* stylelint-disable-line */
}
//ADD MODAL
.maputnik-add-layer-button {
@extend .maputnik-big-button;
@extend .maputnik-big-button; /* stylelint-disable-line */
margin-right: $margin-3;
float: right;
@@ -221,7 +221,7 @@
}
.maputnik-add-source {
@extend .clearfix;
@extend .clearfix; /* stylelint-disable-line */
.maputnik-input-block-label {
width: 30%;
@@ -233,7 +233,7 @@
}
.maputnik-add-source-button {
@extend .maputnik-big-button;
@extend .maputnik-big-button; /* stylelint-disable-line */
display: inline-block;
margin-top: 0;

View File

@@ -1,3 +1,4 @@
@use 'sass:color';
// TOOLBAR
.maputnik-toolbar {
position: fixed;
@@ -75,7 +76,7 @@
}
&:hover .maputnik-toolbar-link-wrapper {
background-color: lighten($color-midgray, 12);
background-color: color.adjust($color-midgray, $lightness: 12%);
color: $color-white;
}
}
@@ -93,13 +94,13 @@
.maputnik-toolbar-action {
background: inherit;
border-width: 0;
@extend .maputnik-toolbar-link;
@extend .maputnik-toolbar-link; /* stylelint-disable-line */
}
.maputnik-toolbar-select {
background: inherit;
border-width: 0;
@extend .maputnik-toolbar-link;
@extend .maputnik-toolbar-link; /* stylelint-disable-line */
select {
margin-left: 6px;

View File

@@ -5,12 +5,12 @@
vertical-align: middle;
padding: 0 $margin-2 0 0;
@extend .maputnik-icon-button;
@extend .maputnik-icon-button; /* stylelint-disable-line */
}
// ZOOM PROPERTY
.maputnik-zoom-spec-property {
@extend .clearfix;
@extend .clearfix; /* stylelint-disable-line */
}
.maputnik-zoom-spec-property-label {
@@ -49,7 +49,7 @@
padding-top: 0;
vertical-align: middle;
@extend .maputnik-icon-button;
@extend .maputnik-icon-button; /* stylelint-disable-line */
}
.maputnik-add-stop {
@@ -65,7 +65,7 @@
vertical-align: middle;
padding: 0 $margin-2 0 0;
@extend .maputnik-icon-button;
@extend .maputnik-icon-button; /* stylelint-disable-line */
}
.maputnik-data-spec-property {

View File

@@ -28,9 +28,6 @@
height: 14px;
}
.maputnik-data-spec-property {
}
.maputnik-data-fieldset-inner {
background: $color-black;
border: solid 1px $color-midgray;

View File

@@ -55,7 +55,9 @@ CodeMirror.registerHelper("lint", "mgl", (text: string, opts: any, doc: any) =>
try {
parser.parse(text);
}
catch (e) {}
catch (e) {
// ignore errors
}
if (found.length > 0) {
// JSON invalid so don't go any further
@@ -154,7 +156,7 @@ CodeMirror.registerHelper("lint", "mgl", (text: string, opts: any, doc: any) =>
found.push(err);
}
else if (key) {
const path = key.replace(/^\[|\]$/g, "").split(/\.|[\[\]]+/).filter(Boolean)
const path = key.replace(/^\[|\]$/g, "").split(/\.|[[\]]+/).filter(Boolean)
const parsedError = getArrayPositionalFromAst(ast, path);
if (!parsedError) {
console.warn("Something went wrong parsing error:", error);

View File

@@ -2,7 +2,7 @@
* If we don't have a default value just make one up
*/
export function findDefaultFromSpec(spec: { type: 'string' | 'color' | 'boolean' | 'array', default?: any }) {
if (spec.hasOwnProperty('default')) {
if (Object.prototype.hasOwnProperty.call(spec, 'default')) {
return spec.default;
}

View File

@@ -3,6 +3,7 @@
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"types": ["geojson"],
"module": "ESNext",
"skipLibCheck": true,