mirror of
https://github.com/a-nyx/maputnik-with-pmtiles.git
synced 2024-12-29 10:40:28 +01:00
Merge pull request #306 from orangemug/feature/accessibility-list-reorder
Keyboard accessible layer options
This commit is contained in:
commit
edd09ef585
8 changed files with 245 additions and 57 deletions
|
@ -7,14 +7,14 @@ templates:
|
|||
name: "Create artifacts directory"
|
||||
command: mkdir /tmp/artifacts
|
||||
- restore_cache:
|
||||
key: v1-dependencies-{{ checksum "package.json" }}
|
||||
key: v1-dependencies-{{ arch }}-{{ checksum "package.json" }}
|
||||
|
||||
- run: npm install
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- node_modules
|
||||
key: v1-dependencies-{{ checksum "package.json" }}
|
||||
key: v1-dependencies-{{ arch }}-{{ checksum "package.json" }}
|
||||
|
||||
- run: mkdir -p /tmp/artifacts/logs
|
||||
- run: npm run build
|
||||
|
@ -30,14 +30,14 @@ templates:
|
|||
name: "Create artifacts directory"
|
||||
command: mkdir /tmp/artifacts
|
||||
- restore_cache:
|
||||
key: v1-dependencies-{{ checksum "package.json" }}
|
||||
key: v1-dependencies-{{ arch }}-{{ checksum "package.json" }}
|
||||
|
||||
- run: npm install
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- node_modules
|
||||
key: v1-dependencies-{{ checksum "package.json" }}
|
||||
key: v1-dependencies-{{ arch }}-{{ checksum "package.json" }}
|
||||
|
||||
- run: mkdir -p /tmp/artifacts/logs
|
||||
- run: npm run build
|
||||
|
@ -71,7 +71,7 @@ jobs:
|
|||
dependencies:
|
||||
override:
|
||||
- brew install node@6
|
||||
working_directory: ~/repo-linux-node-v6
|
||||
working_directory: ~/repo-osx-node-v6
|
||||
steps: *build-steps
|
||||
build-osx-node-v8:
|
||||
macos:
|
||||
|
@ -79,7 +79,7 @@ jobs:
|
|||
dependencies:
|
||||
override:
|
||||
- brew install node@8
|
||||
working_directory: ~/repo-linux-node-v8
|
||||
working_directory: ~/repo-osx-node-v8
|
||||
steps: *build-steps
|
||||
build-osx-node-v10:
|
||||
macos:
|
||||
|
@ -87,7 +87,7 @@ jobs:
|
|||
dependencies:
|
||||
override:
|
||||
- brew install node@10
|
||||
working_directory: ~/repo-linux-node-v10
|
||||
working_directory: ~/repo-osx-node-v10
|
||||
steps: *build-steps
|
||||
|
||||
workflows:
|
||||
|
|
25
package-lock.json
generated
25
package-lock.json
generated
|
@ -5523,6 +5523,11 @@
|
|||
"readable-stream": "2.3.5"
|
||||
}
|
||||
},
|
||||
"focus-group": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/focus-group/-/focus-group-0.3.1.tgz",
|
||||
"integrity": "sha1-4PMu2GsNq91v/OvfiY7LMuR/7c4="
|
||||
},
|
||||
"focus-trap": {
|
||||
"version": "2.4.4",
|
||||
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-2.4.4.tgz",
|
||||
|
@ -8365,6 +8370,11 @@
|
|||
"resolved": "https://registry.npmjs.org/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz",
|
||||
"integrity": "sha1-+CbJtOKoUR2E46yinbBeGk87cqk="
|
||||
},
|
||||
"lodash.clamp": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.clamp/-/lodash.clamp-4.0.3.tgz",
|
||||
"integrity": "sha1-XCS+3u7vB1NWDcK0y0Zx+Qpt36o="
|
||||
},
|
||||
"lodash.clonedeep": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
|
@ -11479,6 +11489,16 @@
|
|||
"object-assign": "4.1.1"
|
||||
}
|
||||
},
|
||||
"react-aria-menubutton": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-aria-menubutton/-/react-aria-menubutton-5.1.1.tgz",
|
||||
"integrity": "sha512-ceBjPvuqwM2jnRFsfMlpfPdyqQ5cz4STNH7NlKpxStr2uETB/zQ2sHiIUMTuqSuOszU1kgUB2vm3aVn3xdjhcA==",
|
||||
"requires": {
|
||||
"focus-group": "0.3.1",
|
||||
"prop-types": "15.6.1",
|
||||
"teeny-tap": "0.2.0"
|
||||
}
|
||||
},
|
||||
"react-aria-modal": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/react-aria-modal/-/react-aria-modal-2.12.1.tgz",
|
||||
|
@ -14298,6 +14318,11 @@
|
|||
"xtend": "4.0.1"
|
||||
}
|
||||
},
|
||||
"teeny-tap": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/teeny-tap/-/teeny-tap-0.2.0.tgz",
|
||||
"integrity": "sha1-Fn5kUYLQasIi1iuyq2eUenClimg="
|
||||
},
|
||||
"text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
"github-api": "^3.0.0",
|
||||
"jsonlint": "github:josdejong/jsonlint#85a19d7",
|
||||
"lodash.capitalize": "^4.2.1",
|
||||
"lodash.clamp": "^4.0.3",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
|
@ -42,6 +43,7 @@
|
|||
"prop-types": "^15.6.0",
|
||||
"react": "^16.3.2",
|
||||
"react-addons-pure-render-mixin": "^15.6.2",
|
||||
"react-aria-menubutton": "^5.1.1",
|
||||
"react-aria-modal": "^2.12.1",
|
||||
"react-autocomplete": "^1.7.2",
|
||||
"react-codemirror2": "^4.2.1",
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import React from 'react'
|
||||
import Mousetrap from 'mousetrap'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
import clamp from 'lodash.clamp'
|
||||
import {arrayMove} from 'react-sortable-hoc';
|
||||
import url from 'url'
|
||||
|
||||
import MapboxGlMap from './map/MapboxGlMap'
|
||||
|
@ -168,6 +171,24 @@ export default class App extends React.Component {
|
|||
})
|
||||
}
|
||||
|
||||
onMoveLayer(move) {
|
||||
let { oldIndex, newIndex } = move;
|
||||
let layers = this.state.mapStyle.layers;
|
||||
oldIndex = clamp(oldIndex, 0, layers.length-1);
|
||||
newIndex = clamp(newIndex, 0, layers.length-1);
|
||||
if(oldIndex === newIndex) return;
|
||||
|
||||
if (oldIndex === this.state.selectedLayerIndex) {
|
||||
this.setState({
|
||||
selectedLayerIndex: newIndex
|
||||
});
|
||||
}
|
||||
|
||||
layers = layers.slice(0);
|
||||
layers = arrayMove(layers, oldIndex, newIndex);
|
||||
this.onLayersChange(layers);
|
||||
}
|
||||
|
||||
onLayersChange(changedLayers) {
|
||||
const changedStyle = {
|
||||
...this.state.mapStyle,
|
||||
|
@ -176,6 +197,40 @@ export default class App extends React.Component {
|
|||
this.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
onLayerDestroy(layerId) {
|
||||
let layers = this.state.mapStyle.layers;
|
||||
const remainingLayers = layers.slice(0);
|
||||
const idx = style.indexOfLayer(remainingLayers, layerId)
|
||||
remainingLayers.splice(idx, 1);
|
||||
this.onLayersChange(remainingLayers);
|
||||
}
|
||||
|
||||
onLayerCopy(layerId) {
|
||||
let layers = this.state.mapStyle.layers;
|
||||
const changedLayers = layers.slice(0)
|
||||
const idx = style.indexOfLayer(changedLayers, layerId)
|
||||
|
||||
const clonedLayer = cloneDeep(changedLayers[idx])
|
||||
clonedLayer.id = clonedLayer.id + "-copy"
|
||||
changedLayers.splice(idx, 0, clonedLayer)
|
||||
this.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
onLayerVisibilityToggle(layerId) {
|
||||
let layers = this.state.mapStyle.layers;
|
||||
const changedLayers = layers.slice(0)
|
||||
const idx = style.indexOfLayer(changedLayers, layerId)
|
||||
|
||||
const layer = { ...changedLayers[idx] }
|
||||
const changedLayout = 'layout' in layer ? {...layer.layout} : {}
|
||||
changedLayout.visibility = changedLayout.visibility === 'none' ? 'visible' : 'none'
|
||||
|
||||
layer.layout = changedLayout
|
||||
changedLayers[idx] = layer
|
||||
this.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
|
||||
onLayerIdChange(oldId, newId) {
|
||||
const changedLayers = this.state.mapStyle.layers.slice(0)
|
||||
const idx = style.indexOfLayer(changedLayers, oldId)
|
||||
|
@ -311,6 +366,10 @@ export default class App extends React.Component {
|
|||
/>
|
||||
|
||||
const layerList = <LayerList
|
||||
onMoveLayer={this.onMoveLayer.bind(this)}
|
||||
onLayerDestroy={this.onLayerDestroy.bind(this)}
|
||||
onLayerCopy={this.onLayerCopy.bind(this)}
|
||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle.bind(this)}
|
||||
onLayersChange={this.onLayersChange.bind(this)}
|
||||
onLayerSelect={this.onLayerSelect.bind(this)}
|
||||
selectedLayerIndex={this.state.selectedLayerIndex}
|
||||
|
@ -320,10 +379,17 @@ export default class App extends React.Component {
|
|||
|
||||
const layerEditor = selectedLayer ? <LayerEditor
|
||||
layer={selectedLayer}
|
||||
layerIndex={this.state.selectedLayerIndex}
|
||||
isFirstLayer={this.state.selectedLayerIndex < 1}
|
||||
isLastLayer={this.state.selectedLayerIndex === this.state.mapStyle.layers.length-1}
|
||||
sources={this.state.sources}
|
||||
vectorLayers={this.state.vectorLayers}
|
||||
spec={this.state.spec}
|
||||
onMoveLayer={this.onMoveLayer.bind(this)}
|
||||
onLayerChanged={this.onLayerChanged.bind(this)}
|
||||
onLayerDestroy={this.onLayerDestroy.bind(this)}
|
||||
onLayerCopy={this.onLayerCopy.bind(this)}
|
||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle.bind(this)}
|
||||
onLayerIdChange={this.onLayerIdChange.bind(this)}
|
||||
/> : null
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Wrapper, Button, Menu, MenuItem } from 'react-aria-menubutton'
|
||||
|
||||
import JSONEditor from './JSONEditor'
|
||||
import FilterEditor from '../filter/FilterEditor'
|
||||
|
@ -13,6 +14,8 @@ import CommentBlock from './CommentBlock'
|
|||
import LayerSourceBlock from './LayerSourceBlock'
|
||||
import LayerSourceLayerBlock from './LayerSourceLayerBlock'
|
||||
|
||||
import MoreVertIcon from 'react-icons/lib/md/more-vert'
|
||||
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import MultiButtonInput from '../inputs/MultiButtonInput'
|
||||
|
||||
|
@ -45,6 +48,13 @@ export default class LayerEditor extends React.Component {
|
|||
spec: PropTypes.object.isRequired,
|
||||
onLayerChanged: PropTypes.func,
|
||||
onLayerIdChange: PropTypes.func,
|
||||
onMoveLayer: PropTypes.func,
|
||||
onLayerDestroy: PropTypes.func,
|
||||
onLayerCopy: PropTypes.func,
|
||||
onLayerVisibilityToggle: PropTypes.func,
|
||||
isFirstLayer: PropTypes.bool,
|
||||
isLastLayer: PropTypes.bool,
|
||||
layerIndex: PropTypes.number,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -176,6 +186,13 @@ export default class LayerEditor extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
moveLayer(offset) {
|
||||
this.props.onMoveLayer({
|
||||
oldIndex: this.props.layerIndex,
|
||||
newIndex: this.props.layerIndex+offset
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const layerType = this.props.layer.type
|
||||
const groups = layoutGroups(layerType).filter(group => {
|
||||
|
@ -192,8 +209,73 @@ export default class LayerEditor extends React.Component {
|
|||
</LayerEditorGroup>
|
||||
})
|
||||
|
||||
const layout = this.props.layer.layout || {}
|
||||
|
||||
const items = {
|
||||
delete: {
|
||||
text: "Delete",
|
||||
handler: () => this.props.onLayerDestroy(this.props.layer.id)
|
||||
},
|
||||
duplicate: {
|
||||
text: "Duplicate",
|
||||
handler: () => this.props.onLayerCopy(this.props.layer.id)
|
||||
},
|
||||
hide: {
|
||||
text: (layout.visibility === "none") ? "Show" : "Hide",
|
||||
handler: () => this.props.onLayerVisibilityToggle(this.props.layer.id)
|
||||
},
|
||||
moveLayerUp: {
|
||||
text: "Move layer up",
|
||||
// Not actually used...
|
||||
disabled: this.props.isFirstLayer,
|
||||
handler: () => this.moveLayer(-1)
|
||||
},
|
||||
moveLayerDown: {
|
||||
text: "Move layer down",
|
||||
// Not actually used...
|
||||
disabled: this.props.isLastLayer,
|
||||
handler: () => this.moveLayer(+1)
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelection(id, event) {
|
||||
event.stopPropagation;
|
||||
items[id].handler();
|
||||
}
|
||||
|
||||
return <div className="maputnik-layer-editor"
|
||||
>
|
||||
<header>
|
||||
<div className="layer-header">
|
||||
<h2 className="layer-header__title">
|
||||
Layer: {this.props.layer.id}
|
||||
</h2>
|
||||
<div className="layer-header__info">
|
||||
<Wrapper
|
||||
className='more-menu'
|
||||
onSelection={handleSelection}
|
||||
closeOnSelection={false}
|
||||
>
|
||||
<Button className='more-menu__button'>
|
||||
<MoreVertIcon className="more-menu__button__svg" />
|
||||
</Button>
|
||||
<Menu>
|
||||
<ul className="more-menu__menu">
|
||||
{Object.keys(items).map((id, idx) => {
|
||||
const item = items[id];
|
||||
return <li key={id}>
|
||||
<MenuItem value={id} className='more-menu__menu__item'>
|
||||
{item.text}
|
||||
</MenuItem>
|
||||
</li>
|
||||
})}
|
||||
</ul>
|
||||
</Menu>
|
||||
</Wrapper>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</header>
|
||||
{groups}
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
|
||||
import Button from '../Button'
|
||||
import LayerListGroup from './LayerListGroup'
|
||||
|
@ -10,7 +9,7 @@ import AddIcon from 'react-icons/lib/md/add-circle-outline'
|
|||
import AddModal from '../modals/AddModal'
|
||||
|
||||
import style from '../../libs/style.js'
|
||||
import {SortableContainer, SortableHandle, arrayMove} from 'react-sortable-hoc';
|
||||
import {SortableContainer, SortableHandle} from 'react-sortable-hoc';
|
||||
|
||||
const layerListPropTypes = {
|
||||
layers: PropTypes.array.isRequired,
|
||||
|
@ -57,36 +56,6 @@ class LayerListContainer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
onLayerDestroy(layerId) {
|
||||
const remainingLayers = this.props.layers.slice(0)
|
||||
const idx = style.indexOfLayer(remainingLayers, layerId)
|
||||
remainingLayers.splice(idx, 1);
|
||||
this.props.onLayersChange(remainingLayers)
|
||||
}
|
||||
|
||||
onLayerCopy(layerId) {
|
||||
const changedLayers = this.props.layers.slice(0)
|
||||
const idx = style.indexOfLayer(changedLayers, layerId)
|
||||
|
||||
const clonedLayer = cloneDeep(changedLayers[idx])
|
||||
clonedLayer.id = clonedLayer.id + "-copy"
|
||||
changedLayers.splice(idx, 0, clonedLayer)
|
||||
this.props.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
onLayerVisibilityToggle(layerId) {
|
||||
const changedLayers = this.props.layers.slice(0)
|
||||
const idx = style.indexOfLayer(changedLayers, layerId)
|
||||
|
||||
const layer = { ...changedLayers[idx] }
|
||||
const changedLayout = 'layout' in layer ? {...layer.layout} : {}
|
||||
changedLayout.visibility = changedLayout.visibility === 'none' ? 'visible' : 'none'
|
||||
|
||||
layer.layout = changedLayout
|
||||
changedLayers[idx] = layer
|
||||
this.props.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
toggleModal(modalName) {
|
||||
this.setState({
|
||||
isOpen: {
|
||||
|
@ -186,9 +155,9 @@ class LayerListContainer extends React.Component {
|
|||
visibility={(layer.layout || {}).visibility}
|
||||
isSelected={idx === this.props.selectedLayerIndex}
|
||||
onLayerSelect={this.props.onLayerSelect}
|
||||
onLayerDestroy={this.onLayerDestroy.bind(this)}
|
||||
onLayerCopy={this.onLayerCopy.bind(this)}
|
||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle.bind(this)}
|
||||
onLayerDestroy={this.props.onLayerDestroy.bind(this)}
|
||||
onLayerCopy={this.props.onLayerCopy.bind(this)}
|
||||
onLayerVisibilityToggle={this.props.onLayerVisibilityToggle.bind(this)}
|
||||
/>
|
||||
listItems.push(listItem)
|
||||
idx += 1
|
||||
|
@ -237,18 +206,10 @@ class LayerListContainer extends React.Component {
|
|||
export default class LayerList extends React.Component {
|
||||
static propTypes = {...layerListPropTypes}
|
||||
|
||||
onSortEnd(move) {
|
||||
const { oldIndex, newIndex } = move
|
||||
if(oldIndex === newIndex) return
|
||||
let layers = this.props.layers.slice(0)
|
||||
layers = arrayMove(layers, oldIndex, newIndex)
|
||||
this.props.onLayersChange(layers)
|
||||
}
|
||||
|
||||
render() {
|
||||
return <LayerListContainer
|
||||
{...this.props}
|
||||
onSortEnd={this.onSortEnd.bind(this)}
|
||||
onSortEnd={this.props.onMoveLayer.bind(this)}
|
||||
useDragHandle={true}
|
||||
/>
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ class IconAction extends React.Component {
|
|||
|
||||
renderIcon() {
|
||||
switch(this.props.action) {
|
||||
case 'copy': return <CopyIcon />
|
||||
case 'duplicate': return <CopyIcon />
|
||||
case 'show': return <VisibilityIcon />
|
||||
case 'hide': return <VisibilityOffIcon />
|
||||
case 'delete': return <DeleteIcon />
|
||||
|
@ -46,13 +46,15 @@ class IconAction extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
return <a
|
||||
return <button
|
||||
tabIndex="-1"
|
||||
title={this.props.action}
|
||||
className="maputnik-layer-list-icon-action"
|
||||
data-wd-key={this.props.wdKey}
|
||||
onClick={this.props.onClick}
|
||||
>
|
||||
{this.renderIcon()}
|
||||
</a>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -109,7 +111,7 @@ class LayerListItem extends React.Component {
|
|||
/>
|
||||
<IconAction
|
||||
wdKey={"layer-list-item:"+this.props.layerId+":copy"}
|
||||
action={'copy'}
|
||||
action={'duplicate'}
|
||||
onClick={e => this.props.onLayerCopy(this.props.layerId)}
|
||||
/>
|
||||
<IconAction
|
||||
|
|
|
@ -50,14 +50,25 @@
|
|||
@include flex-row;
|
||||
}
|
||||
|
||||
&-icon-action svg {
|
||||
&-icon-action {
|
||||
display: none;
|
||||
|
||||
svg {
|
||||
fill: $color-black;
|
||||
}
|
||||
}
|
||||
|
||||
.maputnik-layer-list-item:hover,
|
||||
.maputnik-layer-list-item-selected {
|
||||
background-color: lighten($color-black, 2);
|
||||
|
||||
.maputnik-layer-list-icon-action {
|
||||
display: block;
|
||||
background: initial;
|
||||
border: none;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.maputnik-layer-list-icon-action svg {
|
||||
fill: darken($color-lowgray, 0.5);
|
||||
|
||||
|
@ -126,6 +137,7 @@
|
|||
user-select: none;
|
||||
padding: $margin-2;
|
||||
line-height: 20px;
|
||||
border-top: solid 1px #36383e;
|
||||
|
||||
@include flex-row;
|
||||
|
||||
|
@ -168,3 +180,41 @@
|
|||
color: $color-lowgray;
|
||||
}
|
||||
}
|
||||
|
||||
.more-menu {
|
||||
position: relative;
|
||||
|
||||
&__menu {
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
background: $color-black;
|
||||
border: solid 1px $color-midgray;
|
||||
right: 0;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
&__button__svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
&__menu__item {
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.layer-header {
|
||||
display: flex;
|
||||
padding: 6px;
|
||||
background: $color-black;
|
||||
|
||||
&__title {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
&__info {
|
||||
min-width: 28px;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue