Include list-editor
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
parent
643ec5e98c
commit
08eca99c16
@ -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 @@
|
||||

|
||||
|
||||
# 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 @@
|
||||
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 @@
|
||||
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 @@
|
||||
{
|
||||
"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 @@
|
||||
{
|
||||
"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 @@
|
||||
// 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 @@
|
||||
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 @@
|
||||
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},
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
@ -0,0 +1,11 @@
|
||||
{
|
||||
"spec_dir": "spec",
|
||||
"spec_files": [
|
||||
"**/*[sS]pec.js"
|
||||
],
|
||||
"helpers": [
|
||||
"helpers/**/*.js"
|
||||
],
|
||||
"stopSpecOnExpectationFailure": false,
|
||||
"random": true
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
|
||||
|
||||
describe("A suite", function() {
|
||||
it("contains spec with an expectation", function() {
|
||||