Implemented top/bottom positioning of ActionMenu

This commit is contained in:
jos 2017-12-30 15:47:07 +01:00
parent ccf89162b7
commit d73b79cb4e
5 changed files with 160 additions and 106 deletions

View File

@ -6,22 +6,46 @@ import FloatingMenu from './menu/FloatingMenu'
import { escapeHTML, unescapeHTML } from '../utils/stringUtils'
import { getInnerText, insideRect } from '../utils/domUtils'
import { stringConvert, valueType, isUrl } from '../utils/typeUtils'
import { compileJSONPointer, META, SELECTED, SELECTED_END, SELECTED_AFTER, SELECTED_BEFORE } from '../eson'
import {
compileJSONPointer,
META,
SELECTED, SELECTED_START, SELECTED_END, SELECTED_AFTER, SELECTED_BEFORE, SELECTED_FIRST, SELECTED_LAST
} from '../eson'
// TODO: rename SELECTED, SELECTED_END, etc to AREA_*? It's used for both selection and hovering
const SELECTED_CLASS_NAMES = {
[SELECTED]: ' jsoneditor-selected',
[SELECTED_END]: ' jsoneditor-selected jsoneditor-selected-end',
[SELECTED_AFTER]: ' jsoneditor-selected jsoneditor-selected-insert-area',
[SELECTED_BEFORE]: ' jsoneditor-selected jsoneditor-selected-insert-area',
}
const MENU_ITEMS_OBJECT = [
{type: 'sort'},
{type: 'duplicate'},
{type: 'cut'},
{type: 'copy'},
{type: 'paste'},
{type: 'remove'}
]
const HOVERED_CLASS_NAMES = {
[SELECTED]: ' jsoneditor-hover',
[SELECTED_END]: ' jsoneditor-hover jsoneditor-hover-end',
[SELECTED_AFTER]: ' jsoneditor-hover jsoneditor-hover-insert-area',
[SELECTED_BEFORE]: ' jsoneditor-hover jsoneditor-hover-insert-area',
}
const MENU_ITEMS_ARRAY = [
{type: 'sort'},
{type: 'duplicate'},
{type: 'cut'},
{type: 'copy'},
{type: 'paste'},
{type: 'remove'}
]
const MENU_ITEMS_VALUE = [
// {text: 'String', onClick: this.props.emit('changeType', {type: 'checkbox', checked: false}}),
{type: 'duplicate'},
{type: 'cut'},
{type: 'copy'},
{type: 'paste'},
{type: 'remove'}
]
const MENU_ITEMS_INSERT_BEFORE = [
{type: 'insertStructure'},
{type: 'insertValue'},
{type: 'insertObject'},
{type: 'insertArray'},
{type: 'paste'},
]
export default class JSONNode extends PureComponent {
static URL_TITLE = 'Ctrl+Click or Ctrl+Enter to open url'
@ -46,7 +70,7 @@ export default class JSONNode extends PureComponent {
state = {
menu: null, // can contain object {anchor, root}
appendMenu: null, // can contain object {anchor, root}
hover: false
hover: null
}
componentWillUnmount () {
@ -104,17 +128,7 @@ export default class JSONNode extends PureComponent {
}
}
const floatingMenu = (meta.selected === SELECTED_END)
? this.renderFloatingMenu([
{type: 'sort'},
{type: 'duplicate'},
{type: 'cut'},
{type: 'copy'},
{type: 'paste'},
{type: 'remove'}
])
: null
const floatingMenu = this.renderFloatingMenu(MENU_ITEMS_OBJECT, meta.selected)
const insertArea = this.renderInsertBeforeArea()
return h('div', {
@ -159,17 +173,7 @@ export default class JSONNode extends PureComponent {
}
}
const floatingMenu = (meta.selected === SELECTED_END)
? this.renderFloatingMenu([
{type: 'sort'},
{type: 'duplicate'},
{type: 'cut'},
{type: 'copy'},
{type: 'paste'},
{type: 'remove'}
])
: null
const floatingMenu = this.renderFloatingMenu(MENU_ITEMS_ARRAY, meta.selected)
const insertArea = this.renderInsertBeforeArea()
return h('div', {
@ -194,16 +198,7 @@ export default class JSONNode extends PureComponent {
this.renderError(meta.error)
])
const floatingMenu = (meta.selected === SELECTED_END)
? this.renderFloatingMenu([
// {text: 'String', onClick: this.props.emit('changeType', {type: 'checkbox', checked: false}}),
{type: 'duplicate'},
{type: 'cut'},
{type: 'copy'},
{type: 'paste'},
{type: 'remove'}
])
: null
const floatingMenu = this.renderFloatingMenu(MENU_ITEMS_VALUE, meta.selected)
const insertArea = this.renderInsertBeforeArea()
@ -216,14 +211,9 @@ export default class JSONNode extends PureComponent {
}
renderInsertBeforeArea () {
const floatingMenu = (this.props.value[META].selected === SELECTED_BEFORE)
? this.renderFloatingMenu([
{type: 'insertStructure'},
{type: 'insertValue'},
{type: 'insertObject'},
{type: 'insertArray'},
{type: 'paste'},
])
const floatingMenu = ((this.props.value[META].selected & SELECTED_BEFORE) !== 0)
? this.renderFloatingMenu(MENU_ITEMS_INSERT_BEFORE,
SELECTED + SELECTED_END + SELECTED_FIRST)
: null
return h('div', {
@ -356,9 +346,23 @@ export default class JSONNode extends PureComponent {
}
getContainerClassName (selected, hover) {
return 'jsoneditor-node-container' +
(hover ? (HOVERED_CLASS_NAMES[hover]) : '') +
(selected ? (SELECTED_CLASS_NAMES[selected]) : '')
let classNames = ['jsoneditor-node-container']
if ((selected & SELECTED) !== 0) { classNames.push('jsoneditor-selected') }
if ((selected & SELECTED_START) !== 0) { classNames.push('jsoneditor-selected-start') }
if ((selected & SELECTED_END) !== 0) { classNames.push('jsoneditor-selected-end') }
if ((selected & SELECTED_FIRST) !== 0) { classNames.push('jsoneditor-selected-first') }
if ((selected & SELECTED_LAST) !== 0) { classNames.push('jsoneditor-selected-last') }
if ((selected & SELECTED_BEFORE) !== 0) { classNames.push('jsoneditor-selected-insert-area-before') }
if ((selected & SELECTED_AFTER) !== 0) { classNames.push('jsoneditor-selected-insert-area-after') }
if ((hover & SELECTED) !== 0) { classNames.push('jsoneditor-hover') }
if ((hover & SELECTED_START) !== 0) { classNames.push('jsoneditor-hover-start') }
if ((hover & SELECTED_END) !== 0) { classNames.push('jsoneditor-hover-end') }
if ((hover & SELECTED_BEFORE) !== 0) { classNames.push('jsoneditor-hover-insert-area-before') }
if ((hover & SELECTED_AFTER) !== 0) { classNames.push('jsoneditor-hover-insert-area-after') }
return classNames.join(' ')
}
/**
@ -473,12 +477,20 @@ export default class JSONNode extends PureComponent {
)
}
renderFloatingMenu (items) {
renderFloatingMenu (items, selected) {
if ((selected & SELECTED_END) === 0) {
return null
}
const isLastOfMultiple = ((selected & SELECTED_LAST) !== 0) &&
((selected & SELECTED_FIRST) === 0)
return h(FloatingMenu, {
key: 'floating-menu',
path: this.props.value[META].path,
emit: this.props.emit,
items
items,
position: isLastOfMultiple ? 'bottom' : 'top'
})
}
@ -487,7 +499,7 @@ export default class JSONNode extends PureComponent {
event.stopPropagation()
const hover = (event.target.className.indexOf('jsoneditor-insert-area') !== -1)
? SELECTED_AFTER
? (SELECTED + SELECTED_AFTER)
: SELECTED
if (hoveredNode && hoveredNode !== this) {
@ -505,7 +517,7 @@ export default class JSONNode extends PureComponent {
handleMouseLeave = (event) => {
event.stopPropagation()
// FIXME: this gives issues when the hovered node doesn't exist anymore. check whether mounted?
hoveredNode.setState({hover: false})
hoveredNode.setState({hover: null})
this.setState({hover: null})
}

View File

@ -529,35 +529,35 @@ div.jsoneditor-node-container {
background-color: #ffed99; }
div.jsoneditor-node-container.jsoneditor-selected.jsoneditor-hover {
background-color: #ffdb80; }
div.jsoneditor-node-container.jsoneditor-selected.jsoneditor-hover.jsoneditor-hover-insert-area {
div.jsoneditor-node-container.jsoneditor-selected.jsoneditor-hover.jsoneditor-hover-insert-area-after {
background-color: #ffed99; }
div.jsoneditor-node-container.jsoneditor-selected.jsoneditor-hover.jsoneditor-hover-insert-area > div.jsoneditor-insert-area {
div.jsoneditor-node-container.jsoneditor-selected.jsoneditor-hover.jsoneditor-hover-insert-area-after > div.jsoneditor-insert-area {
border: 1px dashed gray;
background-color: #f2f2f2; }
div.jsoneditor-node-container.jsoneditor-selected.jsoneditor-hover.jsoneditor-hover-insert-area.jsoneditor-selected-insert-area > div.jsoneditor-insert-area {
div.jsoneditor-node-container.jsoneditor-selected.jsoneditor-hover.jsoneditor-hover-insert-area-after.jsoneditor-selected-insert-area-before > div.jsoneditor-insert-area {
border: 1px dashed #f4af41;
background-color: #ffdb80; }
div.jsoneditor-node-container.jsoneditor-selected.jsoneditor-selected-insert-area {
div.jsoneditor-node-container.jsoneditor-selected.jsoneditor-selected-insert-area-before {
background-color: inherit; }
div.jsoneditor-node-container.jsoneditor-selected.jsoneditor-selected-insert-area.jsoneditor-hover {
div.jsoneditor-node-container.jsoneditor-selected.jsoneditor-selected-insert-area-before.jsoneditor-hover {
background-color: #f2f2f2; }
div.jsoneditor-node-container.jsoneditor-selected.jsoneditor-selected-insert-area.jsoneditor-hover.jsoneditor-hover-insert-area {
div.jsoneditor-node-container.jsoneditor-selected.jsoneditor-selected-insert-area-before.jsoneditor-hover.jsoneditor-hover-insert-area-after {
background-color: inherit; }
div.jsoneditor-node-container.jsoneditor-selected.jsoneditor-selected-insert-area > div.jsoneditor-insert-area {
div.jsoneditor-node-container.jsoneditor-selected.jsoneditor-selected-insert-area-before > div.jsoneditor-insert-area {
border: 1px dashed #f4af41;
background: #ffed99; }
div.jsoneditor-node-container.jsoneditor-selected div.jsoneditor-hover {
background-color: #ffdb80; }
div.jsoneditor-node-container.jsoneditor-selected div.jsoneditor-hover.jsoneditor-hover-insert-area {
div.jsoneditor-node-container.jsoneditor-selected div.jsoneditor-hover.jsoneditor-hover-insert-area-after {
background-color: inherit; }
div.jsoneditor-node-container.jsoneditor-selected div.jsoneditor-hover.jsoneditor-hover-insert-area > div.jsoneditor-insert-area {
div.jsoneditor-node-container.jsoneditor-selected div.jsoneditor-hover.jsoneditor-hover-insert-area-after > div.jsoneditor-insert-area {
border: 1px dashed #f4af41;
background: #ffdb80; }
div.jsoneditor-node-container.jsoneditor-hover {
background-color: #f2f2f2; }
div.jsoneditor-node-container.jsoneditor-hover.jsoneditor-hover-insert-area {
div.jsoneditor-node-container.jsoneditor-hover.jsoneditor-hover-insert-area-after {
background-color: inherit; }
div.jsoneditor-node-container.jsoneditor-hover.jsoneditor-hover-insert-area > div.jsoneditor-insert-area {
div.jsoneditor-node-container.jsoneditor-hover.jsoneditor-hover-insert-area-after > div.jsoneditor-insert-area {
border: 1px dashed gray;
background-color: #f2f2f2; }
div.jsoneditor-node-container div.jsoneditor-insert-area {
@ -587,8 +587,17 @@ div.jsoneditor-node-container {
width: 0;
height: 0;
border-top: solid 10px #4d4d4d;
border-bottom: none;
border-left: solid 10px transparent;
border-right: solid 10px transparent; }
div.jsoneditor-node-container div.jsoneditor-floating-menu.jsoneditor-floating-menu-bottom {
bottom: auto;
top: 100%; }
div.jsoneditor-node-container div.jsoneditor-floating-menu.jsoneditor-floating-menu-bottom:after {
top: -10px;
margin-left: -10px;
border-top: none;
border-bottom: solid 10px #4d4d4d; }
div.jsoneditor-node-container div.jsoneditor-floating-menu button.jsoneditor-floating-menu-item {
color: #fff;
background: #4d4d4d;

View File

@ -575,7 +575,7 @@ div.jsoneditor-node-container {
&.jsoneditor-hover {
background-color: $hoverAndSelectedColor;
&.jsoneditor-hover-insert-area {
&.jsoneditor-hover-insert-area-after {
background-color: $selectedColor;
> div.jsoneditor-insert-area {
@ -583,7 +583,7 @@ div.jsoneditor-node-container {
background-color: $hoverColor;
}
&.jsoneditor-selected-insert-area {
&.jsoneditor-selected-insert-area-before {
> div.jsoneditor-insert-area {
border: 1px dashed #f4af41;
background-color: $hoverAndSelectedColor;
@ -592,13 +592,13 @@ div.jsoneditor-node-container {
}
}
&.jsoneditor-selected-insert-area {
&.jsoneditor-selected-insert-area-before {
background-color: inherit;
&.jsoneditor-hover {
background-color: $hoverColor;
&.jsoneditor-hover-insert-area {
&.jsoneditor-hover-insert-area-after {
background-color: inherit;
}
}
@ -613,7 +613,7 @@ div.jsoneditor-node-container {
div.jsoneditor-hover {
background-color: $hoverAndSelectedColor;
&.jsoneditor-hover-insert-area {
&.jsoneditor-hover-insert-area-after {
background-color: inherit;
> div.jsoneditor-insert-area {
@ -627,7 +627,7 @@ div.jsoneditor-node-container {
&.jsoneditor-hover {
background-color: $hoverColor;
&.jsoneditor-hover-insert-area {
&.jsoneditor-hover-insert-area-after {
background-color: inherit;
> div.jsoneditor-insert-area {
@ -662,16 +662,29 @@ div.jsoneditor-node-container {
box-shadow: 0 2px 6px 0 rgba(0,0,0,.24);
&:after {
content:'';
position: absolute;
top: 100%;
left: 35px;
content: '';
position: absolute;
top: 100%;
left: 35px;
margin-left: -10px;
width: 0;
height: 0;
border-top: solid 10px $floating-menu-background;
border-bottom: none;
border-left: solid 10px transparent;
border-right: solid 10px transparent;
}
&.jsoneditor-floating-menu-bottom {
bottom: auto;
top: 100%;
&:after {
top: -10px;
margin-left: -10px;
width: 0;
height: 0;
border-top: solid 10px $floating-menu-background;
border-left: solid 10px transparent;
border-right: solid 10px transparent;
border-top: none;
border-bottom: solid 10px $floating-menu-background;
}
}
button.jsoneditor-floating-menu-item {

View File

@ -2,6 +2,7 @@ import { createElement as h, PureComponent } from 'react'
import PropTypes from 'prop-types'
const MENU_CLASS_NAME = 'jsoneditor-floating-menu'
const MENU_CLASS_NAME_BOTTOM = 'jsoneditor-floating-menu-bottom'
const MENU_ITEM_CLASS_NAME = 'jsoneditor-floating-menu-item'
// Array: Sort | Map | Filter | Duplicate | Cut | Copy | Paste | Remove
@ -134,7 +135,10 @@ export default class FloatingMenu extends PureComponent {
type: PropTypes.string.isRequired
})
]).isRequired
).isRequired
).isRequired,
path: PropTypes.arrayOf(PropTypes.string).isRequired,
emit: PropTypes.func.isRequired,
position: PropTypes.string // 'top' or 'bottom'
}
render () {
@ -150,7 +154,8 @@ export default class FloatingMenu extends PureComponent {
})
return h('div', {
className: MENU_CLASS_NAME,
className: MENU_CLASS_NAME +
(this.props.position === 'bottom' ? (' ' + MENU_CLASS_NAME_BOTTOM) : ''),
onMouseDown: this.handleTouchStart,
onTouchStart: this.handleTouchStart,
}, items)

View File

@ -13,9 +13,12 @@ import initial from 'lodash/initial'
import last from 'lodash/last'
export const SELECTED = 1
export const SELECTED_END = 2
export const SELECTED_BEFORE = 3
export const SELECTED_AFTER = 4
export const SELECTED_START = 2
export const SELECTED_END = 4
export const SELECTED_FIRST = 8
export const SELECTED_LAST = 16
export const SELECTED_BEFORE = 32
export const SELECTED_AFTER = 64
export const META = Symbol('meta')
@ -349,7 +352,7 @@ function setSearchStatus (eson, esonPointer, searchStatus) {
/**
* Merge selection status into the eson object, cleanup previous selection
* @param {ESON} eson
* @param {Selection} [selection]
* @param {Selection | null} selection
* @return {ESON} Returns updated eson object
*/
export function applySelection (eson, selection) {
@ -357,11 +360,13 @@ export function applySelection (eson, selection) {
return cleanupMetaData(eson, 'selected')
}
else if (selection.before) {
const updatedEson = setIn(eson, selection.before.concat([META, 'selected']), SELECTED_BEFORE)
const updatedEson = setIn(eson, selection.before.concat([META, 'selected']),
SELECTED + SELECTED_BEFORE)
return cleanupMetaData(updatedEson, 'selected', [selection.before])
}
else if (selection.after) {
const updatedEson = setIn(eson, selection.after.concat([META, 'selected']), SELECTED_AFTER)
const updatedEson = setIn(eson, selection.after.concat([META, 'selected']),
SELECTED + SELECTED_AFTER)
return cleanupMetaData(updatedEson, 'selected', [selection.after])
}
else { // selection.start and selection.end
@ -379,15 +384,21 @@ export function applySelection (eson, selection) {
const startIndex = root[META].props.indexOf(start)
const endIndex = root[META].props.indexOf(end)
const minIndex = Math.min(startIndex, endIndex)
const maxIndex = Math.max(startIndex, endIndex) + 1 // include max index itself
const firstIndex = Math.min(startIndex, endIndex)
const lastIndex = Math.max(startIndex, endIndex) + 1 // include max index itself
const firstProp = root[META].props[firstIndex]
const lastProp = root[META].props[lastIndex - 1]
const selectedProps = root[META].props.slice(minIndex, maxIndex)
const selectedProps = root[META].props.slice(firstIndex, lastIndex)
selectedPaths = selectedProps.map(prop => rootPath.concat(prop))
let updatedObj = cloneWithSymbols(root)
selectedProps.forEach(prop => {
updatedObj[prop] = setIn(updatedObj[prop], [META, 'selected'],
prop === end ? SELECTED_END : SELECTED)
const selected = SELECTED +
(prop === start ? SELECTED_START : 0) +
(prop === end ? SELECTED_END : 0) +
(prop === firstProp ? SELECTED_FIRST : 0) +
(prop === lastProp ? SELECTED_LAST : 0)
updatedObj[prop] = setIn(updatedObj[prop], [META, 'selected'], selected)
})
return updatedObj
@ -396,17 +407,21 @@ export function applySelection (eson, selection) {
const startIndex = parseInt(start, 10)
const endIndex = parseInt(end, 10)
const minIndex = Math.min(startIndex, endIndex)
const maxIndex = Math.max(startIndex, endIndex) + 1 // include max index itself
const firstIndex = Math.min(startIndex, endIndex)
const lastIndex = Math.max(startIndex, endIndex) + 1 // include max index itself
const selectedIndices = range(minIndex, maxIndex)
const selectedIndices = range(firstIndex, lastIndex)
selectedPaths = selectedIndices.map(index => rootPath.concat(String(index)))
let updatedArr = root.slice()
updatedArr = cloneWithSymbols(root)
selectedIndices.forEach(index => {
updatedArr[index] = setIn(updatedArr[index], [META, 'selected'],
index === endIndex ? SELECTED_END : SELECTED)
const selected = SELECTED +
(index === start ? SELECTED_START : 0) +
(index === end ? SELECTED_END : 0) +
(index === firstIndex ? SELECTED_FIRST : 0) +
(index === lastIndex ? SELECTED_LAST : 0)
updatedArr[index] = setIn(updatedArr[index], [META, 'selected'], selected)
})
return updatedArr