Implement insert
This commit is contained in:
parent
a6bb790f5e
commit
eea6e09bd8
|
@ -10,7 +10,6 @@
|
||||||
export let title = null
|
export let title = null
|
||||||
export let width = '120px'
|
export let width = '120px'
|
||||||
export let visible = false
|
export let visible = false
|
||||||
export let disabled = false
|
|
||||||
|
|
||||||
function toggleShow (event) {
|
function toggleShow (event) {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
@ -51,7 +50,11 @@
|
||||||
<ul>
|
<ul>
|
||||||
{#each items as item}
|
{#each items as item}
|
||||||
<li>
|
<li>
|
||||||
<button on:click={item.onClick} disabled={disabled}>
|
<button
|
||||||
|
on:click={item.onClick}
|
||||||
|
title={item.title}
|
||||||
|
disabled={item.disabled}
|
||||||
|
>
|
||||||
{item.text}
|
{item.text}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
import { tick } from 'svelte'
|
import { tick } from 'svelte'
|
||||||
import {
|
import {
|
||||||
append,
|
|
||||||
duplicate,
|
duplicate,
|
||||||
insertBefore,
|
insert,
|
||||||
removeAll,
|
createNewValue,
|
||||||
replace
|
removeAll
|
||||||
} from '../logic/operations.js'
|
} from '../logic/operations.js'
|
||||||
import {
|
import {
|
||||||
STATE_EXPANDED,
|
STATE_EXPANDED,
|
||||||
|
@ -18,7 +17,8 @@
|
||||||
import {
|
import {
|
||||||
createPathsMap,
|
createPathsMap,
|
||||||
createSelectionFromOperations,
|
createSelectionFromOperations,
|
||||||
expandSelection
|
expandSelection,
|
||||||
|
getParentPath
|
||||||
} from '../logic/selection.js'
|
} from '../logic/selection.js'
|
||||||
import { isContentEditableDiv } from '../utils/domUtils.js'
|
import { isContentEditableDiv } from '../utils/domUtils.js'
|
||||||
import {
|
import {
|
||||||
|
@ -34,6 +34,7 @@
|
||||||
import jump from '../assets/jump.js/src/jump.js'
|
import jump from '../assets/jump.js/src/jump.js'
|
||||||
import { expandPath, syncState, getNextKeys, patchProps } from '../logic/documentState.js'
|
import { expandPath, syncState, getNextKeys, patchProps } from '../logic/documentState.js'
|
||||||
import Menu from './Menu.svelte'
|
import Menu from './Menu.svelte'
|
||||||
|
import { isObjectOrArray } from '../utils/typeUtils.js';
|
||||||
|
|
||||||
let divContents
|
let divContents
|
||||||
let domHiddenInput
|
let domHiddenInput
|
||||||
|
@ -150,42 +151,10 @@
|
||||||
if (selection && clipboard) {
|
if (selection && clipboard) {
|
||||||
console.log('paste', { clipboard, selection })
|
console.log('paste', { clipboard, selection })
|
||||||
|
|
||||||
function createNewSelection (operations) {
|
const operations = insert(doc, state, selection, clipboard)
|
||||||
const paths = operations
|
const newSelection = createSelectionFromOperations(operations)
|
||||||
.filter(operation => operation.op === 'add')
|
|
||||||
.map(operation => parseJSONPointer(operation.path))
|
|
||||||
|
|
||||||
return {
|
handlePatch(operations, newSelection)
|
||||||
paths,
|
|
||||||
pathsMap: createPathsMap(paths)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selection.beforePath) {
|
|
||||||
const parentPath = initial(selection.beforePath)
|
|
||||||
const beforeKey = last(selection.beforePath)
|
|
||||||
const props = getIn(state, parentPath.concat(STATE_PROPS))
|
|
||||||
const nextKeys = getNextKeys(props, beforeKey, true)
|
|
||||||
const operations = insertBefore(doc, selection.beforePath, clipboard, nextKeys)
|
|
||||||
const newSelection = createSelectionFromOperations(operations)
|
|
||||||
|
|
||||||
handlePatch(operations, newSelection)
|
|
||||||
} else if (selection.appendPath) {
|
|
||||||
const operations = append(doc, selection.appendPath, clipboard)
|
|
||||||
const newSelection = createSelectionFromOperations(operations)
|
|
||||||
|
|
||||||
handlePatch(operations, newSelection)
|
|
||||||
} else if (selection.paths) {
|
|
||||||
const lastPath = last(selection.paths) // FIXME: here we assume selection.paths is sorted correctly, that's a dangerous assumption
|
|
||||||
const parentPath = initial(lastPath)
|
|
||||||
const beforeKey = last(lastPath)
|
|
||||||
const props = getIn(state, parentPath.concat(STATE_PROPS))
|
|
||||||
const nextKeys = getNextKeys(props, beforeKey, true)
|
|
||||||
const operations = replace(doc, selection.paths, clipboard, nextKeys)
|
|
||||||
const newSelection = createSelectionFromOperations(operations)
|
|
||||||
|
|
||||||
handlePatch(operations, newSelection)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,6 +162,8 @@
|
||||||
if (selection && selection.paths) {
|
if (selection && selection.paths) {
|
||||||
console.log('duplicate', { selection })
|
console.log('duplicate', { selection })
|
||||||
|
|
||||||
|
// TODO: move this logic inside duplicate()
|
||||||
|
|
||||||
const lastPath = last(selection.paths) // FIXME: here we assume selection.paths is sorted correctly, that's a dangerous assumption
|
const lastPath = last(selection.paths) // FIXME: here we assume selection.paths is sorted correctly, that's a dangerous assumption
|
||||||
const parentPath = initial(lastPath)
|
const parentPath = initial(lastPath)
|
||||||
const beforeKey = last(lastPath)
|
const beforeKey = last(lastPath)
|
||||||
|
@ -202,17 +173,35 @@
|
||||||
const operations = duplicate(doc, selection.paths, nextKeys)
|
const operations = duplicate(doc, selection.paths, nextKeys)
|
||||||
const newSelection = createSelectionFromOperations(operations)
|
const newSelection = createSelectionFromOperations(operations)
|
||||||
|
|
||||||
console.log('newSelection', newSelection)
|
|
||||||
handlePatch(operations, newSelection)
|
handlePatch(operations, newSelection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleInsert() {
|
/**
|
||||||
|
* @param {'value' | 'object' | 'array' | 'structure'} type
|
||||||
|
*/
|
||||||
|
function handleInsert(type) {
|
||||||
if (selection != null) {
|
if (selection != null) {
|
||||||
console.log('insert', { selection })
|
console.log('insert', { type, selection })
|
||||||
|
|
||||||
// TODO: impelemnt insert
|
const value = createNewValue(doc, selection, type)
|
||||||
|
const values = [
|
||||||
|
{
|
||||||
|
key: 'new',
|
||||||
|
value
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const operations = insert(doc, state, selection, values)
|
||||||
|
const newSelection = createSelectionFromOperations(operations)
|
||||||
|
|
||||||
|
handlePatch(operations, newSelection)
|
||||||
|
|
||||||
|
if (isObjectOrArray(value)) {
|
||||||
|
// expand the new object/array in case of inserting a structure
|
||||||
|
operations
|
||||||
|
.filter(operation => operation.op === 'add')
|
||||||
|
.forEach(operation => handleExpand(parseJSONPointer(operation.path), true, true))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -391,7 +380,7 @@
|
||||||
}
|
}
|
||||||
if (combo === 'Ctrl+Insert' || combo === 'Command+Insert') {
|
if (combo === 'Ctrl+Insert' || combo === 'Command+Insert') {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
handleInsert()
|
handleInsert('structure')
|
||||||
}
|
}
|
||||||
if (combo === 'Escape') {
|
if (combo === 'Escape') {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
@ -444,6 +433,8 @@
|
||||||
searchText={searchText}
|
searchText={searchText}
|
||||||
searchResult={searchResult}
|
searchResult={searchResult}
|
||||||
bind:showSearch
|
bind:showSearch
|
||||||
|
|
||||||
|
doc={doc}
|
||||||
selection={selection}
|
selection={selection}
|
||||||
clipboard={clipboard}
|
clipboard={clipboard}
|
||||||
|
|
||||||
|
@ -451,6 +442,7 @@
|
||||||
onCopy={handleCopy}
|
onCopy={handleCopy}
|
||||||
onPaste={handlePaste}
|
onPaste={handlePaste}
|
||||||
onDuplicate={handleDuplicate}
|
onDuplicate={handleDuplicate}
|
||||||
|
onInsert={handleInsert}
|
||||||
onUndo={handleUndo}
|
onUndo={handleUndo}
|
||||||
onRedo={handleRedo}
|
onRedo={handleRedo}
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,13 @@
|
||||||
import { faCut, faClone, faCopy, faPaste, faSearch, faUndo, faRedo, faPlus } from '@fortawesome/free-solid-svg-icons'
|
import { faCut, faClone, faCopy, faPaste, faSearch, faUndo, faRedo, faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||||
import SearchBox from './SearchBox.svelte'
|
import SearchBox from './SearchBox.svelte'
|
||||||
import DropdownMenu from './DropdownMenu.svelte'
|
import DropdownMenu from './DropdownMenu.svelte'
|
||||||
|
import { getParentPath } from '../logic/selection'
|
||||||
|
import { getIn } from '../utils/immutabilityHelpers'
|
||||||
|
|
||||||
export let searchText
|
export let searchText
|
||||||
export let searchResult
|
export let searchResult
|
||||||
export let showSearch = false
|
export let showSearch = false
|
||||||
|
export let doc
|
||||||
export let selection
|
export let selection
|
||||||
export let clipboard
|
export let clipboard
|
||||||
export let historyState
|
export let historyState
|
||||||
|
@ -15,6 +18,7 @@
|
||||||
export let onCopy
|
export let onCopy
|
||||||
export let onPaste
|
export let onPaste
|
||||||
export let onDuplicate
|
export let onDuplicate
|
||||||
|
export let onInsert
|
||||||
export let onUndo
|
export let onUndo
|
||||||
export let onRedo
|
export let onRedo
|
||||||
|
|
||||||
|
@ -24,6 +28,7 @@
|
||||||
|
|
||||||
$: hasSelection = selection != null
|
$: hasSelection = selection != null
|
||||||
$: hasSelectionContents = selection != null && selection.paths != null
|
$: hasSelectionContents = selection != null && selection.paths != null
|
||||||
|
$: hasSelectionWithoutContents = selection != null && selection.paths == null
|
||||||
$: hasClipboardContents = clipboard != null && selection != null
|
$: hasClipboardContents = clipboard != null && selection != null
|
||||||
|
|
||||||
function handleToggleSearch() {
|
function handleToggleSearch() {
|
||||||
|
@ -36,33 +41,36 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleInsertValue () {
|
function handleInsertValue () {
|
||||||
console.log('TODO: insert value')
|
onInsert('value')
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {MenuDropdownItem[]} */
|
/** @type {MenuDropdownItem[]} */
|
||||||
const insertItems = [
|
$: insertItems = [
|
||||||
{
|
{
|
||||||
text: "Insert value",
|
text: 'Insert value',
|
||||||
|
title: 'Insert a new value',
|
||||||
onClick: handleInsertValue,
|
onClick: handleInsertValue,
|
||||||
|
disabled: !hasSelectionWithoutContents,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Insert object",
|
text: 'Insert object',
|
||||||
onClick: () => {
|
title: 'Insert a new object',
|
||||||
console.log('TODO: insert object')
|
onClick: () => onInsert('object'),
|
||||||
}
|
disabled: !hasSelectionWithoutContents
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Insert array",
|
text: 'Insert array',
|
||||||
onClick: () => {
|
title: 'Insert a new array',
|
||||||
console.log('TODO: insert array')
|
onClick: () => onInsert('array'),
|
||||||
}
|
disabled: !hasSelectionWithoutContents
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Insert structure",
|
text: 'Insert structure',
|
||||||
onClick: () => {
|
title: 'Insert a new item with the same structure as other items. ' +
|
||||||
console.log('TODO: insert structure')
|
'Only applicable inside an array',
|
||||||
}
|
onClick: () => onInsert('structure'),
|
||||||
|
disabled: !hasSelectionWithoutContents
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
@ -107,13 +115,12 @@
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
items={insertItems}
|
items={insertItems}
|
||||||
title="Insert new value (Ctrl+Insert)"
|
title="Insert new value (Ctrl+Insert)"
|
||||||
disabled={!hasSelection}
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="button insert"
|
class="button insert"
|
||||||
slot="defaultItem"
|
slot="defaultItem"
|
||||||
on:click={handleInsertValue}
|
on:click={handleInsertValue}
|
||||||
disabled={!hasSelection}
|
disabled={!hasSelectionWithoutContents}
|
||||||
>
|
>
|
||||||
<Icon data={faPlus} />
|
<Icon data={faPlus} />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
import { first, initial, last, pickBy } from 'lodash-es'
|
import { first, initial, isEmpty, last, pickBy, cloneDeepWith } from 'lodash-es'
|
||||||
import { getIn } from '../utils/immutabilityHelpers'
|
import { STATE_PROPS } from '../constants.js'
|
||||||
import { compileJSONPointer } from '../utils/jsonPointer'
|
import { getNextKeys } from '../logic/documentState.js'
|
||||||
import { findUniqueName } from '../utils/stringUtils'
|
import { getParentPath } from '../logic/selection.js'
|
||||||
|
import { getIn } from '../utils/immutabilityHelpers.js'
|
||||||
|
import { compileJSONPointer } from '../utils/jsonPointer.js'
|
||||||
|
import { findUniqueName } from '../utils/stringUtils.js'
|
||||||
|
import { isObjectOrArray } from '../utils/typeUtils.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a JSONPatch for an insert operation.
|
* Create a JSONPatch for an insert operation.
|
||||||
|
@ -229,6 +233,66 @@ export function duplicate (json, paths, nextKeys) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function insert (doc, state, selection, values) {
|
||||||
|
if (selection.beforePath) {
|
||||||
|
const parentPath = initial(selection.beforePath)
|
||||||
|
const beforeKey = last(selection.beforePath)
|
||||||
|
const props = getIn(state, parentPath.concat(STATE_PROPS))
|
||||||
|
const nextKeys = getNextKeys(props, beforeKey, true)
|
||||||
|
const operations = insertBefore(doc, selection.beforePath, values, nextKeys)
|
||||||
|
|
||||||
|
return operations
|
||||||
|
} else if (selection.appendPath) {
|
||||||
|
const operations = append(doc, selection.appendPath, values)
|
||||||
|
|
||||||
|
return operations
|
||||||
|
} else if (selection.paths) {
|
||||||
|
const lastPath = last(selection.paths) // FIXME: here we assume selection.paths is sorted correctly, that's a dangerous assumption
|
||||||
|
const parentPath = initial(lastPath)
|
||||||
|
const beforeKey = last(lastPath)
|
||||||
|
const props = getIn(state, parentPath.concat(STATE_PROPS))
|
||||||
|
const nextKeys = getNextKeys(props, beforeKey, true)
|
||||||
|
const operations = replace(doc, selection.paths, values, nextKeys)
|
||||||
|
|
||||||
|
return operations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createNewValue (doc, selection, type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'value':
|
||||||
|
return ''
|
||||||
|
|
||||||
|
case 'object':
|
||||||
|
return {}
|
||||||
|
|
||||||
|
case 'array':
|
||||||
|
return []
|
||||||
|
|
||||||
|
case 'structure':
|
||||||
|
const parentPath = getParentPath(selection)
|
||||||
|
const parent = getIn(doc, parentPath)
|
||||||
|
|
||||||
|
if (Array.isArray(parent) && !isEmpty(parent)) {
|
||||||
|
const jsonExample = first(parent)
|
||||||
|
const structure = cloneDeepWith(jsonExample, (value) => {
|
||||||
|
return isObjectOrArray(value)
|
||||||
|
? undefined // leave as is
|
||||||
|
: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('structure', jsonExample, structure)
|
||||||
|
|
||||||
|
return structure
|
||||||
|
} else {
|
||||||
|
// no example structure
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a JSONPatch for a remove operation
|
* Create a JSONPatch for a remove operation
|
||||||
* @param {Path} path
|
* @param {Path} path
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { isEqual } from 'lodash-es'
|
import { first, initial, isEqual } from 'lodash-es'
|
||||||
import { STATE_PROPS } from '../constants.js'
|
import { STATE_PROPS } from '../constants.js'
|
||||||
import { getIn } from '../utils/immutabilityHelpers.js'
|
import { getIn } from '../utils/immutabilityHelpers.js'
|
||||||
import { compileJSONPointer, parseJSONPointer } from '../utils/jsonPointer.js'
|
import { compileJSONPointer, parseJSONPointer } from '../utils/jsonPointer.js'
|
||||||
|
@ -65,6 +65,26 @@ export function expandSelection (doc, state, anchorPath, focusPath) {
|
||||||
throw new Error('Failed to create selection')
|
throw new Error('Failed to create selection')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Selection} selection
|
||||||
|
* @return {Path} Returns parent path
|
||||||
|
*/
|
||||||
|
export function getParentPath (selection) {
|
||||||
|
if (selection.beforePath) {
|
||||||
|
return initial(selection.beforePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selection.appendPath) {
|
||||||
|
return selection.appendPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selection.paths) {
|
||||||
|
const firstPath = first(selection.paths)
|
||||||
|
return initial(firstPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {JSONPatchDocument} operations
|
* @param {JSONPatchDocument} operations
|
||||||
* @returns {MultiSelection}
|
* @returns {MultiSelection}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import assert from 'assert'
|
import assert from 'assert'
|
||||||
import { expandSelection } from './selection.js'
|
import { expandSelection, getParentPath } from './selection.js'
|
||||||
import { syncState } from './documentState.js'
|
import { syncState } from './documentState.js'
|
||||||
|
|
||||||
describe ('selection', () => {
|
describe ('selection', () => {
|
||||||
|
@ -65,4 +65,14 @@ describe ('selection', () => {
|
||||||
['obj', 'arr']
|
['obj', 'arr']
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should get parent path from a selection', () => {
|
||||||
|
assert.deepStrictEqual(getParentPath({ beforePath: ['a', 'b']}), ['a'])
|
||||||
|
assert.deepStrictEqual(getParentPath({ appendPath: ['a', 'b']}), ['a', 'b'])
|
||||||
|
assert.deepStrictEqual(getParentPath({ paths:[
|
||||||
|
['a', 'b'],
|
||||||
|
['a', 'c'],
|
||||||
|
['a', 'd']
|
||||||
|
]}), ['a'])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -80,5 +80,6 @@
|
||||||
* @typedef {Object} MenuDropdownItem
|
* @typedef {Object} MenuDropdownItem
|
||||||
* @property {string} text
|
* @property {string} text
|
||||||
* @property {function} onClick
|
* @property {function} onClick
|
||||||
|
* @property {string} [title=undefined]
|
||||||
* @property {boolean} [default=false]
|
* @property {boolean} [default=false]
|
||||||
**/
|
*/
|
||||||
|
|
Loading…
Reference in New Issue