Browse Source

Include list-editor

master
Peter Stuifzand 10 months ago
parent
commit
08eca99c16
  1. 1
      .gitignore
  2. 84
      editor/package-lock.json
  3. 29
      editor/package.json
  4. 19
      editor/src/index.js
  5. 128
      list-editor/CHANGELOG.md
  6. 25
      list-editor/README.md
  7. 58
      list-editor/cursor.js
  8. 500
      list-editor/index.js
  9. 174
      list-editor/package-lock.json
  10. 30
      list-editor/package.json
  11. 69
      list-editor/selection.js
  12. 50
      list-editor/spec/cursor.spec.js
  13. 163
      list-editor/spec/store.spec.js
  14. 11
      list-editor/spec/support/jasmine.json
  15. 7
      list-editor/spec/test.spec.js
  16. 393
      list-editor/store.js
  17. 51
      list-editor/textarea.autosize.js

1
.gitignore

@ -6,3 +6,4 @@ editor/node_modules/*
dist/
_links.json
_documents.json
list-editor/node_modules

84
editor/package-lock.json

@ -1454,11 +1454,11 @@
}
},
"crossvent": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/crossvent/-/crossvent-1.5.4.tgz",
"integrity": "sha1-2ixPj0DJR4JRe/K+7BBEFIGUq5I=",
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/crossvent/-/crossvent-1.5.5.tgz",
"integrity": "sha1-rSCHjkkh6b5z2daXb4suzQ9xoLE=",
"requires": {
"custom-event": "1.0.0"
"custom-event": "^1.0.0"
}
},
"crypto-browserify": {
@ -1555,9 +1555,9 @@
}
},
"custom-event": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.0.tgz",
"integrity": "sha1-LkYovhncSyFLXAJjDFlx6BFhgGI="
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz",
"integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU="
},
"cyclist": {
"version": "1.0.1",
@ -2129,12 +2129,12 @@
}
},
"dragula": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/dragula/-/dragula-3.7.2.tgz",
"integrity": "sha1-SjXJ05gf+sGpScKcpyhQWOhzk84=",
"version": "3.7.3",
"resolved": "https://registry.npmjs.org/dragula/-/dragula-3.7.3.tgz",
"integrity": "sha512-/rRg4zRhcpf81TyDhaHLtXt6sEywdfpv1cRUMeFFy7DuypH2U0WUL0GTdyAQvXegviT4PJK4KuMmOaIDpICseQ==",
"requires": {
"contra": "1.9.4",
"crossvent": "1.5.4"
"crossvent": "1.5.5"
}
},
"duplexify": {
@ -9317,14 +9317,70 @@
}
},
"wiki-list-editor": {
"version": "0.8.12",
"resolved": "https://registry.npmjs.org/wiki-list-editor/-/wiki-list-editor-0.8.12.tgz",
"integrity": "sha512-1YdtzQv38WdbtyGFV9DAO5bJi7ndcyQlFXG64aJRimbguu6x1YRP8dzGiStGKfoVqZpR5zZ8Q+7HrqsxKVb24g==",
"version": "file:../list-editor",
"requires": {
"dragula": "^3.7.2",
"he": "^1.2.0",
"jquery": "^3.5.1",
"lodash": "^4.17.19"
},
"dependencies": {
"atoa": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/atoa/-/atoa-1.0.0.tgz",
"integrity": "sha1-DMDpGkgOc4+SPrwQNnZHF3mzSkk="
},
"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.5",
"resolved": "https://registry.npmjs.org/crossvent/-/crossvent-1.5.5.tgz",
"integrity": "sha1-rSCHjkkh6b5z2daXb4suzQ9xoLE=",
"requires": {
"custom-event": "^1.0.0"
}
},
"custom-event": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz",
"integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU="
},
"dragula": {
"version": "3.7.3",
"resolved": "https://registry.npmjs.org/dragula/-/dragula-3.7.3.tgz",
"integrity": "sha512-/rRg4zRhcpf81TyDhaHLtXt6sEywdfpv1cRUMeFFy7DuypH2U0WUL0GTdyAQvXegviT4PJK4KuMmOaIDpICseQ==",
"requires": {
"contra": "1.9.4",
"crossvent": "1.5.5"
}
},
"he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
},
"jquery": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz",
"integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg=="
},
"lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
},
"ticky": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ticky/-/ticky-1.0.1.tgz",
"integrity": "sha1-t8+nHnaPHJAAxJe5FRswlHxQ5G0="
}
}
},
"window-size": {

29
editor/package.json

@ -1,16 +1,4 @@
{
"devDependencies": {
"clean-webpack-plugin": "^3.0.0",
"esm": "^3.2.25",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.9.0",
"mocha": "^7.2.0",
"mocha-webpack": "^1.1.0",
"scss-loader": "0.0.1",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.7",
"webpack-dev-server": "^3.11.0"
},
"dependencies": {
"@egjs/hammerjs": "^2.0.17",
"axios": "^0.19.0",
@ -18,9 +6,12 @@
"clipboard": "^2.0.6",
"copy-text-to-clipboard": "^2.2.0",
"css-loader": "^3.2.0",
"dragula": "^3.7.3",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^6.0.0",
"fuse.js": "^6.0.0",
"he": "^1.2.0",
"jquery": "^3.5.1",
"jquery-contextmenu": "^2.9.2",
"keycharm": "^0.3.1",
"lodash": ">=4.17.19",
@ -42,7 +33,19 @@
"vis-data": "^6.6.1",
"vis-network": "^7.6.10",
"vis-util": "^4.3.2",
"wiki-list-editor": "^0.8.12"
"wiki-list-editor": "file:../list-editor"
},
"devDependencies": {
"clean-webpack-plugin": "^3.0.0",
"esm": "^3.2.25",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.9.0",
"mocha": "^7.2.0",
"mocha-webpack": "^1.1.0",
"scss-loader": "0.0.1",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.7",
"webpack-dev-server": "^3.11.0"
},
"scripts": {
"test": "node_modules/.bin/mocha -r esm",

19
editor/src/index.js

@ -67,7 +67,7 @@ function Indicator(element, timeout) {
return {
done() {
timeoutId = setTimeout(() => {
element.classList.add('hidden')
element.classList.add('hide')
}, timeout * 1000);
},
@ -77,7 +77,7 @@ function Indicator(element, timeout) {
timeoutId = null;
}
element.innerText = text;
element.classList.remove('hidden')
element.classList.remove('hide')
},
}
}
@ -216,9 +216,8 @@ MD.use(MarkdownItWikilinks({
let holders = document.getElementsByClassName('wiki-list-editor');
_.forEach(holders, (item, i) => {
console.log(i, item)
let EDITOR = new Editor(item)
_.forEach(holders, async (item, i) => {
new Editor(item).then(editor => editor.start());
})
function Editor(holder) {
@ -291,7 +290,9 @@ function Editor(holder) {
renderGraphs();
})
createPageSearch().then(function ({titleSearch, commandSearch, commands}) {
menu.connectContextMenu(editor)
return createPageSearch().then(function ({titleSearch, commandSearch, commands}) {
editor.on('start-editing', function (input) {
const $lc = $('#link-complete');
@ -471,7 +472,6 @@ function Editor(holder) {
}
})
})
editor.on('stop-editing', function (input) {
$(input).parents('.list-item').removeClass('active');
$('#link-complete').off()
@ -479,11 +479,8 @@ function Editor(holder) {
mermaid.init()
renderGraphs();
})
return editor
})
menu.connectContextMenu(editor)
return editor
}
let timeout = null;

128
list-editor/CHANGELOG.md

@ -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

25
list-editor/README.md

@ -0,0 +1,25 @@
![Node.js Package](https://github.com/pstuifzand/list-editor/workflows/Node.js%20Package/badge.svg)
# 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>

58
list-editor/cursor.js

@ -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;

500
list-editor/index.js

@ -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">&#9654;</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

174
list-editor/package-lock.json

@ -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
}
}
}

30
list-editor/package.json

@ -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"
}

69
list-editor/selection.js

@ -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

50
list-editor/spec/cursor.spec.js

@ -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)
})
})
})

163
list-editor/spec/store.spec.js

@ -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", indented: 1},
{text: "c", id: "_c", indented: 2},
])
let tree = store.tree()
expect(tree).toEqual([
{
text: "a", id: "_a", indented: 0, children: [
{
text: "b", id: "_b", indented: 1, children: [
{text: "c", id: "_c", indented: 2}]
}
]
},
])
})
it("contains a tree method that returns deeper 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: 2},
{text: "d", id: "_d", indented: 0},
])
let tree = store.tree()
expect(tree).toEqual([
{
text: "a", id: "_a", indented: 0, children: [
{
text: "b", id: "_b", indented: 1, children: [
{text: "c", id: "_c", indented: 2},
]
},
]
},
{text: "d", id: "_d", indented: 0}
])
})
it("contains a tree method that accepts a from argument", function () {
let store = createStore([
{text: "a", id: "_a", indented: 0},
{text: "b", id: "_b", indented: 1},
{text: "c", id: "_c", indented: 2},
{text: "d", id: "_d", indented: 0},
])
let tree = store.tree("_a")
expect(tree).toEqual([
{
text: "a", id: "_a", indented: 0, children: [
{
text: "b", id: "_b", indented: 1, children: [
{text: "c", id: "_c", indented: 2},
]
},
]
},
])
})
})

11
list-editor/spec/support/jasmine.json

@ -0,0 +1,11 @@
{
"spec_dir": "spec",
"spec_files": [
"**/*[sS]pec.js"
],
"helpers": [
"helpers/**/*.js"
],
"stopSpecOnExpectationFailure": false,
"random": true
}

