Remove multiple selection and add keymaps
This commit is contained in:
parent
b4a9077c9c
commit
680e600fbe
|
@ -4,8 +4,8 @@ import he from 'he'
|
||||||
import dragula from 'dragula'
|
import dragula from 'dragula'
|
||||||
import textareaAutosizeInit from "./textarea.autosize"
|
import textareaAutosizeInit from "./textarea.autosize"
|
||||||
import createCursor from './cursor'
|
import createCursor from './cursor'
|
||||||
import createSelection from './selection'
|
|
||||||
import Store from './store'
|
import Store from './store'
|
||||||
|
import Keymap from './keymap'
|
||||||
import getCaretCoordinates from "../editor/src/caret-position";
|
import getCaretCoordinates from "../editor/src/caret-position";
|
||||||
|
|
||||||
textareaAutosizeInit($)
|
textareaAutosizeInit($)
|
||||||
|
@ -28,10 +28,21 @@ function textareaCursorInfo(target, dir) {
|
||||||
|
|
||||||
function editor(root, inputData, options) {
|
function editor(root, inputData, options) {
|
||||||
let cursor = createCursor()
|
let cursor = createCursor()
|
||||||
let selection = createSelection()
|
|
||||||
let store = createStore(inputData);
|
let store = createStore(inputData);
|
||||||
|
let keymap = new Keymap()
|
||||||
let drake = null;
|
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) {
|
function createStore(inputData) {
|
||||||
let data = [
|
let data = [
|
||||||
{indented: 0, text: '', fold: 'open'},
|
{indented: 0, text: '', fold: 'open'},
|
||||||
|
@ -131,7 +142,19 @@ function editor(root, inputData, options) {
|
||||||
zoomin,
|
zoomin,
|
||||||
treeForId,
|
treeForId,
|
||||||
replaceChildren,
|
replaceChildren,
|
||||||
render: renderUpdate
|
render: renderUpdate,
|
||||||
|
|
||||||
|
deleteBlock,
|
||||||
|
forwardLine,
|
||||||
|
backwardLine,
|
||||||
|
insertLineAbove,
|
||||||
|
insertLineBelow,
|
||||||
|
indentBlock,
|
||||||
|
|
||||||
|
leaveEditor,
|
||||||
|
|
||||||
|
blockMoveBackward,
|
||||||
|
blockMoveForward
|
||||||
};
|
};
|
||||||
|
|
||||||
root.classList.add('root')
|
root.classList.add('root')
|
||||||
|
@ -259,11 +282,7 @@ function editor(root, inputData, options) {
|
||||||
let $li = $(li)
|
let $li = $(li)
|
||||||
.attr('data-id', value.id)
|
.attr('data-id', value.id)
|
||||||
.toggleClass('selected', cursor.atPosition(index))
|
.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('hidden', value.indented >= hideLevel)
|
||||||
.toggleClass('border', value.indented >= 1)
|
|
||||||
.css('margin-left', (value.indented * 32) + 'px')
|
.css('margin-left', (value.indented * 32) + 'px')
|
||||||
|
|
||||||
value.hidden = value.indented >= hideLevel
|
value.hidden = value.indented >= hideLevel
|
||||||
|
@ -290,11 +309,7 @@ function editor(root, inputData, options) {
|
||||||
let value = rootData.value(storeId)
|
let value = rootData.value(storeId)
|
||||||
let $li = newItem(value)
|
let $li = newItem(value)
|
||||||
.css('margin-left', (value.indented * 32) + 'px')
|
.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('selected', cursor.atPosition(index + $enter.length))
|
||||||
.toggleClass('border', value.indented >= 1)
|
|
||||||
.toggleClass('hidden', value.indented >= hideLevel);
|
.toggleClass('hidden', value.indented >= hideLevel);
|
||||||
value.hidden = value.indented >= hideLevel
|
value.hidden = value.indented >= hideLevel
|
||||||
|
|
||||||
|
@ -359,9 +374,7 @@ function editor(root, inputData, options) {
|
||||||
|
|
||||||
let position = store.index(id);
|
let position = store.index(id);
|
||||||
cursor.set(position)
|
cursor.set(position)
|
||||||
selection.selectOne(position, store)
|
|
||||||
|
|
||||||
_.defer(renderUpdate)
|
|
||||||
trigger('change')
|
trigger('change')
|
||||||
})
|
})
|
||||||
return drake;
|
return drake;
|
||||||
|
@ -369,6 +382,7 @@ function editor(root, inputData, options) {
|
||||||
|
|
||||||
function stopEditing(rootElement, store, element) {
|
function stopEditing(rootElement, store, element) {
|
||||||
if (!editing) return
|
if (!editing) return
|
||||||
|
|
||||||
if (element === null) {
|
if (element === null) {
|
||||||
editing = false
|
editing = false
|
||||||
currentEditor = null
|
currentEditor = null
|
||||||
|
@ -453,120 +467,150 @@ function editor(root, inputData, options) {
|
||||||
}))
|
}))
|
||||||
store.insertAfter(currentID, ...newItems)
|
store.insertAfter(currentID, ...newItems)
|
||||||
|
|
||||||
disableDragging(drake)
|
trigger('change')
|
||||||
render(root, store);
|
|
||||||
drake = enableDragging(root)
|
// disableDragging(drake)
|
||||||
|
// render(root, store);
|
||||||
|
// drake = enableDragging(root)
|
||||||
|
|
||||||
return false
|
return false
|
||||||
});
|
});
|
||||||
|
|
||||||
$(root).on('keydown', '.input-line', function (event) {
|
$(root).on('keydown', '.input-line', function (event) {
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
stopEditing(root, store, $(this))
|
stopEditing(root, store, $(this))
|
||||||
selection.selectOne(cursor.get(), store)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
});
|
});
|
||||||
|
|
||||||
$(root).on('keydown', function (event) {
|
function moveCursor(event, dir) {
|
||||||
let target = event.target;
|
let target = event.target;
|
||||||
|
|
||||||
let dir = 0;
|
|
||||||
if (event.key === 'ArrowUp') dir = -1
|
|
||||||
if (event.key === 'ArrowDown') dir = 1
|
|
||||||
|
|
||||||
let cursorInfo = null
|
let cursorInfo = null
|
||||||
if (target.nodeName === 'TEXTAREA') cursorInfo = textareaCursorInfo(target, dir);
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
let next = true
|
if (dir < 0) {
|
||||||
let prevSelected = cursor.save()
|
cursor.moveUp(store)
|
||||||
|
} else if (dir > 0) {
|
||||||
|
cursor.moveDown(store)
|
||||||
|
}
|
||||||
|
|
||||||
if (event.key === 'ArrowUp') {
|
if (editing) {
|
||||||
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') {
|
|
||||||
stopEditing(root, store, currentEditor);
|
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')
|
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);
|
startEditing(root, store, cursor);
|
||||||
return false
|
} else {
|
||||||
} else if (event.key === 'Escape') {
|
trigger('change')
|
||||||
stopEditing(root, store, currentEditor);
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
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 () {
|
$(root).on('click', '.marker', function () {
|
||||||
stopEditing(root, store, $(this).next('textarea'));
|
stopEditing(root, store, $(this).next('textarea'));
|
||||||
return false;
|
return false;
|
||||||
|
@ -587,11 +631,8 @@ function editor(root, inputData, options) {
|
||||||
stopEditing(root, store, currentEditor)
|
stopEditing(root, store, currentEditor)
|
||||||
|
|
||||||
cursor.set(currentIndex)
|
cursor.set(currentIndex)
|
||||||
selection.selectOne(cursor.get(), store)
|
|
||||||
|
|
||||||
disableDragging(drake)
|
trigger('change')
|
||||||
render(root, store);
|
|
||||||
drake = enableDragging(root)
|
|
||||||
|
|
||||||
const $input = startEditing(root, store, cursor)
|
const $input = startEditing(root, store, cursor)
|
||||||
$input.trigger('input')
|
$input.trigger('input')
|
||||||
|
@ -612,9 +653,7 @@ function editor(root, inputData, options) {
|
||||||
return item
|
return item
|
||||||
})
|
})
|
||||||
|
|
||||||
disableDragging(drake)
|
trigger('change')
|
||||||
render(root, store);
|
|
||||||
drake = enableDragging(root)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function update(id, callback) {
|
function update(id, callback) {
|
||||||
|
@ -630,6 +669,8 @@ function editor(root, inputData, options) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EDITOR.on('change', renderUpdate)
|
||||||
|
|
||||||
return EDITOR;
|
return EDITOR;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
51
list-editor/keymap.js
Normal file
51
list-editor/keymap.js
Normal file
|
@ -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
|
|
@ -288,7 +288,7 @@ function Store(inputData) {
|
||||||
*/
|
*/
|
||||||
function moveBefore(from, to) {
|
function moveBefore(from, to) {
|
||||||
let fromIndex = index(from)
|
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
|
let n = lastHigherIndented(fromIndex) - fromIndex
|
||||||
if (toIndex >= fromIndex && toIndex < fromIndex + n) return index(from)
|
if (toIndex >= fromIndex && toIndex < fromIndex + n) return index(from)
|
||||||
if (fromIndex < toIndex) {
|
if (fromIndex < toIndex) {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user