17 changed files with 1754 additions and 38 deletions
@ -0,0 +1,128 @@
@@ -0,0 +1,128 @@
|
||||
# Changelog |
||||
|
||||
All notable changes to this project will be documented in this file. |
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), |
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). |
||||
|
||||
## [Unreleased] |
||||
|
||||
## [0.8.13] - 2020-09-03 |
||||
|
||||
* Allow multiple editors on the same page. |
||||
|
||||
## [0.8.12] - 2020-07-19 |
||||
|
||||
* Upgraded lodash to 4.17.19 |
||||
* Fix typo in import function name |
||||
|
||||
## [0.8.11] - 2020-06-28 |
||||
|
||||
* Fixed store saveTree, remove children from other items, by making a clone |
||||
|
||||
## [0.8.10] - 2020-06-28 |
||||
|
||||
* Publish to Github and NPM |
||||
|
||||
## [0.8.9] - 2020-06-28 |
||||
|
||||
* Added improvements for CI |
||||
|
||||
## [0.8.8] - 2020-06-28 |
||||
|
||||
* Added `saveTree` method with selection by id. This selects all children of |
||||
the element with `id`. |
||||
* Fixed bug where not all items where present in tree result of `saveTree` |
||||
|
||||
## [0.8.7] - 2020-06-24 |
||||
|
||||
* Added `id` parameter to `update` method |
||||
|
||||
## [0.8.6] - 2020-06-24 |
||||
|
||||
* Fixed update method of editor where the before object wasn't cloned, so |
||||
was always equal to replace object |
||||
|
||||
## [0.8.5] - 2020-06-24 |
||||
|
||||
## [0.8.4] - 2020-06-24 |
||||
|
||||
* Added 'update' method to editor. It calls the 'update' method of store. |
||||
|
||||
## [0.8.3] - 2020-06-23 |
||||
|
||||
* Added 'hidden' to default fields newListItem. |
||||
* Fix movement bug when moving up |
||||
|
||||
## [0.8.2] - 2020-06-23 |
||||
|
||||
* Simplify indenting. Now we indent all children with a higher indent numbering. |
||||
|
||||
## [0.8.1] - 2020-06-23 |
||||
|
||||
* Fix small bugs |
||||
|
||||
## [0.8.0] - 2020-06-17 |
||||
|
||||
* Added rendering/rendered events |
||||
* Changed transform option, the second arguments is on element now |
||||
|
||||
## [0.7.6] - 2020-06-15 |
||||
|
||||
* Remove the jump from the textarea with multiple lines when moving to it. |
||||
* Use the indent of the next item when it is larger then the current indent, |
||||
but use the current indent otherwise. |
||||
|
||||
## [0.7.5] - 2020-06-11 |
||||
|
||||
* Add classes for borders when >= 1 indented |
||||
* Fixed bug where content was removed, when the marker was clicked on the |
||||
line with the active editor |
||||
|
||||
## [0.7.4] - 2020-06-11 |
||||
|
||||
* Add .no-children, .open to .list-item, to simplify css |
||||
|
||||
## [0.7.3] - 2020-06-09 |
||||
|
||||
* Remove mirroring of characters |
||||
|
||||
## [0.7.2] - 2020-06-08 |
||||
|
||||
* Make links in content clickable |
||||
* Add "=" handling of selected text. |
||||
|
||||
## [0.7.1] - 2020-06-08 |
||||
|
||||
* Add options |
||||
* Add transform function to options |
||||
|
||||
## [0.7.0] - 2020-06-07 |
||||
|
||||
## [0.6.9] - 2020-06-07 |
||||
|
||||
### Added |
||||
|
||||
* Add `saveTree` method that passes a tree-like structure as the first |
||||
argument. |
||||
|
||||
### Changed |
||||
|
||||
* Start new item opened |
||||
|
||||
[Unreleased]: https://github.com/pstuifzand/list-editor/compare/0.8.13...HEAD |
||||
[0.8.13]: https://github.com/pstuifzand/list-editor/compare/0.8.12...0.8.13 |
||||
[0.8.12]: https://github.com/pstuifzand/list-editor/compare/0.8.11...0.8.12 |
||||
[0.8.11]: https://github.com/pstuifzand/list-editor/compare/0.8.10...0.8.11 |
||||
[0.8.10]: https://github.com/pstuifzand/list-editor/compare/0.8.9...0.8.10 |
||||
[0.8.9]: https://github.com/pstuifzand/list-editor/compare/0.8.8...0.8.9 |
||||
[0.8.8]: https://github.com/pstuifzand/list-editor/compare/0.8.7...0.8.8 |
||||
[0.8.7]: https://github.com/pstuifzand/list-editor/compare/0.8.6...0.8.7 |
||||
[0.8.6]: https://github.com/pstuifzand/list-editor/compare/0.8.5...0.8.6 |
||||
[0.8.5]: https://github.com/pstuifzand/list-editor/compare/0.8.4...0.8.5 |
||||
[0.8.4]: https://github.com/pstuifzand/list-editor/compare/0.8.3...0.8.4 |
||||
[0.8.3]: https://github.com/pstuifzand/list-editor/compare/0.8.2...0.8.3 |
||||
[0.8.2]: https://github.com/pstuifzand/list-editor/compare/0.8.1...0.8.2 |
||||
[0.8.1]: https://github.com/pstuifzand/list-editor/compare/0.8.0...0.8.1 |
||||
[0.8.0]: https://github.com/pstuifzand/list-editor/compare/0.7.8...0.8.0 |
||||
[0.7.8]: https://github.com/pstuifzand/list-editor/compare/0.7.7...0.7.8 |
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
 |
