From 8425579718b885a3d926f0b43ade16891700917b Mon Sep 17 00:00:00 2001 From: jos Date: Fri, 3 Nov 2017 14:23:04 +0100 Subject: [PATCH] Floating menu starting to work (WIP) --- src/components/JSONNode.js | 212 +++++++++++++++++----------- src/components/TreeMode.js | 46 +++--- src/components/menu/FloatingMenu.js | 40 +++++- src/components/utils/domSelector.js | 2 +- src/eson.js | 14 +- src/jsoneditor.less | 108 +++++++++++--- src/types.js | 4 +- test/eson.test.js | 38 ++--- 8 files changed, 316 insertions(+), 148 deletions(-) diff --git a/src/components/JSONNode.js b/src/components/JSONNode.js index a44d6ef..a825ba3 100644 --- a/src/components/JSONNode.js +++ b/src/components/JSONNode.js @@ -8,10 +8,25 @@ import FloatingMenu from './menu/FloatingMenu' import { escapeHTML, unescapeHTML } from '../utils/stringUtils' import { getInnerText, insideRect, findParentWithAttribute } from '../utils/domUtils' import { stringConvert, valueType, isUrl } from '../utils/typeUtils' -import { compileJSONPointer, SELECTED, SELECTED_END } from '../eson' +import { compileJSONPointer, SELECTED, SELECTED_END, SELECTED_AFTER, SELECTED_BEFORE } from '../eson' import type { ESONObjectProperty, ESON, SearchResultStatus, Path } from '../types' +// 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-after', + [SELECTED_BEFORE]: ' jsoneditor-selected jsoneditor-selected-before', +} + +const HOVERED_CLASS_NAMES = { + [SELECTED]: ' jsoneditor-hover', + [SELECTED_END]: ' jsoneditor-hover jsoneditor-hover-end', + [SELECTED_AFTER]: ' jsoneditor-hover jsoneditor-hover-after', + [SELECTED_BEFORE]: ' jsoneditor-hover jsoneditor-hover-before', +} + export default class JSONNode extends PureComponent { static URL_TITLE = 'Ctrl+Click or Ctrl+Enter to open url' @@ -21,6 +36,12 @@ export default class JSONNode extends PureComponent { hover: false } + componentWillUnmount () { + if (hoveredNode === this) { + hoveredNode = null + } + } + render () { const { props } = this @@ -38,9 +59,8 @@ export default class JSONNode extends PureComponent { renderJSONObject ({prop, index, data, options, events}) { const childCount = data.props.length const node = h('div', { - 'data-path': compileJSONPointer(this.props.path), - onKeyDown: this.handleKeyDown, key: 'node', + onKeyDown: this.handleKeyDown, className: 'jsoneditor-node jsoneditor-object' }, [ this.renderExpandButton(), @@ -55,60 +75,49 @@ export default class JSONNode extends PureComponent { let childs if (data.expanded) { if (data.props.length > 0) { - const props = data.props.map(prop => { - return h('li', { key: prop.id, className: JSONNode.selectedClassName(prop.value.selected) }, - h(this.constructor, { - path: this.props.path.concat(prop.name), - prop, - data: prop.value, - options, - events - }) - ) - }) + const props = data.props.map(prop => h(this.constructor, { + key: prop.id, + path: this.props.path.concat(prop.name), + prop, + data: prop.value, + options, + events + })) - childs = h('ul', {key: 'childs', className: 'jsoneditor-list'}, props) + childs = h('div', {key: 'childs', className: 'jsoneditor-list'}, props) } else { - childs = h('ul', {key: 'childs', className: 'jsoneditor-list'}, - h('li', {}, - this.renderAppend('(empty object)') - ) + childs = h('div', {key: 'childs', className: 'jsoneditor-list'}, + this.renderAppend('(empty object)') ) } } - const floatingMenu = this.renderFloatingMenu([ - {type: 'sort'}, - {type: 'duplicate'}, - {type: 'cut'}, - {type: 'copy'}, - {type: 'paste'}, - {type: 'remove'} - ]) + const floatingMenu = (data.selected === SELECTED_END) + ? this.renderFloatingMenu([ + {type: 'sort'}, + {type: 'duplicate'}, + {type: 'cut'}, + {type: 'copy'}, + {type: 'paste'}, + {type: 'remove'} + ]) + : null return h('div', { - className: 'jsoneditor-node-container ' + (this.state.hover ? ' jsoneditor-node-hover': ''), + 'data-path': compileJSONPointer(this.props.path), + className: this.getContainerClassName(data.selected, this.state.hover), onMouseOver: this.handleMouseOver, onMouseLeave: this.handleMouseLeave }, [node, floatingMenu, childs]) } - static selectedClassName(selected: number) { - return (selected === SELECTED) - ? ' jsoneditor-selected' - : (selected === SELECTED_END) - ? 'jsoneditor-selected jsoneditor-selected-end' - : '' - } - // TODO: extract a function renderChilds shared by both renderJSONObject and renderJSONArray (rename .props and .items to .childs?) renderJSONArray ({prop, index, data, options, events}) { const childCount = data.items.length const node = h('div', { - 'data-path': compileJSONPointer(this.props.path), - onKeyDown: this.handleKeyDown, key: 'node', + onKeyDown: this.handleKeyDown, className: 'jsoneditor-node jsoneditor-array' }, [ this.renderExpandButton(), @@ -123,39 +132,38 @@ export default class JSONNode extends PureComponent { let childs if (data.expanded) { if (data.items.length > 0) { - const items = data.items.map((item, index) => { - return h('li', { key : item.id, className: JSONNode.selectedClassName(prop.value.selected)}, - h(this.constructor, { - path: this.props.path.concat(String(index)), - index, - data: item.value, - options, - events - }) - ) - }) - childs = h('ul', {key: 'childs', className: 'jsoneditor-list'}, items) + const items = data.items.map((item, index) => h(this.constructor, { + key : item.id, + path: this.props.path.concat(String(index)), + index, + data: item.value, + options, + events + })) + + childs = h('div', {key: 'childs', className: 'jsoneditor-list'}, items) } else { - childs = h('ul', {key: 'childs', className: 'jsoneditor-list'}, - h('li', {}, - this.renderAppend('(empty array)') - ) + childs = h('div', {key: 'childs', className: 'jsoneditor-list'}, + this.renderAppend('(empty array)') ) } } - const floatingMenu = this.renderFloatingMenu([ - {type: 'sort'}, - {type: 'duplicate'}, - {type: 'cut'}, - {type: 'copy'}, - {type: 'paste'}, - {type: 'remove'} - ]) + const floatingMenu = (data.selected === SELECTED_END) + ? this.renderFloatingMenu([ + {type: 'sort'}, + {type: 'duplicate'}, + {type: 'cut'}, + {type: 'copy'}, + {type: 'paste'}, + {type: 'remove'} + ]) + : null return h('div', { - className: 'jsoneditor-node-container ' + (this.state.hover ? ' jsoneditor-node-hover': ''), + 'data-path': compileJSONPointer(this.props.path), + className: this.getContainerClassName(data.selected, this.state.hover), onMouseOver: this.handleMouseOver, onMouseLeave: this.handleMouseLeave }, [node, floatingMenu, childs]) @@ -163,8 +171,7 @@ export default class JSONNode extends PureComponent { renderJSONValue ({prop, index, data, options}) { const node = h('div', { - key: 'value', - 'data-path': compileJSONPointer(this.props.path), + key: 'node', onKeyDown: this.handleKeyDown, className: 'jsoneditor-node' }, [ @@ -178,20 +185,43 @@ export default class JSONNode extends PureComponent { this.renderError(data.error) ]) - const floatingMenu = this.renderFloatingMenu([ - // {text: 'String', onClick: this.props.events.onChangeType, type: 'checkbox', checked: false}, - {type: 'duplicate'}, - {type: 'cut'}, - {type: 'copy'}, - {type: 'paste'}, - {type: 'remove'} - ]) + const floatingMenu = (data.selected === SELECTED_END) + ? this.renderFloatingMenu([ + // {text: 'String', onClick: this.props.events.onChangeType, type: 'checkbox', checked: false}, + {type: 'duplicate'}, + {type: 'cut'}, + {type: 'copy'}, + {type: 'paste'}, + {type: 'remove'} + ]) + : null + + const insertArea = this.renderInsertArea() return h('div', { - className: 'jsoneditor-node-container ' + (this.state.hover ? ' jsoneditor-node-hover': ''), + 'data-path': compileJSONPointer(this.props.path), + className: this.getContainerClassName(data.selected, this.state.hover), onMouseOver: this.handleMouseOver, onMouseLeave: this.handleMouseLeave - }, [node, floatingMenu]) + }, [node, floatingMenu, insertArea]) + } + + renderInsertArea () { + const floatingMenu = (this.props.data.selected === SELECTED_AFTER) + ? this.renderFloatingMenu([ + {type: 'insertStructure'}, + {type: 'insertValue'}, + {type: 'insertObject'}, + {type: 'insertArray'}, + {type: 'paste'}, + ]) + : null + + return h('div', { + key: 'menu', + className: 'jsoneditor-insert-area', + 'data-area': 'after' + }, [floatingMenu]) } /** @@ -311,6 +341,12 @@ export default class JSONNode extends PureComponent { } } + getContainerClassName (selected, hover) { + return 'jsoneditor-node-container' + + (hover ? (HOVERED_CLASS_NAMES[hover]) : '') + + (selected ? (SELECTED_CLASS_NAMES[selected]) : '') + } + /** * Find the best position for the popover: right, above, below, or left * from the warning icon. @@ -473,7 +509,7 @@ export default class JSONNode extends PureComponent { renderFloatingMenu (items) { return h(FloatingMenu, { - key: 'menu', + key: 'floating-menu', path: this.props.path, events: this.props.events, items @@ -495,23 +531,31 @@ export default class JSONNode extends PureComponent { } handleMouseOver = (event) => { - event.stopPropagation() + if (event.buttons === 0) { // no mouse button down, no dragging + event.stopPropagation() - if (hoveredNode !== this) { + const hover = (event.target.className.indexOf('jsoneditor-insert-area') !== -1) + ? SELECTED_AFTER + : SELECTED - if (hoveredNode) { - // FIXME: this may give issues when the hovered node doesn't exist anymore. check whether mounted - hoveredNode.setState({hover: false}) + if (hoveredNode && hoveredNode !== this) { + // FIXME: this gives issues when the hovered node doesn't exist anymore. check whether mounted? + hoveredNode.setState({hover: null}) } - this.setState({hover: true}) - hoveredNode = this + if (hover !== this.state.hover) { + this.setState({hover}) + hoveredNode = this + } } } handleMouseLeave = (event) => { event.stopPropagation() - this.setState({hover: false}) + // FIXME: this gives issues when the hovered node doesn't exist anymore. check whether mounted? + hoveredNode.setState({hover: false}) + + this.setState({hover: null}) } handleOpenActionMenu = (event) => { diff --git a/src/components/TreeMode.js b/src/components/TreeMode.js index 857dcfb..d1f9e96 100644 --- a/src/components/TreeMode.js +++ b/src/components/TreeMode.js @@ -37,7 +37,7 @@ import { import { createFindKeyBinding } from '../utils/keyBindings' import { KEY_BINDINGS } from '../constants' -import type { ESON, ESONPatch, JSONPath, ESONSelection } from '../types' +import type { ESON, ESONPatch, JSONPath, ESONSelection, ESONPointer } from '../types' const AJV_OPTIONS = { allErrors: true, @@ -101,6 +101,8 @@ export default class TreeMode extends Component { onExpand: this.handleExpand, + onSelect: this.handleSelect, + // TODO: now we're passing not just events but also other methods. reorganize this or rename 'state.events' findKeyBinding: this.handleFindKeyBinding }, @@ -205,12 +207,14 @@ export default class TreeMode extends Component { h(Hammer, { id: this.id, direction: 'DIRECTION_VERTICAL', - onTap: this.handleTap, - onPanStart: this.handlePanStart, onPan: this.handlePan, onPanEnd: this.handlePanEnd }, - h('ul', {className: 'jsoneditor-list jsoneditor-root' + (data.selected ? ' jsoneditor-selected' : '')}, + h('div', { + onMouseDown: this.handleTouchStart, + onTouchStart: this.handleTouchStart, + className: 'jsoneditor-list jsoneditor-root' + + (data.selected ? ' jsoneditor-selected' : '')}, h(Node, { data, events: state.events, @@ -364,6 +368,7 @@ export default class TreeMode extends Component { moveUp(fromElement, 'property') } + this.setState({ selection : null }) this.handlePatch(remove(path)) } @@ -535,6 +540,11 @@ export default class TreeMode extends Component { this.handlePatch(sort(this.state.data, path, order)) } + handleSelect = (selection: ESONSelection) => { + console.log('handleSelect', selection) + this.setState({ selection }) + } + handleExpand = (path, expanded, recurse) => { if (recurse) { const esonPath = toEsonPath(this.state.data, path) @@ -661,22 +671,13 @@ export default class TreeMode extends Component { this.emitOnChange (actions, result.revert, result.data) } - handleTap = (event) => { - const path = this.findDataPathFromElement(event.target.firstChild) - if (this.state.selection) { - this.setState({ selection: {start: {path}, end: {path}}}) + handleTouchStart = (event) => { + const pointer = this.findESONPointerFromElement(event.target) + if (pointer) { + this.setState({ selection: {start: pointer, end: pointer}}) } - } - - handlePanStart = (event) => { - const path = this.findDataPathFromElement(event.target.firstChild) - if (path) { - this.setState({ - selection: { - start: {path}, - end: {path} - } - }) + else { + this.setState({ selection: null }) } } @@ -713,6 +714,13 @@ export default class TreeMode extends Component { return attr ? parseJSONPointer(attr.replace(/\/-$/, '')) : null } + findESONPointerFromElement (element: Element) : ESONPointer { + const path = this.findDataPathFromElement(element) + const area = element && element.getAttribute && element.getAttribute('data-area') || null + + return { path, area } + } + /** * Scroll the window vertically to the node with given path * @param {Path} path diff --git a/src/components/menu/FloatingMenu.js b/src/components/menu/FloatingMenu.js index e271e05..ae95b59 100644 --- a/src/components/menu/FloatingMenu.js +++ b/src/components/menu/FloatingMenu.js @@ -84,11 +84,49 @@ const CREATE_TYPE = { onClick: () => events.onRemove(path), title: 'Remove' }, 'Remove'), + + insertStructure: (path, events) => h('button', { + key: 'insertStructure', + className: MENU_ITEM_CLASS_NAME, + // onClick: () => events.onRemove(path), + title: 'Insert a new object with the same data structure as the item above' + }, 'Insert structure'), + + insertValue: (path, events) => h('button', { + key: 'insertValue', + className: MENU_ITEM_CLASS_NAME, + // onClick: () => events.onRemove(path), + title: 'Insert value' + }, 'Insert value'), + + insertObject: (path, events) => h('button', { + key: 'insertObject', + className: MENU_ITEM_CLASS_NAME, + // onClick: () => events.onRemove(path), + title: 'Insert Object' + }, 'Insert Object'), + + insertArray: (path, events) => h('button', { + key: 'insertArray', + className: MENU_ITEM_CLASS_NAME, + // onClick: () => events.onRemove(path), + title: 'Insert Array' + }, 'Insert Array'), + } export default class FloatingMenu extends PureComponent { + componentDidMount () { + setTimeout(() => { + const firstButton = this.refs.root && this.refs.root.querySelector('button') + if (firstButton) { + firstButton.focus() + } + }) + } + render () { - return h('div', {className: MENU_CLASS_NAME}, this.props.items.map(item => { + return h('div', {ref: 'root', className: MENU_CLASS_NAME}, this.props.items.map(item => { const type = typeof item === 'string' ? item : item.type const createType = CREATE_TYPE[type] if (createType) { diff --git a/src/components/utils/domSelector.js b/src/components/utils/domSelector.js index ad38f2e..4477d78 100644 --- a/src/components/utils/domSelector.js +++ b/src/components/utils/domSelector.js @@ -10,7 +10,7 @@ let lastInputName = null // TODO: create a constants file with the CSS names that are used in domSelector const SEARCH_TEXT_CLASS_NAME = 'jsoneditor-search-text' const SEARCH_COMPONENT_CLASS_NAME = 'jsoneditor-search' -const NODE_CONTAINER_CLASS_NAME = 'jsoneditor-node' +const NODE_CONTAINER_CLASS_NAME = 'jsoneditor-node-container' const CONTENTS_CONTAINER_CLASS_NAME = 'jsoneditor-tree-contents' const PROPERTY_CLASS_NAME = 'jsoneditor-property' const VALUE_CLASS_NAME = 'jsoneditor-value' diff --git a/src/eson.js b/src/eson.js index 8e51cde..9e03cbf 100644 --- a/src/eson.js +++ b/src/eson.js @@ -22,6 +22,8 @@ type RecurseCallback = (value: ESON, path: Path, root: ESON) => ESON export const SELECTED = 1 export const SELECTED_END = 2 +export const SELECTED_BEFORE = 3 +export const SELECTED_AFTER = 4 /** * Expand function which will expand all nodes @@ -263,7 +265,7 @@ export function search (eson: ESON, text: string): ESONPointer[] { const parentPath = initial(path) const parent = getIn(eson, toEsonPath(eson, parentPath)) if (parent.type === 'Object') { - results.push({path, field: 'property'}) + results.push({path, area: 'property'}) } } } @@ -271,7 +273,7 @@ export function search (eson: ESON, text: string): ESONPointer[] { // check value if (value.type === 'value') { if (containsCaseInsensitive(value.value, text)) { - results.push({path, field: 'value'}) + results.push({path, area: 'value'}) } } }) @@ -335,13 +337,13 @@ export function applySearchResults (eson: ESON, searchResults: ESONPointer[], ac let updatedEson = eson searchResults.forEach(function (searchResult) { - if (searchResult.field === 'value') { + if (searchResult.area === 'value') { const esonPath = toEsonPath(updatedEson, searchResult.path).concat('searchResult') const value = isEqual(searchResult, activeSearchResult) ? 'active' : 'normal' updatedEson = setIn(updatedEson, esonPath, value) } - if (searchResult.field === 'property') { + if (searchResult.area === 'property') { const esonPath = toEsonPath(updatedEson, searchResult.path) const propertyPath = initial(esonPath).concat('searchResult') const value = isEqual(searchResult, activeSearchResult) ? 'active' : 'normal' @@ -366,8 +368,10 @@ export function applySelection (eson: ESON, selection: ESONSelection) { if (rootPath.length === selection.start.path.length || rootPath.length === selection.end.path.length) { // select a single node - return setIn(eson, rootEsonPath.concat(['selected']), SELECTED_END) + const selectionType = (selection.start.area === 'after') ? SELECTED_AFTER : SELECTED_END +console.log('selectionType', selectionType, selection) // FIXME: actually mark the end index as SELECTED_END, currently we select the first index + return setIn(eson, rootEsonPath.concat(['selected']), selectionType) } else { // select multiple childs of an object or array diff --git a/src/jsoneditor.less b/src/jsoneditor.less index a4e4c3c..0fe6f36 100644 --- a/src/jsoneditor.less +++ b/src/jsoneditor.less @@ -9,7 +9,8 @@ @floating-menu-color: #fff; // @selectedColor: #e5e5e5; @selectedColor: #ffed99; -@hoverColor: rgba(10, 10, 10, 0.05); +@hoverColor: #f2f2f2; +@hoverAndSelectedColor: #F2E191; .jsoneditor { border: 1px solid @theme-color; @@ -119,16 +120,17 @@ flex: 0 0 auto; } -ul.jsoneditor-list { +div.jsoneditor-list { list-style-type: none; padding-left: 20px; margin: 0; font-size: 0; } -/* no left padding for the root ul element */ -.jsoneditor-contents > ul.jsoneditor-list { +/* no left padding for the root div element */ +.jsoneditor-contents > div.jsoneditor-list { padding-left: 2px; + padding-bottom: 24px; } .jsoneditor-property, @@ -261,10 +263,6 @@ div.jsoneditor-value.jsoneditor-empty::after { content: 'value'; } -.jsoneditor-selected { - background-color: @selectedColor; -} - .jsoneditor-highlight { background-color: yellow; } @@ -567,8 +565,90 @@ div.jsoneditor-node-container { position: relative; transition: background-color 100ms ease-in; + // TODO: can the hover/select css be simplified? + + &.jsoneditor-selected { + background-color: @selectedColor; + + &.jsoneditor-hover { + background-color: @hoverAndSelectedColor; + + &.jsoneditor-hover-after { + background-color: @selectedColor; + + div.jsoneditor-insert-area { + border: 1px dashed gray; + background-color: @hoverColor; + } + + &.jsoneditor-selected-after { + div.jsoneditor-insert-area { + border: 1px dashed #f4af41; + background-color: @hoverAndSelectedColor; + } + } + } + } + + &.jsoneditor-selected-after { + background-color: inherit; + + &.jsoneditor-hover { + background-color: @hoverColor; + + &.jsoneditor-hover-after { + background-color: inherit; + } + } + + div.jsoneditor-insert-area { + border: 1px dashed #f4af41; + background: @selectedColor; + } + } + + // hovering nested elements + .jsoneditor-hover { + background-color: @hoverAndSelectedColor; + + &.jsoneditor-hover-after { + background-color: inherit; + + div.jsoneditor-insert-area { + border: 1px dashed #f4af41; + background: @hoverAndSelectedColor; + } + } + } + } + + &.jsoneditor-hover { + background-color: @hoverColor; + + &.jsoneditor-hover-after { + background-color: inherit; + + div.jsoneditor-insert-area { + border: 1px dashed gray; + background-color: @hoverColor; + } + } + } + + div.jsoneditor-insert-area { + @height: 8px; + + position: absolute; + width: 100%; + height: @height; + left: 0; + bottom: -@height/2; + border: 1px transparent; + box-sizing: border-box; + z-index: 1; // must be on top of next node, it overlaps a bit + } + div.jsoneditor-floating-menu { - display: none; position: absolute; bottom: 100%; right: 0; @@ -599,7 +679,9 @@ div.jsoneditor-node-container { border-right: 1px solid lighten(@floating-menu-background, 10%); padding: 10px; cursor: pointer; + outline: none; + &:focus, &:hover { background: lighten(@floating-menu-background, 10%); } @@ -616,14 +698,6 @@ div.jsoneditor-node-container { } } } - - &.jsoneditor-node-hover { - background-color: @hoverColor; - } -} - -.jsoneditor-selected-end > .jsoneditor-node-container > div.jsoneditor-floating-menu { - display: inherit; } /******************************* **********************************/ diff --git a/src/types.js b/src/types.js index 8c39ff1..4e05318 100644 --- a/src/types.js +++ b/src/types.js @@ -33,7 +33,7 @@ export type JSONArrayType = JSONType[] /********************** TYPES FOR THE ESON OBJECT MODEL *************************/ export type SearchResultStatus = 'normal' | 'active' -export type ESONPointerField = 'value' | 'property' +export type ESONPointerArea = 'value' | 'property' | 'before' | 'after' export type ESONObjectProperty = { id: number, @@ -78,7 +78,7 @@ export type ESONPath = string[] export type ESONPointer = { path: JSONPath, // TODO: change path to an ESONPath? - field?: ESONPointerField + area?: ESONPointerArea } export type ESONSelection = { diff --git a/test/eson.test.js b/test/eson.test.js index 31a20be..3ea22f8 100644 --- a/test/eson.test.js +++ b/test/eson.test.js @@ -194,12 +194,12 @@ test('search', t => { // printJSON(searchResults) t.deepEqual(searchResults, [ - {path: ['obj', 'arr', '2', 'last'], field: 'property'}, - {path: ['str'], field: 'value'}, - {path: ['nill'], field: 'property'}, - {path: ['nill'], field: 'value'}, - {path: ['bool'], field: 'property'}, - {path: ['bool'], field: 'value'} + {path: ['obj', 'arr', '2', 'last'], area: 'property'}, + {path: ['str'], area: 'value'}, + {path: ['nill'], area: 'property'}, + {path: ['nill'], area: 'value'}, + {path: ['bool'], area: 'property'}, + {path: ['bool'], area: 'value'} ]) const activeSearchResult = searchResults[0] @@ -219,30 +219,30 @@ test('search', t => { test('nextSearchResult', t => { const searchResults = [ - {path: ['obj', 'arr', '2', 'last'], field: 'property'}, - {path: ['str'], field: 'value'}, - {path: ['nill'], field: 'property'}, - {path: ['nill'], field: 'value'}, - {path: ['bool'], field: 'property'}, - {path: ['bool'], field: 'value'} + {path: ['obj', 'arr', '2', 'last'], area: 'property'}, + {path: ['str'], area: 'value'}, + {path: ['nill'], area: 'property'}, + {path: ['nill'], area: 'value'}, + {path: ['bool'], area: 'property'}, + {path: ['bool'], area: 'value'} ] t.deepEqual(nextSearchResult(searchResults, - {path: ['nill'], field: 'property'}), - {path: ['nill'], field: 'value'}) + {path: ['nill'], area: 'property'}), + {path: ['nill'], area: 'value'}) // wrap around t.deepEqual(nextSearchResult(searchResults, - {path: ['bool'], field: 'value'}), - {path: ['obj', 'arr', '2', 'last'], field: 'property'}) + {path: ['bool'], area: 'value'}), + {path: ['obj', 'arr', '2', 'last'], area: 'property'}) // return first when current is not found t.deepEqual(nextSearchResult(searchResults, - {path: ['non', 'existing'], field: 'value'}), - {path: ['obj', 'arr', '2', 'last'], field: 'property'}) + {path: ['non', 'existing'], area: 'value'}), + {path: ['obj', 'arr', '2', 'last'], area: 'property'}) // return null when searchResults are empty - t.deepEqual(nextSearchResult([], {path: ['non', 'existing'], field: 'value'}), null) + t.deepEqual(nextSearchResult([], {path: ['non', 'existing'], area: 'value'}), null) }) test('previousSearchResult', t => {