import _ from 'lodash' import $ from 'jquery' import he from 'he' import textareaAutosizeInit from "./textarea.autosize" import dragula from 'dragula' import createCursor from './cursor' import createSelection from './selection' import Store from './store' textareaAutosizeInit($) function editor(root, inputData, options) { root.classList.add('root') root.setAttribute('tabindex', '-1') let cursor = createCursor() let selection = createSelection() let defaults = { transform(text, element) { element.html(he.encode(text)) } } options = _.merge(defaults, options) let drake = null; function createStore(inputData) { let data = [ {indented: 0, text: '', fold: 'open'}, ]; if (inputData.length) { data = inputData } return Store(data); } let store = createStore(inputData); 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 = $('
') .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) line.prepend($('')) line.prepend($('')) el.prepend(line) return el; } /** * @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).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') .find('.content') value.hidden = value.indented >= hideLevel options.transform(value.text, $li) 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).data('id') }) drake.on('drop', function (el, target, source, sibling) { let stopID = $(sibling).data('id') if (startID === stopID) { return } let id = store.moveBefore(startID, stopID) let position = store.index(id); cursor.set(position) selection.selectOne(position, store) 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'); store.update(element.data('id'), (value) => { return _.merge(value, { text: text }) }) if (store.hasChanged()) { trigger('change') store.clearChanged() } let $span = $('
'); options.transform(text, $span) element.replaceWith($span); trigger('stop-editing', currentEditor[0]) 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 = $('