Peter Stuifzand
c30156dd10
All checks were successful
continuous-integration/drone/push Build is passing
Solution: allow one line text as normals text
852 lines
24 KiB
JavaScript
852 lines
24 KiB
JavaScript
import _ from 'lodash'
|
|
import $ from 'jquery'
|
|
import he from 'he'
|
|
import dragula from 'dragula'
|
|
import textareaAutosizeInit from "./textarea.autosize"
|
|
import createCursor from './cursor'
|
|
import Store from './store'
|
|
import Keymap from './keymap'
|
|
import getCaretCoordinates from "../editor/src/caret-position";
|
|
|
|
textareaAutosizeInit($)
|
|
|
|
function textareaCursorInfo(target, dir) {
|
|
const minLine = 0;
|
|
let pos = getCaretCoordinates(target, target.selectionEnd, {})
|
|
let line = (pos.top - 3) / pos.height;
|
|
let maxLine = Math.round(target.clientHeight / pos.height)
|
|
let nextLine = line
|
|
nextLine += dir
|
|
return {
|
|
leaving: !(nextLine >= minLine && nextLine < maxLine),
|
|
min: minLine,
|
|
max: maxLine,
|
|
current: line,
|
|
next: nextLine
|
|
};
|
|
}
|
|
|
|
function editor(root, inputData, options) {
|
|
let cursor = createCursor()
|
|
let store = createStore(inputData);
|
|
let drake = null;
|
|
|
|
let editorKeymap = new Keymap()
|
|
editorKeymap.mapKey('ArrowUp', 'backwardLine')
|
|
editorKeymap.mapKey('ArrowDown', 'forwardLine')
|
|
editorKeymap.mapKey('S-Delete', 'deleteBlock')
|
|
editorKeymap.mapKey('C-Enter', 'insertLineAbove')
|
|
editorKeymap.mapKey('Enter', 'insertLineBelow')
|
|
editorKeymap.mapKey('Tab', 'indentBlock')
|
|
editorKeymap.mapKey('S-Tab', 'indentBlock')
|
|
editorKeymap.mapKey('Escape', 'leaveEditor')
|
|
editorKeymap.mapKey('C-S-ArrowUp', 'blockMoveBackward')
|
|
editorKeymap.mapKey('C-S-ArrowDown', 'blockMoveForward')
|
|
editorKeymap.mapKey('C-.', 'toggleBlock')
|
|
editorKeymap.mapKey('Backspace', 'deleteCharacterBackward')
|
|
// keymap.mapKey('C-]', 'zoomIn')
|
|
// keymap.mapKey('C-[', 'zoomOut')
|
|
|
|
let normalKeymap = new Keymap()
|
|
normalKeymap.mapKey('k', 'backwardLine')
|
|
normalKeymap.mapKey('j', 'forwardLine')
|
|
normalKeymap.mapKey('S-O', 'insertLineAbove')
|
|
normalKeymap.mapKey('o', 'insertLineBelow')
|
|
normalKeymap.mapKey('d', 'deleteBlock')
|
|
normalKeymap.mapKey('i', 'enterEditor')
|
|
normalKeymap.mapKey('S-I', 'enterEditor')
|
|
normalKeymap.mapKey('S-A', 'enterEditorAppend')
|
|
normalKeymap.mapKey('Tab', 'indentBlock')
|
|
normalKeymap.mapKey('S-Tab', 'indentBlock')
|
|
normalKeymap.mapKey('C-.', 'toggleBlock')
|
|
normalKeymap.mapKey('C-;', 'toggleTodo')
|
|
|
|
function createStore(inputData) {
|
|
let data = [
|
|
{indented: 0, text: '', fold: 'open'},
|
|
];
|
|
if (inputData.length) {
|
|
data = inputData
|
|
}
|
|
return Store(data);
|
|
}
|
|
|
|
function save() {
|
|
return new Promise(function (resolve, reject) {
|
|
|
|
if (store.hasChanged()) {
|
|
let result = store.debug().result
|
|
resolve(result)
|
|
store.clearChanged()
|
|
}
|
|
});
|
|
}
|
|
|
|
function saveTree(from, opt) {
|
|
opt = _.merge({force: false}, opt)
|
|
return new Promise(function (resolve, reject) {
|
|
if (opt.force || store.hasChanged()) {
|
|
resolve(store.tree(from))
|
|
store.clearChanged()
|
|
}
|
|
});
|
|
}
|
|
|
|
function treeForId(id) {
|
|
return new Promise(function (resolve, reject) {
|
|
resolve(store.tree(id))
|
|
})
|
|
}
|
|
|
|
function copy(element, opt) {
|
|
let item = $(element).parents('.list-item')
|
|
let id = item.attr('data-id')
|
|
|
|
if (opt.recursive) {
|
|
return saveTree(id, {force: true})
|
|
}
|
|
|
|
return new Promise(function (resolve, reject) {
|
|
resolve(store.value(id));
|
|
});
|
|
}
|
|
|
|
function flat(element, opt) {
|
|
opt = opt || {}
|
|
let item = $(element).parents('.list-item')
|
|
let id = item.attr('data-id')
|
|
|
|
return new Promise(function (resolve, reject) {
|
|
resolve(store.flat(id, opt));
|
|
});
|
|
}
|
|
|
|
function zoomin(element, opt) {
|
|
let item = $(element).parents('.list-item')
|
|
let id = item.attr('data-id')
|
|
return new Promise(function (resolve, reject) {
|
|
resolve(id);
|
|
});
|
|
}
|
|
|
|
function on(evt, handler) {
|
|
events[evt].push(handler)
|
|
}
|
|
|
|
function trigger(event) {
|
|
let args = [...arguments]
|
|
args.splice(0, 1)
|
|
_.each(events[event], function (handler) {
|
|
handler(...args)
|
|
})
|
|
}
|
|
|
|
function start() {
|
|
root.focus();
|
|
$(root).on('click', '.marker', function (event) {
|
|
if (event.ctrlKey) {
|
|
zoomin(this).then(id => {
|
|
location.href = '/edit/' + id;
|
|
})
|
|
}
|
|
return true;
|
|
})
|
|
cursor.set(0)
|
|
renderUpdate()
|
|
}
|
|
|
|
function getChildren(id) {
|
|
return store.children(id)
|
|
}
|
|
|
|
function replaceChildren(id, children) {
|
|
store.replaceChildren(id, children)
|
|
}
|
|
|
|
function renderUpdate() {
|
|
disableDragging(drake)
|
|
render(root, store);
|
|
drake = enableDragging(root)
|
|
}
|
|
|
|
let EDITOR = {
|
|
normalKeymap,
|
|
editorKeymap,
|
|
|
|
on,
|
|
save,
|
|
saveTree,
|
|
copy,
|
|
flat,
|
|
update,
|
|
start,
|
|
zoomin,
|
|
treeForId,
|
|
getChildren,
|
|
replaceChildren,
|
|
render: renderUpdate,
|
|
|
|
deleteBlock,
|
|
forwardLine,
|
|
backwardLine,
|
|
insertLineAbove,
|
|
insertLineBelow,
|
|
indentBlock,
|
|
|
|
leaveEditor,
|
|
enterEditor,
|
|
enterEditorAppend,
|
|
|
|
blockMoveBackward,
|
|
blockMoveForward,
|
|
|
|
deleteCharacterBackward,
|
|
|
|
expandBlock,
|
|
collapseBlock,
|
|
toggleBlock,
|
|
|
|
toggleTodo
|
|
};
|
|
|
|
root.classList.add('root')
|
|
root.setAttribute('tabindex', '-1')
|
|
|
|
let defaults = {
|
|
transform(text, element) {
|
|
element.html(he.encode(text))
|
|
}
|
|
}
|
|
|
|
options = _.merge(defaults, options)
|
|
|
|
|
|
let events = {
|
|
change: [],
|
|
'start-editing': [],
|
|
'stop-editing': [],
|
|
'rendering': [],
|
|
'rendered': []
|
|
}
|
|
|
|
let editing = false
|
|
let currentEditor = null;
|
|
|
|
function newListItem(indented) {
|
|
return {indented: indented, text: '', fold: 'open', hidden: false}
|
|
}
|
|
|
|
function newItem(value) {
|
|
let el = document.createElement('div')
|
|
el.classList.add('list-item')
|
|
el.setAttribute('data-id', value.id)
|
|
el.style.marginLeft = (value.indented * 32) + 'px'
|
|
|
|
let $el = $(el).data('indented', value.indented)
|
|
|
|
let line = document.createElement('div')
|
|
line.classList.add('line')
|
|
|
|
let content = document.createElement('div')
|
|
content.classList.add('content')
|
|
|
|
line.prepend(content)
|
|
|
|
options.transform(value.text, $(content), value.id, EDITOR)
|
|
|
|
let marker = document.createElement('span')
|
|
marker.classList.add('marker')
|
|
|
|
let fold = document.createElement('span')
|
|
fold.classList.add('fold')
|
|
fold.innerHTML = '▶'
|
|
|
|
line.prepend(marker)
|
|
line.prepend(fold)
|
|
|
|
el.prepend(line)
|
|
|
|
return $el;
|
|
}
|
|
|
|
// TODO: build an actual tree of list items
|
|
function renderTree(rootElement, rootData) {
|
|
const el = (tag, children = []) => {
|
|
let elt = document.createElement(tag)
|
|
_.each(children, item => {
|
|
elt.appendChild(item)
|
|
})
|
|
return elt
|
|
}
|
|
|
|
/**
|
|
* @param {Item[]} items
|
|
* @returns {*}
|
|
*/
|
|
let buildTree = (items) => {
|
|
return _.map(items, (item) => {
|
|
return el("li", [
|
|
el("div", [document.createTextNode(item.text)]),
|
|
el("ul", buildTree(item.children))
|
|
])
|
|
})
|
|
}
|
|
|
|
let tree = rootData.tree();
|
|
$(rootElement).children().remove()
|
|
|
|
let $list = buildTree(tree)
|
|
let $ul = $('<ul>')
|
|
$ul.append($list)
|
|
$(rootElement).append($ul)
|
|
}
|
|
|
|
/**
|
|
* @param {Element} rootElement
|
|
* @param rootData
|
|
*/
|
|
function render(rootElement, rootData) {
|
|
trigger('rendering')
|
|
|
|
let first = 0;
|
|
let last = rootData.length();
|
|
|
|
let elements = $(rootElement).children('div.list-item');
|
|
|
|
let $enter = elements.slice(first, last);
|
|
let enterData = rootData.slice(first, $enter.length);
|
|
|
|
let exitData = rootData.slice($enter.length);
|
|
let $exitEl = elements.slice($enter.length)
|
|
|
|
let hideLevel = 99999;
|
|
|
|
let closedFolds = JSON.parse(localStorage.getItem('closed-folds') || '{}') || {}
|
|
|
|
$enter.each(function (index, li) {
|
|
let storeId = enterData[index]
|
|
let value = rootData.value(storeId)
|
|
value.fold = closedFolds[value.id] ? 'closed' : 'open'
|
|
|
|
let hasChildren = false;
|
|
if (index + 1 < last) {
|
|
let next = rootData.afterValue(storeId)
|
|
hasChildren = next && (value.indented < next.indented)
|
|
}
|
|
|
|
let $li = $(li)
|
|
.attr('data-id', value.id)
|
|
.toggleClass('selected', cursor.atPosition(index))
|
|
.toggleClass('hidden', value.indented >= hideLevel)
|
|
.css('margin-left', (value.indented * 32) + 'px')
|
|
|
|
value.hidden = value.indented >= hideLevel
|
|
|
|
options.transform(value.text, $li.find('.content'), value.id, EDITOR)
|
|
|
|
if (value.indented < hideLevel) {
|
|
if (value.fold !== 'open') {
|
|
hideLevel = value.indented + 1
|
|
} else {
|
|
hideLevel = 99999;
|
|
}
|
|
}
|
|
|
|
$('.fold', $(li))
|
|
.toggleClass('open', value.fold === 'open')
|
|
.toggleClass('no-children', !hasChildren)
|
|
|
|
$li.toggleClass('no-children', !hasChildren)
|
|
.toggleClass('open', value.fold === 'open')
|
|
});
|
|
|
|
_.each(exitData, function (storeId, index) {
|
|
let value = rootData.value(storeId)
|
|
value.fold = closedFolds[value.id] ? 'closed' : 'open'
|
|
|
|
let $li = newItem(value)
|
|
.css('margin-left', (value.indented * 32) + 'px')
|
|
.toggleClass('selected', cursor.atPosition(index + $enter.length))
|
|
.toggleClass('hidden', value.indented >= hideLevel);
|
|
value.hidden = value.indented >= hideLevel
|
|
|
|
let hasChildren = false;
|
|
if (enterData.length + index + 1 < last) {
|
|
let next = rootData.afterValue(storeId)
|
|
hasChildren = next && (value.indented < next.indented)
|
|
}
|
|
|
|
if (value.indented < hideLevel) {
|
|
if (value.fold === 'open') {
|
|
hideLevel = 99999;
|
|
} else {
|
|
hideLevel = value.indented + 1
|
|
}
|
|
}
|
|
|
|
$('.fold', $li)
|
|
.toggleClass('open', value.fold === 'open')
|
|
.toggleClass('no-children', !hasChildren)
|
|
|
|
$li.toggleClass('no-children', !hasChildren)
|
|
.toggleClass('open', value.fold === 'open')
|
|
|
|
$(rootElement).append($li)
|
|
})
|
|
|
|
$exitEl.remove()
|
|
|
|
trigger('rendered')
|
|
}
|
|
|
|
function disableDragging(drake) {
|
|
if (drake) drake.destroy();
|
|
}
|
|
|
|
function enableDragging(rootElement) {
|
|
let start = -1;
|
|
let startID = null;
|
|
|
|
let drake = dragula([rootElement], {
|
|
moves: function (el, container, handle, sibling) {
|
|
return handle.classList.contains('marker')
|
|
},
|
|
accepts: function (el, target, source, sibling) {
|
|
el.style.marginLeft = sibling === null ? 0 : sibling.style.marginLeft
|
|
return true
|
|
}
|
|
})
|
|
|
|
drake.on('drag', function (el, source) {
|
|
startID = $(el).attr('data-id')
|
|
})
|
|
|
|
drake.on('drop', function (el, target, source, sibling) {
|
|
let wasEditing = editing
|
|
|
|
stopEditing(root, store, currentEditor)
|
|
|
|
let stopID = $(sibling).attr('data-id')
|
|
if (startID === stopID) {
|
|
return
|
|
}
|
|
|
|
let newPosition = store.moveBefore(startID, stopID)
|
|
cursor.set(newPosition[0])
|
|
// fix indent
|
|
|
|
_.defer(() => {
|
|
trigger('change')
|
|
|
|
if (wasEditing) {
|
|
startEditing(root, store, cursor);
|
|
}
|
|
})
|
|
|
|
})
|
|
return drake;
|
|
}
|
|
|
|
function stopEditing(rootElement, store, element) {
|
|
if (!editing) return
|
|
|
|
if (element === null) {
|
|
editing = false
|
|
currentEditor = null
|
|
return
|
|
}
|
|
|
|
let text = element.val()
|
|
element.closest('.list-item').removeClass('editor');
|
|
let id = element.data('id')
|
|
store.update(element.data('id'), (value) => {
|
|
return _.merge(value, {
|
|
text: text
|
|
})
|
|
})
|
|
|
|
let $span = $('<div class="content">');
|
|
options.transform(text, $span, id, EDITOR)
|
|
element.replaceWith($span);
|
|
trigger('stop-editing', currentEditor[0], id)
|
|
editing = false
|
|
currentEditor = null
|
|
|
|
$(root).focus()
|
|
}
|
|
|
|
/**
|
|
* @param {Element} rootElement
|
|
* @param {Store} store
|
|
* @param cursor
|
|
* @returns {jQuery|HTMLElement}
|
|
*/
|
|
function startEditing(rootElement, store, cursor) {
|
|
if (editing) return
|
|
editing = true
|
|
let elements = $(rootElement).children('div.list-item');
|
|
let $textarea = $('<textarea rows=1 class="input-line" spellcheck="false">');
|
|
$textarea.val(cursor.getCurrent(store).text);
|
|
if (cursor.lastDir() > 0) {
|
|
$textarea[0].selectionStart = $textarea[0].selectionEnd = 0
|
|
}
|
|
let currentElement = cursor.getCurrentElement(elements);
|
|
currentElement.find('.content').replaceWith($textarea)
|
|
currentElement.addClass('editor');
|
|
$textarea.focus()
|
|
$textarea.data(cursor.getCurrent(store))
|
|
currentEditor = $textarea
|
|
trigger('start-editing', currentEditor[0])
|
|
$textarea.trigger('input')
|
|
$textarea.textareaAutoSize()
|
|
return $textarea
|
|
}
|
|
|
|
$(root).on('paste', '.input-line', function (event) {
|
|
let tag = event.target.tagName.toLowerCase();
|
|
if (tag === 'textarea' && currentEditor[0].value.substring(0, 3) === '```') {
|
|
return true
|
|
}
|
|
|
|
let pastedData = event.originalEvent.clipboardData.getData('text/plain')
|
|
try {
|
|
let items = JSON.parse(pastedData.toString());
|
|
let item = $(this).parents('.list-item')
|
|
let id = item.attr('data-id')
|
|
|
|
// reset ids
|
|
_.each(items, item => {
|
|
item.id = null
|
|
})
|
|
|
|
store.insertAfter(id, ...items)
|
|
trigger('change')
|
|
return false
|
|
} catch (e) {
|
|
let items = pastedData.toString().split(/\n+/);
|
|
let item = $(this).parents('.list-item')
|
|
let id = item.attr('data-id')
|
|
if (items.length === 1) {
|
|
return true
|
|
} else {
|
|
const firstItem = store.value(id)
|
|
items = _.map(items, text => {
|
|
const item = newListItem(firstItem.indented)
|
|
item.text = text
|
|
return item
|
|
})
|
|
store.insertAfter(id, ...items)
|
|
trigger('change')
|
|
}
|
|
return false
|
|
}
|
|
});
|
|
|
|
function moveCursor(event, dir) {
|
|
let target = event.target;
|
|
|
|
let cursorInfo = null
|
|
if (target.nodeName === 'TEXTAREA') cursorInfo = textareaCursorInfo(target, dir);
|
|
if (cursorInfo !== null && !cursorInfo.leaving && dir !== 0 && !event.ctrlKey) { // FIXME: don't check modifier here
|
|
return true
|
|
}
|
|
|
|
if (dir < 0) {
|
|
cursor.moveUp(store)
|
|
} else if (dir > 0) {
|
|
cursor.moveDown(store)
|
|
}
|
|
|
|
if (editing) {
|
|
stopEditing(root, store, currentEditor);
|
|
trigger('change')
|
|
startEditing(root, store, cursor);
|
|
} else {
|
|
trigger('change')
|
|
}
|
|
|
|
_.defer(() => {
|
|
$('.list-item.selected')[0].scrollIntoView({behavior: "smooth", block: "nearest"})
|
|
})
|
|
|
|
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);
|
|
trigger('change')
|
|
return false
|
|
}
|
|
|
|
function enterEditor(event) {
|
|
let $input = startEditing(root, store, cursor)
|
|
$input[0].selectStart = $input[0].selectionEnd = 0
|
|
return false
|
|
}
|
|
|
|
function enterEditorAppend(event) {
|
|
let $input = startEditing(root, store, cursor)
|
|
$input[0].selectStart = $input[0].selectionEnd = $input[0].value.length
|
|
return false
|
|
}
|
|
|
|
function blockMoveBackward(event) {
|
|
stopEditing(root, store, currentEditor)
|
|
|
|
let before = _.clamp(cursor.get() - 1, 0, store.length())
|
|
let beforeId = store.currentID(before)
|
|
let item = cursor.getCurrent(store)
|
|
let [index, n] = store.moveBefore(item.id, beforeId)
|
|
let beforeItem = store.value(beforeId)
|
|
let dir = beforeItem ? (beforeItem.indented - item.indented) : -item.indented
|
|
store.indent(index, n, dir)
|
|
cursor.set(index)
|
|
trigger('change')
|
|
|
|
return false
|
|
}
|
|
|
|
function blockMoveForward(event) {
|
|
stopEditing(root, store, currentEditor);
|
|
let blockLen = store.lastHigherIndented(cursor.get()) - cursor.get()
|
|
let before = _.clamp(cursor.get() + 1 + blockLen, 0, store.length())
|
|
let beforeId = store.currentID(before)
|
|
let item = cursor.getCurrent(store);
|
|
let [index, n] = store.moveBefore(item.id, beforeId)
|
|
let beforeItem = store.value(beforeId)
|
|
let dir = beforeItem ? (beforeItem.indented - item.indented) : -item.indented
|
|
store.indent(index, n, dir)
|
|
cursor.set(index)
|
|
trigger('change')
|
|
return false
|
|
}
|
|
|
|
function toggleBlock(event, open) {
|
|
store.update(cursor.getId(store), function (item) {
|
|
if (open === undefined) {
|
|
open = item.fold === 'closed'
|
|
}
|
|
item.fold = open ? 'open' : 'closed'
|
|
return item
|
|
})
|
|
trigger('change')
|
|
return false
|
|
}
|
|
|
|
function expandBlock(event) {
|
|
return toggleBlock(event, true)
|
|
}
|
|
|
|
function collapseBlock(event) {
|
|
return toggleBlock(event, false)
|
|
}
|
|
|
|
function toggleTodo(event) {
|
|
store.update(cursor.getId(store), function (item) {
|
|
const res = item.text.match(/^#\[\[(TODO|DONE)\]\]/)
|
|
if (res) {
|
|
if (res[1] === 'TODO') {
|
|
item.text = item.text.replace(/#\[\[TODO\]\]\s*/, '#[[DONE]] ')
|
|
} else {
|
|
item.text = item.text.replace(/#\[\[DONE\]\]\s*/, '')
|
|
}
|
|
} else {
|
|
item.text = '#[[TODO]] ' + item.text
|
|
}
|
|
return item
|
|
})
|
|
trigger('change')
|
|
return false
|
|
}
|
|
|
|
function countBraces(sset, as) {
|
|
let set = _(sset).chain().split('').value()
|
|
let defaults = {}
|
|
defaults[set[0]] = 0
|
|
defaults[set[1]] = 0
|
|
return _(as)
|
|
.chain()
|
|
.takeWhile(c => _.includes(set, c))
|
|
.countBy()
|
|
.defaults(defaults)
|
|
.thru(x => x[set[0]] - x[set[1]])
|
|
.value()
|
|
}
|
|
|
|
function deleteCharacterBackward(event) {
|
|
let input = event.target
|
|
let value = input.value
|
|
|
|
// There is text selected, so we skip
|
|
if (input.selectionStart !== input.selectionEnd) return true
|
|
|
|
let prefix = value.slice(0, input.selectionStart)
|
|
let suffix = value.slice(input.selectionStart)
|
|
|
|
let braces = {
|
|
'[': '[]',
|
|
'(': '()',
|
|
'{': '{}',
|
|
}
|
|
|
|
let c = prefix[prefix.length - 1]
|
|
let braceSet = braces[c]
|
|
|
|
let prefixCount = _(prefix)
|
|
.split('').reverse()
|
|
.thru(_.partial(countBraces, braceSet))
|
|
.value()
|
|
|
|
let suffixCount = _(suffix)
|
|
.split('')
|
|
.thru(_.partial(countBraces, braceSet))
|
|
.value()
|
|
|
|
if (prefixCount > 0 && suffixCount < 0 && prefixCount + suffixCount === 0) {
|
|
event.preventDefault()
|
|
|
|
input.value = prefix.slice(0, prefix.length - 1) + suffix.slice(1)
|
|
_.defer(() => {
|
|
input.selectionStart = prefix.length - 1
|
|
input.selectionEnd = input.selectionStart
|
|
})
|
|
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
$(root).on('keydown', function (event) {
|
|
if (editing) {
|
|
return editorKeymap.handleKey(EDITOR, event)
|
|
} else {
|
|
return normalKeymap.handleKey(EDITOR, event)
|
|
}
|
|
})
|
|
|
|
$(root).on('click', '.marker', function () {
|
|
stopEditing(root, store, $(this).next('textarea'));
|
|
return true;
|
|
});
|
|
|
|
$(root).on('click', '.content a', function (event) {
|
|
event.stopPropagation()
|
|
return true
|
|
})
|
|
|
|
$(root).on('click', '.content', function (event) {
|
|
if ($(event.target).hasClass('checkbox')) return true;
|
|
|
|
let currentIndex = $(root).children('div.list-item').index($(this).parents('.list-item')[0])
|
|
if (cursor.atPosition(currentIndex) && currentEditor !== null && currentEditor.closest('.list-item')[0] === this) {
|
|
return true
|
|
}
|
|
stopEditing(root, store, currentEditor)
|
|
|
|
cursor.set(currentIndex)
|
|
|
|
trigger('change')
|
|
|
|
const $input = startEditing(root, store, cursor)
|
|
$input.trigger('input')
|
|
|
|
return false
|
|
})
|
|
|
|
$(root).on('click', '.fold', function () {
|
|
let open = !$(this).hasClass('open');
|
|
$(this).toggleClass('open', open)
|
|
$(this).toggleClass('closed', !open)
|
|
|
|
let item = $(this).parents('.list-item')
|
|
let elements = $(root).children('div.list-item');
|
|
let index = elements.index(item)
|
|
store.update(item.attr('data-id'), function (item) {
|
|
item.fold = open ? 'open' : 'closed'
|
|
return item
|
|
})
|
|
|
|
trigger('change')
|
|
});
|
|
|
|
function update(id, callback) {
|
|
let changed = false
|
|
store.update(id, function (item, prev, next) {
|
|
let before = Object.assign({}, item)
|
|
item = callback(item, prev, next)
|
|
changed = item.text !== before.text || item.indented !== before.indented || item.fold !== before.fold
|
|
return item
|
|
})
|
|
if (changed) {
|
|
trigger('change')
|
|
}
|
|
}
|
|
|
|
EDITOR.on('change', renderUpdate)
|
|
|
|
return EDITOR;
|
|
}
|
|
|
|
export default editor
|