Implement SearchBox (WIP)
This commit is contained in:
parent
b66a1cf0fa
commit
9c8febb1ff
|
@ -32,6 +32,7 @@
|
|||
#testEditor {
|
||||
width: 800px;
|
||||
height: 500px;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
|
|
@ -39,6 +39,14 @@
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
.separator {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
box-sizing: border-box;
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -60,6 +68,13 @@
|
|||
.contents {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
|
||||
.search {
|
||||
position: absolute;
|
||||
top: $search-box-offset;
|
||||
right: $search-box-offset;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
height: $input-padding;
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
<script>
|
||||
import SearchBox from './SearchBox.svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { faSearch, faUndo, faRedo } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faSearch, faUndo, faRedo } from '@fortawesome/free-solid-svg-icons'
|
||||
import { createHistory } from './history.js'
|
||||
import Node from './JSONNode.svelte'
|
||||
import { search } from './search'
|
||||
import { keyComboFromEvent } from './utils/keyBindings.js'
|
||||
import { search } from './utils/search.js'
|
||||
import { immutableJSONPatch } from './utils/immutableJSONPatch'
|
||||
|
||||
export let json = {}
|
||||
export let onChangeJson = () => {}
|
||||
export let searchText = ''
|
||||
export let onChangeJson = () => {
|
||||
}
|
||||
|
||||
let showSearch = true // FIXME: change to false
|
||||
let searchText = ''
|
||||
|
||||
const history = createHistory({
|
||||
onChange: (state) => {
|
||||
|
@ -26,7 +31,7 @@
|
|||
history.clear()
|
||||
}
|
||||
|
||||
export function patch (operations) {
|
||||
export function patch(operations) {
|
||||
console.log('patch', operations)
|
||||
|
||||
const patchResult = immutableJSONPatch(json, operations)
|
||||
|
@ -46,11 +51,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
function getPath () {
|
||||
function getPath() {
|
||||
return []
|
||||
}
|
||||
|
||||
function doSearch (json, searchText) {
|
||||
function doSearch(json, searchText) {
|
||||
console.time('search')
|
||||
const result = search(null, json, searchText)
|
||||
console.timeEnd('search')
|
||||
|
@ -59,12 +64,12 @@
|
|||
|
||||
$: searchResult = searchText ? doSearch(json, searchText) : undefined
|
||||
|
||||
function handleChangeKey (key, oldKey) {
|
||||
function handleChangeKey(key, oldKey) {
|
||||
// console.log('handleChangeKey', { key, oldKey })
|
||||
// TODO: this should not happen?
|
||||
}
|
||||
|
||||
function emitOnChange () {
|
||||
function emitOnChange() {
|
||||
// TODO: add more logic here to emit onChange, onChangeJson, onChangeText, etc.
|
||||
onChangeJson(json)
|
||||
}
|
||||
|
@ -72,7 +77,7 @@
|
|||
/**
|
||||
* @param {JSONPatchDocument} operations
|
||||
*/
|
||||
function handlePatch (operations) {
|
||||
function handlePatch(operations) {
|
||||
// console.log('handlePatch', operations)
|
||||
|
||||
patch(operations)
|
||||
|
@ -80,7 +85,11 @@
|
|||
emitOnChange()
|
||||
}
|
||||
|
||||
function handleUndo () {
|
||||
function handleToggleSearch() {
|
||||
showSearch = !showSearch
|
||||
}
|
||||
|
||||
function handleUndo() {
|
||||
if (history.getState().canUndo) {
|
||||
const item = history.undo()
|
||||
if (item) {
|
||||
|
@ -90,7 +99,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
function handleRedo () {
|
||||
function handleRedo() {
|
||||
if (history.getState().canRedo) {
|
||||
const item = history.redo()
|
||||
if (item) {
|
||||
|
@ -100,20 +109,75 @@
|
|||
}
|
||||
}
|
||||
|
||||
function handleKeyDown (event) {
|
||||
const combo = keyComboFromEvent(event)
|
||||
|
||||
if (combo === 'Ctrl+F' || combo === 'Command+F') {
|
||||
event.preventDefault()
|
||||
showSearch = true
|
||||
}
|
||||
|
||||
if (combo === 'Ctrl+Z' || combo === 'Command+Z') {
|
||||
event.preventDefault()
|
||||
|
||||
// TODO: find a better way to restore focus
|
||||
const activeElement = document.activeElement
|
||||
if (activeElement && activeElement.blur && activeElement.focus) {
|
||||
activeElement.blur()
|
||||
setTimeout(() => {
|
||||
handleUndo()
|
||||
setTimeout(() => activeElement.focus())
|
||||
})
|
||||
} else {
|
||||
handleUndo()
|
||||
}
|
||||
}
|
||||
|
||||
if (combo === 'Ctrl+Shift+Z' || combo === 'Command+Shift+Z') {
|
||||
event.preventDefault()
|
||||
|
||||
// TODO: find a better way to restore focus
|
||||
const activeElement = document.activeElement
|
||||
if (activeElement && activeElement.blur && activeElement.focus) {
|
||||
activeElement.blur()
|
||||
setTimeout(() => {
|
||||
handleRedo()
|
||||
setTimeout(() => activeElement.focus())
|
||||
})
|
||||
} else {
|
||||
handleRedo()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="jsoneditor">
|
||||
<div class="jsoneditor" on:keydown={handleKeyDown}>
|
||||
<div class="menu">
|
||||
<button class="button undo" disabled={!historyState.canUndo} on:click={handleUndo}>
|
||||
<button
|
||||
class="button search"
|
||||
on:click={handleToggleSearch}
|
||||
title="Search (Ctrl+F)"
|
||||
>
|
||||
<Icon data={faSearch} />
|
||||
</button>
|
||||
<div class="separator"></div>
|
||||
<button
|
||||
class="button undo"
|
||||
disabled={!historyState.canUndo}
|
||||
on:click={handleUndo}
|
||||
title="Undo (Ctrl+Z)"
|
||||
>
|
||||
<Icon data={faUndo} />
|
||||
</button>
|
||||
<button class="button redo" disabled={!historyState.canRedo} on:click={handleRedo}>
|
||||
<button
|
||||
class="button redo"
|
||||
disabled={!historyState.canRedo}
|
||||
on:click={handleRedo}
|
||||
title="Redo (Ctrl+Shift+Z)"
|
||||
>
|
||||
<Icon data={faRedo} />
|
||||
</button>
|
||||
<div class="space"></div>
|
||||
<div class="search-box">
|
||||
<span class="search-icon"><Icon data={faSearch} /></span> Search: <input class="search-input" bind:value={searchText} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="contents">
|
||||
<Node
|
||||
|
@ -125,6 +189,15 @@
|
|||
getParentPath={getPath}
|
||||
/>
|
||||
<div class='bottom'></div>
|
||||
{#if showSearch}
|
||||
<div class="search">
|
||||
<SearchBox
|
||||
text={searchText}
|
||||
onChange={text => searchText = text}
|
||||
onClose={() => showSearch = false}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { getPlainText, setPlainText } from './utils/domUtils.js'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons'
|
||||
import { SEARCH_PROPERTY, SEARCH_VALUE } from './search'
|
||||
import { SEARCH_PROPERTY, SEARCH_VALUE } from './utils/search.js'
|
||||
import classnames from 'classnames'
|
||||
import debounce from 'lodash/debounce'
|
||||
import { findUniqueName } from './utils/stringUtils.js'
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
@import './styles.scss';
|
||||
|
||||
$search-size: 24px;
|
||||
|
||||
.search-box {
|
||||
border: 2px solid $theme-color;
|
||||
border-radius: $border-radius;
|
||||
background: $white;
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 5px;
|
||||
|
||||
.search-icon {
|
||||
color: $light-gray;
|
||||
display: block;
|
||||
width: $search-size;
|
||||
height: $search-size;
|
||||
text-align: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button.search-icon {
|
||||
width: 20px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
color: $gray;
|
||||
}
|
||||
}
|
||||
|
||||
.search-input {
|
||||
border: none;
|
||||
height: $search-size;
|
||||
padding: 0 $input-padding;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.search-count {
|
||||
color: $light-gray;
|
||||
font-size: $font-size-small;
|
||||
visibility: hidden;
|
||||
padding: 0 $input-padding;
|
||||
|
||||
&.visible {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
<script>
|
||||
import { debounce } from 'lodash'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { faSearch, faChevronDown, faChevronUp, faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||
import { keyComboFromEvent } from './utils/keyBindings.js'
|
||||
|
||||
const DEBOUNCE_DELAY = 300 // milliseconds TODO: make the debounce delay configurable?
|
||||
|
||||
export let text = ''
|
||||
export let onChange = () => {}
|
||||
export let onClose = () => {}
|
||||
|
||||
let activeResultIndex = 0
|
||||
let resultCount = 0
|
||||
|
||||
$: onChangeDebounced = debounce(onChange, DEBOUNCE_DELAY)
|
||||
|
||||
function handleSubmit (event) {
|
||||
event.preventDefault()
|
||||
|
||||
onChangeDebounced.cancel()
|
||||
onChange(text)
|
||||
}
|
||||
|
||||
function handleInput (event) {
|
||||
text = event.target.value
|
||||
|
||||
onChangeDebounced(text)
|
||||
// TODO: fire debounced onChange
|
||||
}
|
||||
|
||||
function handleClose () {
|
||||
onChange('')
|
||||
onClose()
|
||||
}
|
||||
|
||||
function handleKeyDown (event) {
|
||||
const combo = keyComboFromEvent(event)
|
||||
if (combo === 'Ctrl+Enter' || combo === 'Command+Enter') {
|
||||
event.preventDefault()
|
||||
// this.props.onFocusActive() // FIXME
|
||||
}
|
||||
|
||||
if (combo === 'Escape') {
|
||||
event.preventDefault()
|
||||
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
|
||||
function initSearchInput (element) {
|
||||
element.select()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="search-box">
|
||||
<form class="search-form" on:submit={handleSubmit} on:keydown={handleKeyDown}>
|
||||
<button class="search-icon">
|
||||
<Icon data={faSearch} />
|
||||
</button>
|
||||
<label about="search input">
|
||||
<input
|
||||
class="search-input"
|
||||
type="text"
|
||||
value={text}
|
||||
on:input={handleInput}
|
||||
use:initSearchInput
|
||||
/>
|
||||
</label>
|
||||
<div class="search-count" class:visible={text !== ''}>
|
||||
{activeResultIndex}/{resultCount}
|
||||
</div>
|
||||
<button class="search-icon search-next">
|
||||
<Icon data={faChevronDown} />
|
||||
</button>
|
||||
<button class="search-icon search-previous">
|
||||
<Icon data={faChevronUp} />
|
||||
</button>
|
||||
<button class="search-icon search-clear" on:click={handleClose}>
|
||||
<Icon data={faTimes} />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<style src="SearchBox.scss"></style>
|
|
@ -27,5 +27,7 @@ $light-gray: #c0c0c0;
|
|||
$line-height: 18px;
|
||||
$indentation-width: 18px;
|
||||
$input-padding: 5px;
|
||||
$search-box-offset: 10px;
|
||||
$border-radius: 3px;
|
||||
|
||||
$menu-padding: 5px;
|
||||
|
|
|
@ -0,0 +1,215 @@
|
|||
// inspiration: https://github.com/andrepolischuk/keycomb
|
||||
|
||||
// TODO: write unit tests for keyBindings
|
||||
|
||||
// FIXME: implement an escape sequence for the separator +
|
||||
|
||||
/**
|
||||
* Get a named key from a key code.
|
||||
* For example:
|
||||
* keyFromCode(65) returns 'A'
|
||||
* keyFromCode(13) returns 'Enter'
|
||||
* @param {string} code
|
||||
* @return {string}
|
||||
*/
|
||||
export function nameFromKeyCode(code) {
|
||||
return codes[code] || ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the active key combination from a keyboard event.
|
||||
* For example returns "Ctrl+Shift+Up" or "Ctrl+A"
|
||||
* @param {KeyboardEvent} event
|
||||
* @return {string}
|
||||
*/
|
||||
export function keyComboFromEvent (event) {
|
||||
let combi = []
|
||||
|
||||
if (event.ctrlKey) { combi.push('Ctrl') }
|
||||
if (event.metaKey) { combi.push('Command') }
|
||||
if (event.altKey) { combi.push(isMac ? 'Option' : 'Alt') }
|
||||
if (event.shiftKey) { combi.push('Shift') }
|
||||
|
||||
const keyName = nameFromKeyCode(event.which)
|
||||
if (!metaCodes[keyName]) { // prevent output like 'Ctrl+Ctrl'
|
||||
combi.push(keyName)
|
||||
}
|
||||
|
||||
return combi.join('+')
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a function which can quickly find a keyBinding from a set of
|
||||
* keyBindings.
|
||||
* @param {Object.<String, String[]>} keyBindings
|
||||
* @return {function} Returns a findKeyBinding function
|
||||
*/
|
||||
export function createFindKeyBinding (keyBindings) {
|
||||
// turn the map with key bindings by name (multiple per binding) into a map by key combo
|
||||
const keyCombos = {}
|
||||
Object.keys(keyBindings).forEach ((name) => {
|
||||
keyBindings[name].forEach(combo => keyCombos[normalizeKeyCombo(combo)] = name)
|
||||
})
|
||||
|
||||
return function findKeyBinding (event) {
|
||||
const keyCombo = keyComboFromEvent(event)
|
||||
|
||||
return keyCombos[normalizeKeyCombo(keyCombo)] || null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a key combo:
|
||||
*
|
||||
* - to upper case
|
||||
* - replace aliases like "?" with "/"
|
||||
*
|
||||
* @param {string} combo
|
||||
* @return {string}
|
||||
*/
|
||||
function normalizeKeyCombo (combo) {
|
||||
const upper = combo.toUpperCase()
|
||||
|
||||
const last = upper[upper.length - 1]
|
||||
if (last in aliases) {
|
||||
return upper.substring(0, upper.length - 1) + aliases[last]
|
||||
}
|
||||
|
||||
return upper
|
||||
}
|
||||
|
||||
const isMac = window.navigator.platform.toUpperCase().indexOf('MAC') >= 0
|
||||
|
||||
const metaCodes = {
|
||||
'Ctrl': true,
|
||||
'Command': true,
|
||||
'Alt': true,
|
||||
'Option': true,
|
||||
'Shift': true
|
||||
}
|
||||
|
||||
const codes = {
|
||||
'8': 'Backspace',
|
||||
'9': 'Tab',
|
||||
'13': 'Enter',
|
||||
'16': 'Shift',
|
||||
'17': 'Ctrl',
|
||||
'18': 'Alt',
|
||||
'19': 'Pause_Break',
|
||||
'20': 'Caps_Lock',
|
||||
'27': 'Escape',
|
||||
'33': 'Page_Up',
|
||||
'34': 'Page_Down',
|
||||
'35': 'End',
|
||||
'36': 'Home',
|
||||
'37': 'Left',
|
||||
'38': 'Up',
|
||||
'39': 'Right',
|
||||
'40': 'Down',
|
||||
'45': 'Insert',
|
||||
'46': 'Delete',
|
||||
'48': '0',
|
||||
'49': '1',
|
||||
'50': '2',
|
||||
'51': '3',
|
||||
'52': '4',
|
||||
'53': '5',
|
||||
'54': '6',
|
||||
'55': '7',
|
||||
'56': '8',
|
||||
'57': '9',
|
||||
'65': 'A',
|
||||
'66': 'B',
|
||||
'67': 'C',
|
||||
'68': 'D',
|
||||
'69': 'E',
|
||||
'70': 'F',
|
||||
'71': 'G',
|
||||
'72': 'H',
|
||||
'73': 'I',
|
||||
'74': 'J',
|
||||
'75': 'K',
|
||||
'76': 'L',
|
||||
'77': 'M',
|
||||
'78': 'N',
|
||||
'79': 'O',
|
||||
'80': 'P',
|
||||
'81': 'Q',
|
||||
'82': 'R',
|
||||
'83': 'S',
|
||||
'84': 'T',
|
||||
'85': 'U',
|
||||
'86': 'V',
|
||||
'87': 'W',
|
||||
'88': 'X',
|
||||
'89': 'Y',
|
||||
'90': 'Z',
|
||||
'91': 'Left_Window_Key',
|
||||
'92': 'Right_Window_Key',
|
||||
'93': 'Select_Key',
|
||||
'96': 'Numpad_0',
|
||||
'97': 'Numpad_1',
|
||||
'98': 'Numpad_2',
|
||||
'99': 'Numpad_3',
|
||||
'100': 'Numpad_4',
|
||||
'101': 'Numpad_5',
|
||||
'102': 'Numpad_6',
|
||||
'103': 'Numpad_7',
|
||||
'104': 'Numpad_8',
|
||||
'105': 'Numpad_9',
|
||||
'106': 'Numpad_*',
|
||||
'107': 'Numpad_+',
|
||||
'109': 'Numpad_-',
|
||||
'110': 'Numpad_.',
|
||||
'111': 'Numpad_/',
|
||||
'112': 'F1',
|
||||
'113': 'F2',
|
||||
'114': 'F3',
|
||||
'115': 'F4',
|
||||
'116': 'F5',
|
||||
'117': 'F6',
|
||||
'118': 'F7',
|
||||
'119': 'F8',
|
||||
'120': 'F9',
|
||||
'121': 'F10',
|
||||
'122': 'F11',
|
||||
'123': 'F12',
|
||||
'144': 'Num_Lock',
|
||||
'145': 'Scroll_Lock',
|
||||
'186': ';',
|
||||
'187': '=',
|
||||
'188': ',',
|
||||
'189': '-',
|
||||
'190': '.',
|
||||
'191': '/',
|
||||
'192': '`',
|
||||
'219': '[',
|
||||
'220': '\\',
|
||||
'221': ']',
|
||||
'222': '\''
|
||||
}
|
||||
|
||||
// all secondary characters of the keyboard buttons (used via Shift)
|
||||
const aliases = {
|
||||
'~': '`',
|
||||
'!': '1',
|
||||
'@': '2',
|
||||
'#': '3',
|
||||
'$': '4',
|
||||
'%': '5',
|
||||
'^': '6',
|
||||
'&': '7',
|
||||
'*': '8',
|
||||
'(': '9',
|
||||
')': '0',
|
||||
'_': '-',
|
||||
'+': '=',
|
||||
'{': '[',
|
||||
'}': ']',
|
||||
'|': '\\',
|
||||
':': ';',
|
||||
'"': '',
|
||||
'<': ',',
|
||||
'>': '.',
|
||||
'?': '/'
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { valueType } from './utils/typeUtils'
|
||||
import { valueType } from './typeUtils.js'
|
||||
|
||||
export const SEARCH_PROPERTY = '$jse:search:property'
|
||||
export const SEARCH_VALUE = '$jse:search:value'
|
||||
|
@ -38,11 +38,6 @@ function createOrAdd(object, key, value) {
|
|||
if (object) {
|
||||
object[key] = value
|
||||
return object
|
||||
|
||||
// return {
|
||||
// ...object,
|
||||
// [key]: value
|
||||
// }
|
||||
} else {
|
||||
return {
|
||||
[key]: value
|
Loading…
Reference in New Issue