import _ from 'lodash' import $ from 'jquery' 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 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 selection = createSelection() let store = createStore(inputData); let drake = null; 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()) { resolve(store.debug().result) store.clearChanged() } else { reject() } }); } function saveTree(from) { return new Promise(function (resolve, reject) { if (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) } return new Promise(function (resolve, reject) { resolve(store.value(id)); }); } 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() { disableDragging(drake) render(root, store); drake = enableDragging(root) cursor.set(0) startEditing(root, store, cursor) } function replaceChildren(id, children) { store.replaceChildren(id, children) } function renderUpdate() { disableDragging(drake) render(root, store); drake = enableDragging(root) } let EDITOR = { on, save, saveTree, copy, update, start, zoomin, treeForId, replaceChildren, render: renderUpdate }; 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 = $('
') .attr('data-id', value.id) .data('indented', value.indented) .css('margin-left', (value.indented * 32) + 'px') let line = $('
') let content = $('
') line.prepend(content) options.transform(value.text, content, value.id, EDITOR) line.prepend($('')) line.prepend($('')) 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.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; $enter.each(function (index, li) { let storeId = enterData[index] let value = rootData.value(storeId) 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('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 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) 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 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 drake = dragula([rootElement], { moves: function (el, container, handle) { return handle.classList.contains('marker') } }); let start = -1; let startID = null; drake.on('drag', function (el, source) { startID = $(el).attr('data-id') }) drake.on('drop', function (el, target, source, sibling) { stopEditing(root, store, currentEditor) let stopID = $(sibling).attr('data-id') if (startID === stopID) { return } let id = store.moveBefore(startID, stopID) if (id === startID) { return } let position = store.index(id); cursor.set(position) selection.selectOne(position, store) _.defer(renderUpdate) trigger('change') }) 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 }) }) if (store.hasChanged()) { trigger('change') store.clearChanged() } let $span = $('
    '); 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 = $('