||||
|
||||
# Wiki List Editor |
||||
|
||||
## Installation |
||||
|
||||
First install the package. |
||||
|
||||
```bash |
||||
npm install wiki-list-editor --save |
||||
``` |
||||
|
||||
And then use it in your javascript code. |
||||
|
||||
```js |
||||
import editor from 'wiki-list-editor'; |
||||
|
||||
let div = document.createElement('div') |
||||
let listEditor = editor(div); |
||||
document.body.appendChild(div); |
||||
``` |
||||
|
||||
## Author |
||||
|
||||
Peter Stuifzand <peter@p83.nl> |
@ -0,0 +1,58 @@
@@ -0,0 +1,58 @@
|
||||
function createCursor(start) { |
||||
let cursor = start; |
||||
|
||||
return { |
||||
get() { |
||||
return cursor; |
||||
}, |
||||
set(newPosition) { |
||||
cursor = newPosition; |
||||
}, |
||||
getId(store) { |
||||
return store.currentID(cursor) |
||||
}, |
||||
atFirst() { |
||||
return cursor === 0; |
||||
}, |
||||
atPosition(other) { |
||||
return cursor === other; |
||||
}, |
||||
atEnd(store) { |
||||
return cursor === store.length() |
||||
}, |
||||
hasMoved(saved) { |
||||
return cursor !== saved.get() |
||||
}, |
||||
getCurrent(store) { |
||||
let id = store.currentID(cursor) |
||||
return store.value(id) |
||||
}, |
||||
getCurrentElement(elements) { |
||||
return elements.slice(cursor, cursor + 1); |
||||
}, |
||||
save() { |
||||
return createCursor(cursor); |
||||
}, |
||||
moveUp(store) { |
||||
cursor = store.prevCursorPosition(cursor) |
||||
}, |
||||
moveDown(store) { |
||||
cursor = store.nextCursorPosition(cursor, true) |
||||
}, |
||||
remove(store) { |
||||
store.remove(cursor, 1) |
||||
}, |
||||
insertAbove(store, item) { |
||||
store.insertBefore(store.currentID(cursor), item) |
||||
}, |
||||
insertBelow(store, item) { |
||||
let id = store.insertAfter(store.currentID(cursor), item) |
||||
cursor = store.index(id) |
||||
}, |
||||
forwardToNextVisible(store) { |
||||
cursor = store.nextCursorPosition(cursor, false) |
||||
}, |
||||
}; |
||||
} |
||||
|
||||
export default createCursor; |
@ -0,0 +1,500 @@
@@ -0,0 +1,500 @@
|
||||
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') |
||||
|
||||
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 = $('<div class="list-item">') |
||||
.data('id', value.id) |
||||
.data('indented', value.indented) |
||||
.css('margin-left', (value.indented * 32) + 'px') |
||||
let line = $('<div class="line">') |
||||
let content = $('<div class="content">') |
||||
line.prepend(content) |
||||
options.transform(value.text, content) |
||||
line.prepend($('<span class="marker"></span>')) |
||||
line.prepend($('<span class="fold">▶</span>')) |
||||
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 |
||||
}) |
||||
}) |
||||
|
||||
trigger('change') |
||||
|
||||
let $span = $('<div class="content">'); |
||||
options.transform(text, $span) |
||||
element.replaceWith($span); |
||||
trigger('stop-editing', currentEditor[0]) |
||||
editing = false |
||||
currentEditor = null |
||||
} |
||||
|
||||
/** |
||||
* @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">'); |
||||
$textarea.val(cursor.getCurrent(store).text).trigger('input') |
||||
let currentElement = cursor.getCurrentElement(elements); |
||||
currentElement.find('.content').replaceWith($textarea) |
||||
currentElement.addClass('editor'); |
||||
$textarea.focus() |
||||
$textarea.data(cursor.getCurrent(store)) |
||||
$textarea.textareaAutoSize() |
||||
currentEditor = $textarea |
||||
trigger('start-editing', currentEditor[0]) |
||||
return $textarea |
||||
} |
||||
|
||||
function save() { |
||||
return new Promise(function (resolve, reject) { |
||||
resolve(store.debug().result); |
||||
}); |
||||
} |
||||
|
||||
function saveTree(from) { |
||||
return new Promise(function (resolve, reject) { |
||||
resolve(store.tree(from)) |
||||
}); |
||||
} |
||||
|
||||
function copy(element, opt) { |
||||
let item = $(element).parents('.list-item') |
||||
let id = item.data('id') |
||||
|
||||
if (opt.recursive) { |
||||
return saveTree(id) |
||||
} |
||||
|
||||
return new Promise(function (resolve, reject) { |
||||
resolve(store.value(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) |
||||
} |
||||
|
||||
$(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 parentItem = $(this).parents('.list-item') |
||||
let index = $(root).children('div.list-item').index(parentItem) |
||||
let pastedData = event.originalEvent.clipboardData.getData('text') |
||||
let lines = pastedData.split(/\n/) |
||||
if (lines.length === 1) { |
||||
return true; |
||||
} |
||||
|
||||
let currentID = store.currentID(index); |
||||
let baseIndent = store.value(currentID).indented |
||||
|
||||
let newItems = _.filter(_.map(lines, function (line) { |
||||
if (line.length === 0) return; |
||||
let matches = line.match(/(\s{4})/g) |
||||
let relIndent = matches ? matches.length : 0; |
||||
let newItem = newListItem(baseIndent + relIndent) |
||||
newItem.text = line.replace(/^\s+/, '') |
||||
return newItem |
||||
})) |
||||
store.insertAfter(currentID, ...newItems) |
||||
|
||||
disableDragging(drake) |
||||
render(root, store); |
||||
drake = enableDragging(root) |
||||
return false |
||||
}); |
||||
|
||||
$(root).on('keydown', '.input-line', function (event) { |
||||
if (event.key === 'Escape') { |
||||
stopEditing(root, store, $(this)) |
||||
selection.selectOne(cursor.get(), store) |
||||
return false |
||||
} |
||||
return true |
||||
}); |
||||
|
||||
$(root).on('keydown', function (event) { |
||||
let tag = event.target.tagName.toLowerCase(); |
||||
if (tag === 'textarea' && currentEditor[0].value.substring(0, 3) === '```') { |
||||
return true |
||||
} |
||||
let next = true |
||||
let prevSelected = cursor.save(); |
||||
if (event.key === 'ArrowUp') { |
||||
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); |
||||
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) |
||||
cursor.insertBelow(store, item) |
||||
} |
||||
|
||||
selection.selectOne(cursor.get(), store) |
||||
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); |
||||
} |
||||
} |
||||
if (event.key === 'Enter') { |
||||
startEditing(root, store, cursor); |
||||
return false |
||||
} else if (event.key === 'Escape') { |
||||
stopEditing(root, store, currentEditor); |
||||
return false |
||||
} |
||||
return next |
||||
}) |
||||
$(root).on('click', '.marker', function () { |
||||
stopEditing(root, store, $(this).next('textarea')); |
||||
return false; |
||||
}); |
||||
|
||||
$(root).on('click', '.content a', function (event) { |
||||
event.stopPropagation() |
||||
return true |
||||
}) |
||||
|
||||
$(root).on('click', '.list-item', function () { |
||||
let currentIndex = $(root).children('div.list-item').index(this) |
||||
if (cursor.atPosition(currentIndex) && currentEditor !== null && currentEditor.closest('.list-item')[0] === this) { |
||||
return true |
||||
} |
||||
stopEditing(root, store, currentEditor) |
||||
|
||||
cursor.set(currentIndex) |
||||
selection.selectOne(cursor.get(), store) |
||||
|
||||
disableDragging(drake) |
||||
render(root, store); |
||||
drake = enableDragging(root) |
||||
|
||||
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.data('id'), function (item) { |
||||
item.fold = open ? 'open' : 'closed' |
||||
return item |
||||
}) |
||||
|
||||
disableDragging(drake) |
||||
render(root, store); |
||||
drake = enableDragging(root) |
||||
}); |
||||
|
||||
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') |
||||
} |
||||
} |
||||
|
||||
return { |
||||
on, |
||||
save, |
||||
saveTree, |
||||
copy, |
||||
update, |
||||
start |
||||
}; |
||||
} |
||||
|
||||
export default editor |
@ -0,0 +1,174 @@
@@ -0,0 +1,174 @@
|
||||
{ |
||||
"name": "wiki-list-editor", |
||||
"version": "0.8.12", |
||||
"lockfileVersion": 1, |
||||
"requires": true, |
||||
"dependencies": { |
||||
"atoa": { |
||||
"version": "1.0.0", |
||||
"resolved": "https://registry.npmjs.org/atoa/-/atoa-1.0.0.tgz", |
||||
"integrity": "sha1-DMDpGkgOc4+SPrwQNnZHF3mzSkk=" |
||||
}, |
||||
"balanced-match": { |
||||
"version": "1.0.0", |
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", |
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", |
||||
"dev": true |
||||
}, |
||||
"brace-expansion": { |
||||
"version": "1.1.11", |
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", |
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", |
||||
"dev": true, |
||||
"requires": { |
||||
"balanced-match": "^1.0.0", |
||||
"concat-map": "0.0.1" |
||||
} |
||||
}, |
||||
"concat-map": { |
||||
"version": "0.0.1", |
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", |
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", |
||||
"dev": true |
||||
}, |
||||
"contra": { |
||||
"version": "1.9.4", |
||||
"resolved": "https://registry.npmjs.org/contra/-/contra-1.9.4.tgz", |
||||
"integrity": "sha1-9TveQtfltZhcrk2ZqNYQUm3o8o0=", |
||||
"requires": { |
||||
"atoa": "1.0.0", |
||||
"ticky": "1.0.1" |
||||
} |
||||
}, |
||||
"crossvent": { |
||||
"version": "1.5.4", |
||||
"resolved": "https://registry.npmjs.org/crossvent/-/crossvent-1.5.4.tgz", |
||||
"integrity": "sha1-2ixPj0DJR4JRe/K+7BBEFIGUq5I=", |
||||
"requires": { |
||||
"custom-event": "1.0.0" |
||||
} |
||||
}, |
||||
"custom-event": { |
||||
"version": "1.0.0", |
||||
"resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.0.tgz", |
||||
"integrity": "sha1-LkYovhncSyFLXAJjDFlx6BFhgGI=" |
||||
}, |
||||
"dragula": { |
||||
"version": "3.7.2", |
||||
"resolved": "https://registry.npmjs.org/dragula/-/dragula-3.7.2.tgz", |
||||
"integrity": "sha1-SjXJ05gf+sGpScKcpyhQWOhzk84=", |
||||
"requires": { |
||||
"contra": "1.9.4", |
||||
"crossvent": "1.5.4" |
||||
} |
||||
}, |
||||
"esm": { |
||||
"version": "3.2.25", |
||||
"resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", |
||||
"integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", |
||||
"dev": true |
||||
}, |
||||
"fs.realpath": { |
||||
"version": "1.0.0", |
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", |
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", |
||||
"dev": true |
||||
}, |
||||
"glob": { |
||||
"version": "7.1.6", |
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", |
||||
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", |
||||
"dev": true, |
||||
"requires": { |
||||
"fs.realpath": "^1.0.0", |
||||
"inflight": "^1.0.4", |
||||
"inherits": "2", |
||||
"minimatch": "^3.0.4", |
||||
"once": "^1.3.0", |
||||
"path-is-absolute": "^1.0.0" |
||||
} |
||||
}, |
||||
"he": { |
||||
"version": "1.2.0", |
||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", |
||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" |
||||
}, |
||||
"inflight": { |
||||
"version": "1.0.6", |
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", |
||||
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", |
||||
"dev": true, |
||||
"requires": { |
||||
"once": "^1.3.0", |
||||
"wrappy": "1" |
||||
} |
||||
}, |
||||
"inherits": { |
||||
"version": "2.0.4", |
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", |
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", |
||||
"dev": true |
||||
}, |
||||
"jasmine": { |
||||
"version": "3.5.0", |
||||
"resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.5.0.tgz", |
||||
"integrity": "sha512-DYypSryORqzsGoMazemIHUfMkXM7I7easFaxAvNM3Mr6Xz3Fy36TupTrAOxZWN8MVKEU5xECv22J4tUQf3uBzQ==", |
||||
"dev": true, |
||||
"requires": { |
||||
"glob": "^7.1.4", |
||||
"jasmine-core": "~3.5.0" |
||||
} |
||||
}, |
||||
"jasmine-core": { |
||||
"version": "3.5.0", |
||||
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz", |
||||
"integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==", |
||||
"dev": true |
||||
}, |
||||
"jquery": { |
||||
"version": "3.5.1", |
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz", |
||||
"integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg==" |
||||
}, |
||||
"lodash": { |
||||
"version": "4.17.19", |
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", |
||||
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" |
||||
}, |
||||
"minimatch": { |
||||
"version": "3.0.4", |
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", |
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", |
||||
"dev": true, |
||||
"requires": { |
||||
"brace-expansion": "^1.1.7" |
||||
} |
||||
}, |
||||
"once": { |
||||
"version": "1.4.0", |
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", |
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", |
||||
"dev": true, |
||||
"requires": { |
||||
"wrappy": "1" |
||||
} |
||||
}, |
||||
"path-is-absolute": { |
||||
"version": "1.0.1", |
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", |
||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", |
||||
"dev": true |
||||
}, |
||||
"ticky": { |
||||
"version": "1.0.1", |
||||
"resolved": "https://registry.npmjs.org/ticky/-/ticky-1.0.1.tgz", |
||||
"integrity": "sha1-t8+nHnaPHJAAxJe5FRswlHxQ5G0=" |
||||
}, |
||||
"wrappy": { |
||||
"version": "1.0.2", |
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", |
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", |
||||
"dev": true |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,30 @@
@@ -0,0 +1,30 @@
|
||||
{ |
||||
"name": "wiki-list-editor", |
||||
"version": "0.8.13", |
||||
"description": "Simple editor of lists", |
||||
"author": "Peter Stuifzand <peter@p83.nl>", |
||||
"main": "index.js", |
||||
"scripts": { |
||||
"test": "jasmine --require=esm" |
||||
}, |
||||
"keywords": [], |
||||
"license": "ISC", |
||||
"dependencies": { |
||||
"dragula": "^3.7.2", |
||||
"he": "^1.2.0", |
||||
"jquery": "^3.5.1", |
||||
"lodash": "^4.17.19" |
||||
}, |
||||
"devDependencies": { |
||||
"esm": "^3.2.25", |
||||
"jasmine": "^3.5.0" |
||||
}, |
||||
"repository": { |
||||
"type": "git", |
||||
"url": "git+ssh://git@github.com/pstuifzand/list-editor.git" |
||||
}, |
||||
"bugs": { |
||||
"url": "https://github.com/pstuifzand/list-editor/issues" |
||||
}, |
||||
"homepage": "https://github.com/pstuifzand/list-editor#readme" |
||||
} |
@ -0,0 +1,69 @@
@@ -0,0 +1,69 @@
|
||||
// consecutive for now
|
||||
function createSelection() { |
||||
let first = -1; |
||||
let last = -1; |
||||
|
||||
return { |
||||
reset() { |
||||
first = -1 |
||||
last = -1 |
||||
}, |
||||
|
||||
/** |
||||
* @param {int} f |
||||
* @param {Store} store |
||||
*/ |
||||
selectOne(f, store) { |
||||
if (f > last) { |
||||
last = first |
||||
} |
||||
first = f |
||||
last = first + 1 |
||||
}, |
||||
|
||||
selectNothing(f) { |
||||
first = f |
||||
last = f |
||||
}, |
||||
|
||||
remove(store) { |
||||
store.remove(first, last - first) |
||||
}, |
||||
|
||||
hasSelection() { |
||||
return first >= 0 && last >= 0 && first !== last |
||||
}, |
||||
|
||||
isSelected(line) { |
||||
return first >= 0 && last >= 0 && line >= first && line < last |
||||
}, |
||||
|
||||
isSelectedFirst(line) { |
||||
return first >= 0 && last >= 0 && line === first && line < last |
||||
}, |
||||
|
||||
isSelectedLast(line) { |
||||
return first >= 0 && last >= 0 && line >= first && line < last && line + 1 === last |
||||
}, |
||||
|
||||
indent(store, dir) { |
||||
store.indent(first, last - first, dir) |
||||
}, |
||||
|
||||
/** |
||||
* @param {int} index |
||||
* @param store |
||||
*/ |
||||
include(index, store) { |
||||
first = Math.min(first, index) |
||||
last = Math.max(last, index + 1); |
||||
}, |
||||
|
||||
debug() { |
||||
return {first, last} |
||||
} |
||||
}; |
||||
} |
||||
|
||||
|
||||
export default createSelection |
@ -0,0 +1,50 @@
@@ -0,0 +1,50 @@
|
||||
import createCursor from '../cursor' |
||||
import createStore from '../store' |
||||
|
||||
describe("A cursor", function() { |
||||
beforeEach(function() { |
||||
this.cursor = createCursor(0) |
||||
}) |
||||
|
||||
it("contains a get method", function() { |
||||
expect(this.cursor.get()).toBe(0) |
||||
}) |
||||
|
||||
it("contains a set method", function() { |
||||
this.cursor.set(4) |
||||
expect(this.cursor.get()).toBe(4) |
||||
}) |
||||
|
||||
it("contains an atFirst method", function() { |
||||
expect(this.cursor.atFirst()).toBe(true) |
||||
}) |
||||
|
||||
describe("with a store", function () { |
||||
beforeEach(function () { |
||||
this.store = createStore([]) |
||||
}) |
||||
|
||||
it("contains an atEnd method", function () { |
||||
this.cursor.set(0) |
||||
expect(this.cursor.atEnd(this.store)).toBe(true) |
||||
}) |
||||
}) |
||||
|
||||
describe("with a store", function () { |
||||
beforeEach(function () { |
||||
this.store = createStore([ |
||||
{indented:0, fold: 'open'}, |
||||
{indented:1, fold: 'open'}, |
||||
{indented:2, fold: 'open'}, |
||||
{indented:3, fold: 'open'}, |
||||
{indented:1, fold: 'open'}, |
||||
]) |
||||
}) |
||||
|
||||
it("moveUp moves up by one", function() { |
||||
this.cursor.set(4) |
||||
this.cursor.moveUp(this.store) |
||||
expect(this.cursor.get()).toBe(3) |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,163 @@
@@ -0,0 +1,163 @@
|
||||
import createStore from '../store' |
||||
|
||||
describe("A store", function () { |
||||
beforeEach(function () { |
||||
this.store = createStore([ |
||||
{text: "Hello", id: "_a", indented: 0} |
||||
]) |
||||
}) |
||||
|
||||
it("contains a length method", function () { |
||||
expect(this.store.length()).toBe(1) |
||||
}) |
||||
|
||||
it("contains an append method", function () { |
||||
this.store.append({text: "1"}) |
||||
expect(this.store.length()).toBe(2) |
||||
}) |
||||
|
||||
it("contains an nextCursorPosition method", function () { |
||||
this.store.append({text: "1"}) |
||||
expect(this.store.length()).toBe(2) |
||||
}) |
||||
|
||||
it("contains an indent method", function () { |
||||
this.store.append({text: "1", indented: 0}) |
||||
this.store.append({text: "2", indented: 1}) |
||||
this.store.append({text: "3", indented: 2}) |
||||
this.store.indent(1, 3, 1) |
||||
|
||||
expect(this.store.value(this.store.currentID(0)).indented).toBe(0) |
||||
expect(this.store.value(this.store.currentID(1)).indented).toBe(1) |
||||
expect(this.store.value(this.store.currentID(2)).indented).toBe(2) |
||||
expect(this.store.value(this.store.currentID(3)).indented).toBe(3) |
||||
}) |
||||
|
||||
it("contains a lastHigherIndented method", function () { |
||||
this.store.append({text: "1", indented: 0}) |
||||
this.store.append({text: "2", indented: 1}) |
||||
this.store.append({text: "3", indented: 2}) |
||||
this.store.append({text: "3", indented: 0}) |
||||
|
||||
expect(this.store.lastHigherIndented(1)).toBe(4) |
||||
}) |
||||
|
||||
it("contains a tree method that returns same indent items as a list", function () { |
||||
let store = createStore([ |
||||
{text: "a", id: "_a", indented: 0}, |
||||
{text: "b", id: "_b", indented: 0}, |
||||
{text: "c", id: "_c", indented: 0}, |
||||
{text: "d", id: "_d", indented: 0} |
||||
]) |
||||
let tree = store.tree() |
||||
expect(tree).toEqual([ |
||||
{text: "a", id: "_a", indented: 0}, |
||||
{text: "b", id: "_b", indented: 0}, |
||||
{text: "c", id: "_c", indented: 0}, |
||||
{text: "d", id: "_d", indented: 0} |
||||
]) |
||||
}) |
||||
|
||||
it("contains a tree method that returns children as a list in children", function () { |
||||
let store = createStore([ |
||||
{text: "a", id: "_a", indented: 0}, |
||||
{text: "b", id: "_b", indented: 1}, |
||||
{text: "c", id: "_c", indented: 1}, |
||||
{text: "d", id: "_d", indented: 1} |
||||
]) |
||||
let tree = store.tree() |
||||
expect(tree).toEqual([ |
||||
{ |
||||
text: "a", id: "_a", indented: 0, children: [ |
||||
{text: "b", id: "_b", indented: 1}, |
||||
{text: "c", id: "_c", indented: 1}, |
||||
{text: "d", id: "_d", indented: 1} |
||||
] |
||||
}, |
||||
]) |
||||
}) |
||||
|
||||
it("contains a tree method that returns multiple children as a list in children", function () { |
||||
let store = createStore([ |
||||
{text: "a", id: "_a", indented: 0}, |
||||
{text: "b", id: "_b", indented: 1}, |
||||
{text: "c", id: "_c", indented: 0}, |
||||
{text: "d", id: "_d", indented: 1} |
||||
]) |
||||
let tree = store.tree() |
||||
expect(tree).toEqual([ |
||||
{ |
||||
text: "a", id: "_a", indented: 0, children: [ |
||||
{text: "b", id: "_b", indented: 1}, |
||||
] |
||||
}, |
||||
{ |
||||
text: "c", id: "_c", indented: 0, children: [ |
||||
{text: "d", id: "_d", indented: 1} |
||||
] |
||||
}, |
||||
]) |
||||
}) |
||||
|
||||
it("contains a tree method that transform 0,1,2", function () { |
||||
let store = createStore([ |
||||
{text: "a", id: "_a", indented: 0}, |
||||
{text: "b", id: "_b" |