7
list-editor/spec/test.spec.js

@ -0,0 +1,7 @@
describe("A suite", function() {
it("contains spec with an expectation", function() {
expect(true).toBe(true);
});
});

393
list-editor/store.js

@ -0,0 +1,393 @@
import _ from 'lodash';
/**
* NOTE: Store should contain all methods that work with items. At the moment
* there are still a few places where we change the items from the outside,
* while it's very important that the items behave a certain way.
*/
function Store(inputData) {
let idList = [];
let values = {};
let ID = function () {
return '_' + Math.random().toString(36).substr(2, 12);
};
/**
* @param {int} index
* @returns {string}
*/
function currentID(index) {
if (index === idList.length) {
return 'at-end'
}
return idList[index];
}
/**
* @param {string} id
* @returns {number}
*/
function index(id) {
return _.findIndex(idList, value => value === id)
}
/**
* @param {string} id
* @return {object}
*/
function value(id) {
return values[id];
}
/**
* @param {string} afterId
* @return {object}
*/
function afterValue(afterId) {
let i = index(afterId)
return values[idList[i + 1]]
}
function prevCursorPosition(cursor) {
let curIndent = values[idList[cursor]].indented
let curClosed = values[idList[cursor]].fold !== 'open';
if (!curClosed) {
curIndent = 10000000;
}
let moving = true
while (moving) {
cursor--
if (cursor < 0) {
cursor = idList.length - 1
curIndent = values[idList[cursor]].indented
}
let next = values[idList[cursor]];
if (curIndent >= next.indented && !next.hidden) {
moving = false
}
}
return cursor
}
/**
* Find the next 'open' position in the list.
*
* @param {number} cursor
* @param {bool} wrap
* @returns {number}
*/
function nextCursorPosition(cursor, wrap) {
let curIndent = values[idList[cursor]].indented
let curClosed = values[idList[cursor]].fold !== 'open';
if (!curClosed) {
curIndent = 10000000;
}
let moving = true
while (moving) {
cursor++
if (wrap) {
if (cursor >= idList.length) {