diff --git a/list-editor/index.js b/list-editor/index.js index c2908e9..fdfd3c8 100644 --- a/list-editor/index.js +++ b/list-editor/index.js @@ -4,8 +4,8 @@ import he from 'he' import dragula from 'dragula' import textareaAutosizeInit from "./textarea.autosize" import createCursor from './cursor' -import createSelection from './selection' import Store from './store' +import Keymap from './keymap' import getCaretCoordinates from "../editor/src/caret-position"; textareaAutosizeInit($) @@ -28,10 +28,21 @@ function textareaCursorInfo(target, dir) { function editor(root, inputData, options) { let cursor = createCursor() - let selection = createSelection() let store = createStore(inputData); + let keymap = new Keymap() let drake = null; + keymap.mapKey('ArrowUp', 'backwardLine') + keymap.mapKey('ArrowDown', 'forwardLine') + keymap.mapKey('S-Delete', 'deleteBlock') + keymap.mapKey('C-Enter', 'insertLineAbove') + keymap.mapKey('Enter', 'insertLineBelow') + keymap.mapKey('Tab', 'indentBlock') + keymap.mapKey('S-Tab', 'indentBlock') + keymap.mapKey('Escape', 'leaveEditor') + keymap.mapKey('C-S-ArrowUp', 'blockMoveBackward') + keymap.mapKey('C-S-ArrowDown', 'blockMoveForward') + function createStore(inputData) { let data = [ {indented: 0, text: '', fold: 'open'}, @@ -131,7 +142,19 @@ function editor(root, inputData, options) { zoomin, treeForId, replaceChildren, - render: renderUpdate + render: renderUpdate, + + deleteBlock, + forwardLine, + backwardLine, + insertLineAbove, + insertLineBelow, + indentBlock, + + leaveEditor, + + blockMoveBackward, + blockMoveForward }; root.classList.add('root') @@ -259,11 +282,7 @@ function editor(root, inputData, options) { let $li = $(li) .attr('data-id', value.id) .toggleClass('selected', cursor.atPosition(index)) - .toggleClass('selection-first', selection.isSelectedFirst(index)) - .toggleClass('selection-last', selection.isSelectedLast(index)) - .toggleClass('selection', selection.isSelected(index)) .toggleClass('hidden', value.indented >= hideLevel) - .toggleClass('border', value.indented >= 1) .css('margin-left', (value.indented * 32) + 'px') value.hidden = value.indented >= hideLevel @@ -290,11 +309,7 @@ function editor(root, inputData, options) { let value = rootData.value(storeId) let $li = newItem(value) .css('margin-left', (value.indented * 32) + 'px') - .toggleClass('selection-first', selection.isSelectedFirst(index)) - .toggleClass('selection-last', selection.isSelectedLast(index)) - .toggleClass('selection', selection.isSelected(index)) .toggleClass('selected', cursor.atPosition(index + $enter.length)) - .toggleClass('border', value.indented >= 1) .toggleClass('hidden', value.indented >= hideLevel); value.hidden = value.indented >= hideLevel @@ -359,9 +374,7 @@ function editor(root, inputData, options) { let position = store.index(id); cursor.set(position) - selection.selectOne(position, store) - _.defer(renderUpdate) trigger('change') }) return drake; @@ -369,6 +382,7 @@ function editor(root, inputData, options) { function stopEditing(rootElement, store, element) { if (!editing) return + if (element === null) { editing = false currentEditor = null @@ -453,120 +467,150 @@ function editor(root, inputData, options) { })) store.insertAfter(currentID, ...newItems) - disableDragging(drake) - render(root, store); - drake = enableDragging(root) + trigger('change') + + // disableDragging(drake) + // render(root, store); + // drake = enableDragging(root) + return false }); $(root).on('keydown', '.input-line', function (event) { if (event.key === 'Escape') { stopEditing(root, store, $(this)) - selection.selectOne(cursor.get(), store) return false } return true }); - $(root).on('keydown', function (event) { + function moveCursor(event, dir) { let target = event.target; - let dir = 0; - if (event.key === 'ArrowUp') dir = -1 - if (event.key === 'ArrowDown') dir = 1 - let cursorInfo = null if (target.nodeName === 'TEXTAREA') cursorInfo = textareaCursorInfo(target, dir); - if (cursorInfo !== null && !cursorInfo.leaving && dir !== 0 && !event.ctrlKey) { + if (cursorInfo !== null && !cursorInfo.leaving && dir !== 0 && !event.ctrlKey) { // FIXME: don't check modifier here return true } - let next = true - let prevSelected = cursor.save() + if (dir < 0) { + cursor.moveUp(store) + } else if (dir > 0) { + cursor.moveDown(store) + } - if (event.key === 'ArrowUp') { - cursor.moveUp(store); - if (event.shiftKey) { - selection.include(cursor.get(), store) - } else { - selection.selectNothing(cursor.get()) - } - next = false - } else if (event.key === 'ArrowDown') { - cursor.moveDown(store); - if (event.shiftKey) { - selection.include(cursor.get(), store) - } else { - selection.selectNothing(cursor.get()) - } - next = false - } else if (event.shiftKey && event.key === 'Delete') { + if (editing) { stopEditing(root, store, currentEditor); - if (selection.hasSelection()) { - selection.remove(store) - // FIXME: adjust cursor - } else { - cursor.remove(store) - } - next = false - - trigger('change'); - } else if (event.key === 'Enter') { - stopEditing(root, store, currentEditor); - next = false - - if (event.ctrlKey) { - let id = store.currentID(cursor.get()) - let current = store.value(id) - let indent = current.indented - let item = newListItem(indent) - cursor.insertAbove(store, item) - } else { - let insertion = cursor.save() - let currentValue = store.value(store.currentID(cursor.get())); - let current = currentValue ? currentValue.indented : 0 - let next = cursor.get() + 1 < store.length() ? store.value(store.currentID(cursor.get() + 1)).indented : current - let indent = next > current ? next : current - let item = newListItem(indent) - if (currentValue.text.match(/^#\[\[TODO\]\]/)) { - item.text = '#[[TODO]] '; - } - cursor.insertBelow(store, item) - } - - selection.selectOne(cursor.get(), store) trigger('change') - } else if (event.key === 'Tab') { - store.indent(cursor.get(), store.lastHigherIndented(cursor.get()) - cursor.get(), event.shiftKey ? -1 : 1) - next = false - } else { - return true - } - - disableDragging(drake) - render(root, store); - drake = enableDragging(root) - - if (cursor.hasMoved(prevSelected)) { - if (!selection.hasSelection() && editing && (event.key === 'ArrowUp' || event.key === 'ArrowDown')) { - stopEditing(root, store, currentEditor); - startEditing(root, store, cursor); - return false - } else if (selection.hasSelection()) { - stopEditing(root, store, currentEditor); - } - cursor.resetLastMove() - } - - if (event.key === 'Enter') { startEditing(root, store, cursor); - return false - } else if (event.key === 'Escape') { - stopEditing(root, store, currentEditor); - return false + } else { + trigger('change') } - return next + + cursor.resetLastMove() + + return false + } + + function insertLine(event, dir) { + stopEditing(root, store, currentEditor); + + if (dir < 0) { + let id = store.currentID(cursor.get()) + let current = store.value(id) + let indent = current.indented + let item = newListItem(indent) + cursor.insertAbove(store, item) + } else if (dir > 0) { + let insertion = cursor.save() + let currentValue = store.value(store.currentID(cursor.get())); + let current = currentValue ? currentValue.indented : 0 + let next = cursor.get() + 1 < store.length() ? store.value(store.currentID(cursor.get() + 1)).indented : current + let indent = next > current ? next : current + let item = newListItem(indent) + if (currentValue.text.match(/^#\[\[TODO\]\]/)) { + item.text = '#[[TODO]] '; + } + cursor.insertBelow(store, item) + } + + trigger('change') + + startEditing(root, store, cursor); + + return false + } + + function insertLineAbove(event) { + return insertLine(event, -1) + } + + function insertLineBelow(event) { + return insertLine(event, 1) + } + + function backwardLine(event) { + return moveCursor(event, -1) + } + + function forwardLine(event) { + return moveCursor(event, 1) + } + + function deleteBlock(event) { + stopEditing(root, store, currentEditor); + cursor.remove(store) + trigger('change') + return false + } + + function indentBlock(event) { + store.indent(cursor.get(), store.lastHigherIndented(cursor.get()) - cursor.get(), event.shiftKey ? -1 : 1) + trigger('change') + return false + } + + function leaveEditor(event) { + stopEditing(root, store, currentEditor); + return false + } + + function blockMoveBackward(event) { + stopEditing(root, store, currentEditor); + + let item = cursor.getCurrent(store); + let before = store.firstSameIndented(cursor.get(), item.indented) + let beforeID = store.currentID(before); + let beforeItem = store.value(beforeID); + store.moveBefore(item.id, beforeID) + // NOTE: should moveBefore adjust the indents of the children + if (item.indented > beforeItem.indented) { + item.indented = beforeItem.indented + } + cursor.set(store.index(item.id)) + trigger('change') + + return false + } + + function blockMoveForward(event) { + stopEditing(root, store, currentEditor); + + let item = cursor.getCurrent(store); + let after = store.lastHigherIndented(cursor.get(), item.indented) + after = store.lastHigherIndented(after, item.indented) + store.moveBefore(item.id, store.currentID(after)) + + cursor.set(store.index(item.id)) + trigger('change') + + return false + } + + $(root).on('keydown', function (event) { + return keymap.handleKey(EDITOR, event) }) + $(root).on('click', '.marker', function () { stopEditing(root, store, $(this).next('textarea')); return false; @@ -587,11 +631,8 @@ function editor(root, inputData, options) { stopEditing(root, store, currentEditor) cursor.set(currentIndex) - selection.selectOne(cursor.get(), store) - disableDragging(drake) - render(root, store); - drake = enableDragging(root) + trigger('change') const $input = startEditing(root, store, cursor) $input.trigger('input') @@ -612,9 +653,7 @@ function editor(root, inputData, options) { return item }) - disableDragging(drake) - render(root, store); - drake = enableDragging(root) + trigger('change') }); function update(id, callback) { @@ -630,6 +669,8 @@ function editor(root, inputData, options) { } } + EDITOR.on('change', renderUpdate) + return EDITOR; } diff --git a/list-editor/keymap.js b/list-editor/keymap.js new file mode 100644 index 0000000..d545693 --- /dev/null +++ b/list-editor/keymap.js @@ -0,0 +1,51 @@ +function canonicalKey(event) { + let name = []; + + if (event.altKey) name.push('M') + if (event.ctrlKey) name.push('C') + if (event.metaKey) name.push('H') + if (event.shiftKey) name.push('S') + + name.push(event.key) + + return name.join('-') +} + +function handleKeyDown(editor, event) { + if (event.repeating) return true; + + const key = canonicalKey(event) + + console.log('Key down ' + key) + + if (this.keys.hasOwnProperty(key)) { + const action = this.keys[key] + if (editor.hasOwnProperty(action)) { + console.log('Calling action ' + action) + return editor[action](event) + } else { + console.warn('Unknown action on editor: ' + action) + } + } + + return true +} + +function mapKey(key, action) { + if (this.keys.hasOwnProperty(key)) { + console.warn(`Re-defining ${key} to call ${action}`) + } else { + console.log(`Defining ${key} to call ${action}`) + } + this.keys[key] = action +} + +function Keymap() { + return { + keys: {}, + mapKey, + handleKey: handleKeyDown + } +} + +export default Keymap diff --git a/list-editor/store.js b/list-editor/store.js index 4d4836c..aa84788 100644 --- a/list-editor/store.js +++ b/list-editor/store.js @@ -288,7 +288,7 @@ function Store(inputData) { */ function moveBefore(from, to) { let fromIndex = index(from) - let toIndex = to ? index(to) : idList.length + let toIndex = to && to !== 'at-end' ? index(to) : idList.length let n = lastHigherIndented(fromIndex) - fromIndex if (toIndex >= fromIndex && toIndex < fromIndex + n) return index(from) if (fromIndex < toIndex) {