wiki/list-editor/index.js

901 lines
26 KiB
JavaScript
Raw Normal View History

2020-10-14 21:43:52 +00:00
import _ from 'lodash'
import $ from 'jquery'
import he from 'he'
import dragula from 'dragula'
2020-10-25 18:49:22 +00:00
import textareaAutosizeInit from "./textarea.autosize"
2020-10-14 21:43:52 +00:00
import createCursor from './cursor'
import Store from './store'
import Keymap from './keymap'
2022-01-15 00:24:16 +00:00
import getCaretCoordinates from "../editor/src/caret-position"
import TurndownService from "turndown"
2020-10-14 21:43:52 +00:00
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
};
}
2020-10-14 21:43:52 +00:00
function editor(root, inputData, options) {
let cursor = createCursor()
2020-10-25 14:22:49 +00:00
let store = createStore(inputData);
2020-10-28 19:01:25 +00:00
let drake = null;
2020-10-14 21:43:52 +00:00
2020-11-08 20:15:54 +00:00
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')
2020-11-03 20:23:43 +00:00
// keymap.mapKey('C-]', 'zoomIn')
// keymap.mapKey('C-[', 'zoomOut')
2020-11-08 20:15:54 +00:00
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')
2020-11-08 20:15:54 +00:00
2020-10-14 21:43:52 +00:00
function createStore(inputData) {
let data = [
{indented: 0, text: '', fold: 'open'},
];
if (inputData.length) {
data = inputData
}
return Store(data);
}
2020-10-25 14:22:49 +00:00
function save() {
return new Promise(function (resolve, reject) {
2021-09-25 18:58:08 +00:00
2020-10-25 14:22:49 +00:00
if (store.hasChanged()) {
2021-09-25 18:58:08 +00:00
let result = store.debug().result
resolve(result)
2020-10-25 14:22:49 +00:00
store.clearChanged()
}
});
}
2020-11-02 21:51:19 +00:00
function saveTree(from, opt) {
opt = _.merge({force: false}, opt)
2020-10-25 14:22:49 +00:00
return new Promise(function (resolve, reject) {
2020-11-02 21:51:19 +00:00
if (opt.force || store.hasChanged()) {
2020-10-25 14:22:49 +00:00
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')
2020-10-26 23:14:45 +00:00
let id = item.attr('data-id')
2020-10-25 14:22:49 +00:00
if (opt.recursive) {
2020-11-02 21:51:19 +00:00
return saveTree(id, {force: true})
2020-10-25 14:22:49 +00:00
}
return new Promise(function (resolve, reject) {
resolve(store.value(id));
});
}
2021-11-06 13:36:10 +00:00
function flat(element, opt) {
opt = opt || {}
let item = $(element).parents('.list-item')
let id = item.attr('data-id')
return new Promise(function (resolve, reject) {
2021-11-06 13:36:10 +00:00
resolve(store.flat(id, opt));
});
}
2020-10-25 14:22:49 +00:00
function zoomin(element, opt) {
let item = $(element).parents('.list-item')
2020-10-26 23:14:45 +00:00
let id = item.attr('data-id')
2020-10-25 14:22:49 +00:00
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;
})
}
2021-09-25 18:58:08 +00:00
return true;
})
2020-10-25 14:22:49 +00:00
cursor.set(0)
renderUpdate()
2020-10-25 14:22:49 +00:00
}
2020-11-14 14:56:22 +00:00
function getChildren(id) {
return store.children(id)
}
2020-11-14 14:56:22 +00:00
2020-10-29 22:09:42 +00:00
function replaceChildren(id, children) {
2020-10-28 19:01:25 +00:00
store.replaceChildren(id, children)
}
2020-10-29 22:09:42 +00:00
function renderUpdate() {
2020-10-28 19:01:25 +00:00
disableDragging(drake)
render(root, store);
drake = enableDragging(root)
}
2020-10-25 14:22:49 +00:00
let EDITOR = {
normalKeymap,
editorKeymap,
2020-10-25 14:22:49 +00:00
on,
save,
saveTree,
copy,
flat,
2020-10-25 14:22:49 +00:00
update,
start,
zoomin,
2020-10-28 19:01:25 +00:00
treeForId,
getChildren,
2020-10-28 19:01:25 +00:00
replaceChildren,
render: renderUpdate,
deleteBlock,
forwardLine,
backwardLine,
insertLineAbove,
insertLineBelow,
indentBlock,
leaveEditor,
2020-11-08 20:15:54 +00:00
enterEditor,
enterEditorAppend,
blockMoveBackward,
2020-11-01 21:10:00 +00:00
blockMoveForward,
deleteCharacterBackward,
2020-11-01 21:10:00 +00:00
expandBlock,
2020-11-02 21:43:05 +00:00
collapseBlock,
toggleBlock,
toggleTodo
2020-10-25 14:22:49 +00:00
};
root.classList.add('root')
root.setAttribute('tabindex', '-1')
let defaults = {
transform(text, element) {
element.html(he.encode(text))
}
}
options = _.merge(defaults, options)
2020-10-14 21:43:52 +00:00
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) {
2020-10-31 22:29:01 +00:00
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')
2020-10-14 21:43:52 +00:00
line.prepend(content)
2020-10-31 22:29:01 +00:00
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 = '&#9654;'
line.prepend(marker)
line.prepend(fold)
2020-10-14 21:43:52 +00:00
el.prepend(line)
2020-10-31 22:29:01 +00:00
return $el;
2020-10-14 21:43:52 +00:00
}
2020-10-31 19:56:45 +00:00
// 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)
}
2020-10-14 21:43:52 +00:00
/**
* @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') || '{}') || {}
2020-10-14 21:43:52 +00:00
$enter.each(function (index, li) {
let storeId = enterData[index]
let value = rootData.value(storeId)
value.fold = closedFolds[value.id] ? 'closed' : 'open'
2020-10-14 21:43:52 +00:00
let hasChildren = false;
if (index + 1 < last) {
let next = rootData.afterValue(storeId)
hasChildren = next && (value.indented < next.indented)
}
2020-10-26 23:14:45 +00:00
let $li = $(li)
.attr('data-id', value.id)
2020-10-14 21:43:52 +00:00
.toggleClass('selected', cursor.atPosition(index))
.toggleClass('hidden', value.indented >= hideLevel)
.css('margin-left', (value.indented * 32) + 'px')
2020-10-26 23:14:45 +00:00
2020-10-14 21:43:52 +00:00
value.hidden = value.indented >= hideLevel
2020-10-26 23:14:45 +00:00
options.transform(value.text, $li.find('.content'), value.id, EDITOR)
2020-10-14 21:43:52 +00:00
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)
2020-10-26 23:14:45 +00:00
$li.toggleClass('no-children', !hasChildren)
2020-10-14 21:43:52 +00:00
.toggleClass('open', value.fold === 'open')
});
_.each(exitData, function (storeId, index) {
let value = rootData.value(storeId)
value.fold = closedFolds[value.id] ? 'closed' : 'open'
2020-10-14 21:43:52 +00:00
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;
2020-10-14 21:43:52 +00:00
let drake = dragula([rootElement], {
moves: function (el, container, handle, sibling) {
2020-10-14 21:43:52 +00:00
return handle.classList.contains('marker')
},
accepts: function (el, target, source, sibling) {
el.style.marginLeft = sibling === null ? 0 : sibling.style.marginLeft
return true
2020-10-14 21:43:52 +00:00
}
2020-11-05 19:51:12 +00:00
})
2020-10-14 21:43:52 +00:00
drake.on('drag', function (el, source) {
2020-10-26 23:14:45 +00:00
startID = $(el).attr('data-id')
2020-10-14 21:43:52 +00:00
})
drake.on('drop', function (el, target, source, sibling) {
2020-11-02 20:56:42 +00:00
let wasEditing = editing
2020-10-30 22:43:00 +00:00
stopEditing(root, store, currentEditor)
2020-11-02 20:56:42 +00:00
2020-10-26 23:14:45 +00:00
let stopID = $(sibling).attr('data-id')
2020-10-14 21:43:52 +00:00
if (startID === stopID) {
return
}
2020-11-02 20:56:42 +00:00
let newPosition = store.moveBefore(startID, stopID)
2020-11-04 22:41:13 +00:00
cursor.set(newPosition[0])
// fix indent
2020-10-14 21:43:52 +00:00
2020-11-02 20:56:42 +00:00
_.defer(() => {
trigger('change')
if (wasEditing) {
startEditing(root, store, cursor);
}
})
2020-10-14 21:43:52 +00:00
})
return drake;
}
function stopEditing(rootElement, store, element) {
if (!editing) return
2020-10-14 21:43:52 +00:00
if (element === null) {
editing = false
currentEditor = null
return
}
let text = element.val()
2020-10-28 19:01:25 +00:00
element.closest('.list-item').removeClass('editor');
let id = element.data('id')
2020-10-27 06:38:07 +00:00
store.update(element.data('id'), (value) => {
2020-10-14 21:43:52 +00:00
return _.merge(value, {
text: text
})
})
let $span = $('<div class="content">');
2020-10-28 19:01:25 +00:00
options.transform(text, $span, id, EDITOR)
2020-10-14 21:43:52 +00:00
element.replaceWith($span);
2020-10-28 19:01:25 +00:00
trigger('stop-editing', currentEditor[0], id)
2020-10-14 21:43:52 +00:00
editing = false
currentEditor = null
$(root).focus()
2020-10-14 21:43:52 +00:00
}
/**
* @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');
2020-10-15 20:38:52 +00:00
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
}
2020-10-14 21:43:52 +00:00
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])
2020-10-31 12:37:09 +00:00
$textarea.trigger('input')
$textarea.textareaAutoSize()
2020-10-14 21:43:52 +00:00
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
}
2022-01-15 00:24:16 +00:00
if (event.originalEvent.clipboardData.types.includes("text/html")) {
let pastedData = event.originalEvent.clipboardData.getData('text/html')
const turndownService = new TurndownService({
headingStyle: 'atx',
codeBlockStyle: 'fenced',
})
const markdown = turndownService.turndown(pastedData)
let items = markdown.split(/\n+/);
console.log(items)
let item = $(this).parents('.list-item')
let id = item.attr('data-id')
const firstItem = store.value(id)
items = _.map(items, text => {
const m = text.match(/^(\s*)\*\s*(.*)$/)
if (m) {
const item = newListItem(firstItem.indented+1+Math.trunc(m[1].length/4))
item.text = m[2]
return item
}
const item = newListItem(firstItem.indented)
item.text = text
return item
})
store.insertAfter(id, ...items)
trigger('change')
return false
}
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 inputString = pastedData.toString()
if (inputString.match(/^- /)) {
let item = $(this).parents('.list-item')
let id = item.attr('data-id')
let lines = inputString.split(/^( *)- /ms)
lines.shift()
const firstItem = store.value(id)
const firstIndent = firstItem.indented
let items = _.map(_.chunk(lines, 2), line => {
const indent = Math.trunc(line[0].length / 4);
const item = newListItem(firstItem.indented+indent)
item.text = _.trimEnd(line[1]).replace(/\n/g, " \n")
return item
})
store.insertAfter(id, ...items)
trigger('change')
return false
} else {
let items = inputString.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
}
}
2020-10-14 21:43:52 +00:00
});
function moveCursor(event, dir) {
let target = event.target;
2020-10-31 13:55:13 +00:00
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
2020-10-14 21:43:52 +00:00
return true
}
if (dir < 0) {
cursor.moveUp(store)
} else if (dir > 0) {
cursor.moveDown(store)
}
2020-10-14 21:43:52 +00:00
if (editing) {
2020-10-14 21:43:52 +00:00
stopEditing(root, store, currentEditor);
trigger('change')
startEditing(root, store, cursor);
2020-10-14 21:43:52 +00:00
} else {
trigger('change')
2020-10-14 21:43:52 +00:00
}
_.defer(() => {
$('.list-item.selected')[0].scrollIntoView({behavior: "smooth", block: "nearest"})
})
cursor.resetLastMove()
2020-10-14 21:43:52 +00:00
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]] ';
2020-10-14 21:43:52 +00:00
}
cursor.insertBelow(store, item)
2020-10-14 21:43:52 +00:00
}
2020-10-28 19:01:25 +00:00
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);
2020-11-09 20:30:03 +00:00
trigger('change')
return false
}
2020-11-08 20:15:54 +00:00
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) {
2020-11-02 21:42:09 +00:00
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);
2020-11-02 21:42:09 +00:00
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);
2020-11-02 21:42:09 +00:00
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
}
2020-11-02 21:43:05 +00:00
function toggleBlock(event, open) {
2020-11-01 21:10:00 +00:00
store.update(cursor.getId(store), function (item) {
2020-11-02 21:43:05 +00:00
if (open === undefined) {
open = item.fold === 'closed'
}
2020-11-01 21:10:00 +00:00
item.fold = open ? 'open' : 'closed'
return item
})
trigger('change')
return false
}
2020-11-02 21:43:05 +00:00
function expandBlock(event) {
return toggleBlock(event, true)
}
2020-11-01 21:10:00 +00:00
function collapseBlock(event) {
2020-11-02 21:43:05 +00:00
return toggleBlock(event, false)
2020-11-01 21:10:00 +00:00
}
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) {
2020-11-08 20:15:54 +00:00
if (editing) {
return editorKeymap.handleKey(EDITOR, event)
} else {
return normalKeymap.handleKey(EDITOR, event)
}
2020-10-14 21:43:52 +00:00
})
2020-10-14 21:43:52 +00:00
$(root).on('click', '.marker', function () {
stopEditing(root, store, $(this).next('textarea'));
return true;
2020-10-14 21:43:52 +00:00
});
$(root).on('click', '.content a', function (event) {
event.stopPropagation()
return true
})
2020-10-21 18:49:23 +00:00
$(root).on('click', '.content', function (event) {
2020-10-26 23:14:45 +00:00
if ($(event.target).hasClass('checkbox')) return true;
2020-10-21 18:49:23 +00:00
let currentIndex = $(root).children('div.list-item').index($(this).parents('.list-item')[0])
2020-10-14 21:43:52 +00:00
if (cursor.atPosition(currentIndex) && currentEditor !== null && currentEditor.closest('.list-item')[0] === this) {
return true
}
stopEditing(root, store, currentEditor)
cursor.set(currentIndex)
trigger('change')
2020-10-14 21:43:52 +00:00
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)
2020-10-26 23:14:45 +00:00
store.update(item.attr('data-id'), function (item) {
2020-10-14 21:43:52 +00:00
item.fold = open ? 'open' : 'closed'
return item
})
trigger('change')
2020-10-14 21:43:52 +00:00
});
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)
2020-10-25 14:22:49 +00:00
return EDITOR;
2020-10-14 21:43:52 +00:00
}
export default editor