2023-09-27 15:05:34 +03:00
|
|
|
/**********************************************************************
|
|
|
|
|
*
|
|
|
|
|
*
|
|
|
|
|
*
|
|
|
|
|
**********************************************************************/
|
|
|
|
|
|
|
|
|
|
|
2023-09-28 20:49:06 +03:00
|
|
|
//---------------------------------------------------------------------
|
2023-10-19 15:35:42 +03:00
|
|
|
// Helpers...
|
2023-09-28 20:49:06 +03:00
|
|
|
|
2023-10-14 22:49:02 +03:00
|
|
|
/*
|
|
|
|
|
function clickPoint(x,y){
|
|
|
|
|
document
|
|
|
|
|
.elementFromPoint(x, y)
|
|
|
|
|
.dispatchEvent(
|
|
|
|
|
new MouseEvent( 'click', {
|
|
|
|
|
view: window,
|
|
|
|
|
bubbles: true,
|
|
|
|
|
cancelable: true,
|
|
|
|
|
screenX: x,
|
|
|
|
|
screenY: y,
|
|
|
|
|
} )) }
|
|
|
|
|
//*/
|
|
|
|
|
|
2023-10-04 15:33:07 +03:00
|
|
|
|
2023-10-19 00:08:21 +03:00
|
|
|
// Get the character offset at coordinates...
|
|
|
|
|
//
|
|
|
|
|
// This is done by moving a range down the element until its bounding
|
|
|
|
|
// box corresponds the to desired coordinates. This accounts for nested
|
|
|
|
|
// elements.
|
|
|
|
|
//
|
2023-10-19 15:35:42 +03:00
|
|
|
// XXX it would be a better idea to do a binary search instead of a liner
|
2023-10-30 04:00:05 +03:00
|
|
|
// pass...
|
|
|
|
|
// ...though b-search will get us to the target, we stll need to count...
|
2023-10-19 00:08:21 +03:00
|
|
|
// XXX HACK -- is there a better way to do this???
|
2023-10-18 23:30:15 +03:00
|
|
|
var getCharOffset = function(elem, x, y, c){
|
|
|
|
|
c = c ?? 0
|
|
|
|
|
var r = document.createRange()
|
|
|
|
|
for(var e of [...elem.childNodes]){
|
2023-10-19 02:36:08 +03:00
|
|
|
// text node...
|
2023-10-18 23:30:15 +03:00
|
|
|
if(e instanceof Text){
|
2023-10-19 15:35:42 +03:00
|
|
|
var prev, b
|
|
|
|
|
for(var i=0; i <= e.length; i++){
|
2023-10-18 23:30:15 +03:00
|
|
|
r.setStart(e, i)
|
|
|
|
|
r.setEnd(e, i)
|
2023-10-19 15:35:42 +03:00
|
|
|
prev = b
|
|
|
|
|
b = r.getBoundingClientRect()
|
2023-10-18 23:30:15 +03:00
|
|
|
// found target...
|
|
|
|
|
if(b.x >= x
|
|
|
|
|
&& b.y <= y
|
|
|
|
|
&& b.bottom >= y){
|
2023-10-19 15:35:42 +03:00
|
|
|
// get the closest gap between chars to the click...
|
|
|
|
|
return Math.abs(b.x - x) <= Math.abs(prev.x - x) ?
|
|
|
|
|
c + i
|
|
|
|
|
: c + i - 1 } }
|
2023-10-30 03:46:07 +03:00
|
|
|
c += i - 1
|
2023-10-19 02:36:08 +03:00
|
|
|
// html node...
|
2023-10-18 23:30:15 +03:00
|
|
|
} else {
|
|
|
|
|
var res = getCharOffset(e, x, y, c)
|
|
|
|
|
if(!(res instanceof Array)){
|
|
|
|
|
return res }
|
|
|
|
|
;[c, res] = res } }
|
2023-10-19 02:36:08 +03:00
|
|
|
// no result was found...
|
2023-10-18 23:30:15 +03:00
|
|
|
return arguments.length > 3 ?
|
|
|
|
|
[c, null]
|
|
|
|
|
: null }
|
2023-10-19 00:08:21 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
// Get offset in markdown relative to the resulting text...
|
|
|
|
|
//
|
|
|
|
|
// v <----- position
|
|
|
|
|
// text: 'Hea|ding'
|
|
|
|
|
// |
|
|
|
|
|
// +-+ <--- offset in markdown
|
|
|
|
|
// |
|
|
|
|
|
// markdown: '# Hea|ding'
|
|
|
|
|
//
|
2023-10-18 23:30:15 +03:00
|
|
|
var getMarkdownOffset = function(markdown, text, i){
|
|
|
|
|
i = i ?? text.length
|
|
|
|
|
var m = 0
|
|
|
|
|
// walk both strings skipping/counting non-matching stuff...
|
2023-10-19 15:07:51 +03:00
|
|
|
for(var n=0; n <= i; n++, m++){
|
2023-10-18 23:30:15 +03:00
|
|
|
var c = text[n]
|
2023-10-19 00:08:21 +03:00
|
|
|
var p = m
|
|
|
|
|
// walk to next match...
|
|
|
|
|
while(c != markdown[m] && m < markdown.length){
|
|
|
|
|
m++ }
|
|
|
|
|
// reached something unrepresentable directly in markdown (html
|
|
|
|
|
// entity, symbol, ...)
|
|
|
|
|
if(m >= markdown.length){
|
|
|
|
|
m = p } }
|
2023-10-18 23:30:15 +03:00
|
|
|
return m - n }
|
|
|
|
|
|
|
|
|
|
|
2023-09-27 15:05:34 +03:00
|
|
|
|
2023-10-13 22:14:36 +03:00
|
|
|
//---------------------------------------------------------------------
|
2023-10-14 15:46:42 +03:00
|
|
|
// Plugins...
|
2023-10-13 22:14:36 +03:00
|
|
|
|
2023-10-14 15:46:42 +03:00
|
|
|
// general helpers and utils...
|
2023-10-14 02:42:29 +03:00
|
|
|
var plugin = {
|
2023-10-14 15:46:42 +03:00
|
|
|
encode: function(text){
|
|
|
|
|
return text
|
|
|
|
|
.replace(/(?<!\\)&/g, '&')
|
|
|
|
|
.replace(/(?<!\\)</g, '<')
|
|
|
|
|
.replace(/(?<!\\)>/g, '>')
|
|
|
|
|
.replace(/\\(?!`)/g, '\\\\') },
|
|
|
|
|
|
2023-10-14 02:42:29 +03:00
|
|
|
// XXX make this more generic...
|
|
|
|
|
style: function(editor, elem, style, code=undefined){
|
|
|
|
|
style = [style].flat()
|
|
|
|
|
editor.__styles = [...new Set([
|
|
|
|
|
...(editor.__styles ?? []),
|
|
|
|
|
...style,
|
|
|
|
|
])]
|
|
|
|
|
return function(_, text){
|
|
|
|
|
elem.style ??= []
|
|
|
|
|
elem.style.push(...style)
|
|
|
|
|
return code
|
|
|
|
|
?? text } },
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
2023-10-22 23:28:04 +03:00
|
|
|
// XXX style attributes...
|
2023-10-14 02:42:29 +03:00
|
|
|
var attributes = {
|
|
|
|
|
__proto__: plugin,
|
|
|
|
|
|
2023-10-22 23:28:04 +03:00
|
|
|
__parse__: function(text, editor, elem){
|
|
|
|
|
var skip = new Set([
|
|
|
|
|
'text',
|
|
|
|
|
'focused',
|
|
|
|
|
'collapsed',
|
|
|
|
|
'id',
|
|
|
|
|
'children',
|
|
|
|
|
'style',
|
|
|
|
|
])
|
2023-10-14 02:42:29 +03:00
|
|
|
return text
|
2023-10-22 23:28:04 +03:00
|
|
|
+ Object.entries(elem)
|
|
|
|
|
.reduce(function(res, [key, value]){
|
|
|
|
|
return skip.has(key) ?
|
|
|
|
|
res
|
|
|
|
|
: res + `<br>${key}: ${value}` }, '') },
|
2023-10-14 02:42:29 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
|
|
var blocks = {
|
|
|
|
|
__proto__: plugin,
|
|
|
|
|
|
|
|
|
|
__pre_parse__: function(text, editor, elem){
|
|
|
|
|
return text
|
|
|
|
|
// markdown...
|
|
|
|
|
// style: headings...
|
|
|
|
|
.replace(/^(?<!\\)######\s+(.*)$/m, this.style(editor, elem, 'heading-6'))
|
|
|
|
|
.replace(/^(?<!\\)#####\s+(.*)$/m, this.style(editor, elem, 'heading-5'))
|
|
|
|
|
.replace(/^(?<!\\)####\s+(.*)$/m, this.style(editor, elem, 'heading-4'))
|
|
|
|
|
.replace(/^(?<!\\)###\s+(.*)$/m, this.style(editor, elem, 'heading-3'))
|
|
|
|
|
.replace(/^(?<!\\)##\s+(.*)$/m, this.style(editor, elem, 'heading-2'))
|
|
|
|
|
.replace(/^(?<!\\)#\s+(.*)$/m, this.style(editor, elem, 'heading-1'))
|
|
|
|
|
// style: list...
|
|
|
|
|
//.replace(/^(?<!\\)[-\*]\s+(.*)$/m, style('list-item'))
|
|
|
|
|
.replace(/^\s*(.*)(?<!\\):\s*$/m, this.style(editor, elem, 'list'))
|
|
|
|
|
.replace(/^\s*(.*)(?<!\\)#\s*$/m, this.style(editor, elem, 'numbered-list'))
|
|
|
|
|
// style: misc...
|
|
|
|
|
.replace(/^\s*(?<!\\)>\s+(.*)$/m, this.style(editor, elem, 'quote'))
|
|
|
|
|
.replace(/^\s*(?<!\\)((\/\/|;)\s+.*)$/m, this.style(editor, elem, 'comment'))
|
|
|
|
|
.replace(/^\s*(?<!\\)NOTE:?\s*(.*)$/m, this.style(editor, elem, 'NOTE'))
|
|
|
|
|
.replace(/^\s*(?<!\\)XXX\s+(.*)$/m, this.style(editor, elem, 'XXX'))
|
2023-10-26 21:46:14 +03:00
|
|
|
.replace(/^(.*)\s*(?<!\\)XXX$/m, this.style(editor, elem, 'XXX'))
|
|
|
|
|
.replace(/^\s*---\s*$/m, this.style(editor, elem, 'hr', '<hr>')) } ,
|
2023-10-14 02:42:29 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
|
|
// XXX add actions...
|
|
|
|
|
var quoted = {
|
|
|
|
|
__proto__: plugin,
|
|
|
|
|
|
2023-10-13 22:14:36 +03:00
|
|
|
// can be used in:
|
2023-10-14 02:42:29 +03:00
|
|
|
// <string>.replace(quoted.pattern, quoted.handler)
|
|
|
|
|
quote_pattern: /(?<!\\)`(?=[^\s])(([^`]|\\`)*[^\s])(?<!\\)`/gm,
|
|
|
|
|
quote: function(_, code){
|
|
|
|
|
return `<code>${ this.encode(code) }</code>` },
|
|
|
|
|
|
|
|
|
|
pre_pattern: /(?<!\\)```(.*\s*\n)((\n|.)*?)\h*(?<!\\)```/g,
|
|
|
|
|
pre: function(_, language, code){
|
2023-10-13 22:14:36 +03:00
|
|
|
language = language.trim()
|
|
|
|
|
language = language ?
|
|
|
|
|
'language-'+language
|
|
|
|
|
: language
|
|
|
|
|
return `<pre>`
|
|
|
|
|
+`<code contenteditable="true" class="${language}">${
|
2023-10-14 02:42:29 +03:00
|
|
|
this.encode(code)
|
2023-10-13 22:14:36 +03:00
|
|
|
}</code>`
|
|
|
|
|
+`</pre>` },
|
|
|
|
|
|
|
|
|
|
map: function(text, func){
|
2023-10-14 02:42:29 +03:00
|
|
|
return text.replace(this.pre_pattern, func) },
|
2023-10-13 22:14:36 +03:00
|
|
|
replace: function(text, index, updated){
|
|
|
|
|
return this.map(text,
|
|
|
|
|
function(match, language, code){
|
|
|
|
|
return index-- != 0 ?
|
|
|
|
|
match
|
|
|
|
|
: ('```'+language
|
|
|
|
|
+ (typeof(updated) == 'function' ?
|
|
|
|
|
updated(code)
|
|
|
|
|
: updated)
|
|
|
|
|
+'```') }) },
|
|
|
|
|
toHTML: function(text){
|
|
|
|
|
return this.map(text, this.handler) },
|
2023-10-14 02:42:29 +03:00
|
|
|
|
|
|
|
|
__pre_parse__: function(text, editor, elem){
|
|
|
|
|
return text
|
|
|
|
|
.replace(this.pre_pattern, this.pre.bind(this))
|
|
|
|
|
.replace(this.quote_pattern, this.quote.bind(this)) },
|
2023-10-14 22:49:02 +03:00
|
|
|
|
|
|
|
|
// XXX is this a good strategy???
|
|
|
|
|
__state: undefined,
|
|
|
|
|
__keydown__: function(evt, editor, elem){
|
|
|
|
|
// code editing...
|
|
|
|
|
if(elem.nodeName == 'CODE'
|
|
|
|
|
&& elem.getAttribute('contenteditable') == 'true'){
|
|
|
|
|
// XXX can keydown and keyup be triggered from different elements???
|
|
|
|
|
this.__state = elem.innerText
|
|
|
|
|
return false } },
|
|
|
|
|
// defined <plugin>.__editedview__(..) handler
|
|
|
|
|
__keyup__: function(evt, editor, elem){
|
|
|
|
|
var elem = evt.target
|
|
|
|
|
if(elem.nodeName == 'CODE'
|
|
|
|
|
&& elem.getAttribute('contenteditable') == 'true'){
|
|
|
|
|
// trigger if state actually changed..
|
|
|
|
|
this.__state != elem.innerText
|
|
|
|
|
&& editor.runPlugins('__editedview__', evt, editor, elem) } },
|
|
|
|
|
__focusout__: function(){
|
|
|
|
|
this.__state = undefined },
|
|
|
|
|
__editedview__: function(evt, editor, elem){
|
|
|
|
|
// editable code...
|
|
|
|
|
var block = editor.get(elem)
|
|
|
|
|
var code = block.querySelector('.code')
|
|
|
|
|
|
|
|
|
|
var update = elem.innerText
|
|
|
|
|
var i = [...block
|
|
|
|
|
.querySelectorAll('.view code[contenteditable=true]')]
|
|
|
|
|
.indexOf(elem)
|
|
|
|
|
// update element content...
|
|
|
|
|
code.value = quoted.replace(code.value, i, update)
|
|
|
|
|
|
|
|
|
|
return this },
|
2023-10-14 02:42:29 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2023-10-18 14:41:49 +03:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
|
|
var tasks = {
|
|
|
|
|
__proto__: plugin,
|
|
|
|
|
|
2023-10-30 18:06:13 +03:00
|
|
|
status: [
|
|
|
|
|
'DONE',
|
|
|
|
|
'REJECT',
|
2023-10-18 19:48:00 +03:00
|
|
|
],
|
2023-10-30 18:06:13 +03:00
|
|
|
// format:
|
|
|
|
|
// [
|
|
|
|
|
// <status>: <pattern>,
|
|
|
|
|
// ...
|
|
|
|
|
// ]
|
|
|
|
|
__status_patterns: undefined,
|
|
|
|
|
__status_pattern_tpl: `^(?:\\s*(?<!\\\\)$STATUS:?\\s+(.*)$|(.*)\\s+(?<!\\\\)$STATUS\\s*)$`,
|
|
|
|
|
get status_patterns(){
|
|
|
|
|
var that = this
|
|
|
|
|
return (this.__status_patterns
|
|
|
|
|
??= this.status
|
|
|
|
|
.reduce(function(res, status){
|
|
|
|
|
res[status] = new RegExp(
|
|
|
|
|
that.__status_pattern_tpl
|
|
|
|
|
.replace(/\$STATUS/g, status), 'm')
|
|
|
|
|
return res }, {})) },
|
2023-10-18 19:48:00 +03:00
|
|
|
|
2023-10-18 23:30:15 +03:00
|
|
|
// State...
|
2023-10-18 14:41:49 +03:00
|
|
|
updateStatus: function(editor, node){
|
|
|
|
|
node = editor.get(node)
|
|
|
|
|
if(node == null){
|
|
|
|
|
return this }
|
|
|
|
|
var state = node
|
|
|
|
|
.querySelector('.view')
|
|
|
|
|
.querySelector('.completion')
|
|
|
|
|
if(state){
|
|
|
|
|
var c =
|
|
|
|
|
((node.querySelectorAll('input[type=checkbox]:checked').length
|
|
|
|
|
/ node.querySelectorAll('input[type=checkbox]').length)
|
|
|
|
|
* 100)
|
|
|
|
|
.toFixed(0)
|
|
|
|
|
!isNaN(c)
|
|
|
|
|
&& state.setAttribute('completion', c +'%') }
|
|
|
|
|
return this },
|
2023-10-18 18:44:24 +03:00
|
|
|
updateBranchStatus: function(editor, node){
|
2023-10-18 14:41:49 +03:00
|
|
|
if(!node){
|
|
|
|
|
return this }
|
|
|
|
|
var outline = editor.outline
|
|
|
|
|
var p = node
|
|
|
|
|
while(p !== outline){
|
|
|
|
|
this.updateStatus(editor, p)
|
|
|
|
|
p = editor.get(p, 'parent') }
|
|
|
|
|
return this },
|
2023-10-18 18:44:24 +03:00
|
|
|
updateAllStatus: function(editor){
|
2023-10-18 14:41:49 +03:00
|
|
|
for(var e of [...editor.outline.querySelectorAll('.block>.view .completion')]){
|
|
|
|
|
this.updateStatus(editor, e) }
|
|
|
|
|
return this },
|
2023-10-18 23:30:15 +03:00
|
|
|
// Checkboxes...
|
2023-10-18 18:40:04 +03:00
|
|
|
getCheckbox: function(editor, elem, offset=0){
|
|
|
|
|
elem = elem
|
|
|
|
|
?? editor.get()
|
|
|
|
|
if(elem == null
|
|
|
|
|
|| (offset == 0
|
|
|
|
|
&& elem.type == 'checkbox')){
|
|
|
|
|
return elem }
|
|
|
|
|
var node = editor.get(elem)
|
|
|
|
|
var view = node.querySelector('.view')
|
|
|
|
|
var cur = view.querySelector('input[type=checkbox].selected')
|
|
|
|
|
?? view.querySelector('input[type=checkbox]')
|
|
|
|
|
if(offset == 0 && cur == null){
|
|
|
|
|
return}
|
|
|
|
|
var checkboxes = [...editor.outline.querySelectorAll('.view input[type=checkbox]')]
|
|
|
|
|
if(checkboxes.length == 0){
|
|
|
|
|
return }
|
|
|
|
|
// no checkbox in node -> get closest to cur in offset direction...
|
|
|
|
|
if(cur == null){
|
|
|
|
|
var nodes = [...editor.outline.querySelectorAll('.block')]
|
|
|
|
|
var checkbox_nodes = checkboxes
|
|
|
|
|
.map(function(e){
|
|
|
|
|
return editor.get(e) })
|
|
|
|
|
var i = nodes.indexOf(node)
|
|
|
|
|
var p, n
|
|
|
|
|
for(var c of checkbox_nodes){
|
|
|
|
|
p = n
|
|
|
|
|
var j = nodes.indexOf(c)
|
|
|
|
|
if(j >= i){
|
|
|
|
|
n = j
|
|
|
|
|
break } }
|
|
|
|
|
cur = offset < 0 ?
|
|
|
|
|
nodes[p]
|
|
|
|
|
: nodes[n] }
|
|
|
|
|
var elem = cur == null ?
|
|
|
|
|
checkboxes.at(
|
|
|
|
|
offset > 0 ?
|
|
|
|
|
offset -1
|
|
|
|
|
: offset)
|
|
|
|
|
: checkboxes.at(
|
|
|
|
|
(checkboxes.indexOf(cur) + offset) % checkboxes.length)
|
|
|
|
|
return elem },
|
2023-10-18 18:44:24 +03:00
|
|
|
updateCheckboxes: function(editor, elem){
|
2023-10-18 18:40:04 +03:00
|
|
|
elem = this.getCheckbox(editor, elem)
|
2023-10-26 18:11:12 +03:00
|
|
|
var node = editor.get(elem, false)
|
|
|
|
|
var data = editor.data(node)
|
2023-10-18 18:40:04 +03:00
|
|
|
var text = node.querySelector('.code')
|
|
|
|
|
// get the checkbox order...
|
|
|
|
|
var i = [...node.querySelectorAll('input[type=checkbox]')].indexOf(elem)
|
|
|
|
|
var to = elem.checked ?
|
|
|
|
|
'[X]'
|
|
|
|
|
: '[_]'
|
|
|
|
|
var toggle = function(m){
|
|
|
|
|
return i-- == 0 ?
|
|
|
|
|
to
|
|
|
|
|
: m }
|
|
|
|
|
text.value = text.value.replace(/\[[Xx_]\]/g, toggle)
|
2023-10-26 18:11:12 +03:00
|
|
|
// NOTE: status is updated via a timeout set in .__parse__(..)...
|
|
|
|
|
editor.setUndo(
|
|
|
|
|
editor.path(node),
|
|
|
|
|
'update',
|
|
|
|
|
[editor.path(node),
|
|
|
|
|
data])
|
2023-10-18 18:40:04 +03:00
|
|
|
return elem },
|
|
|
|
|
toggleCheckbox: function(editor, checkbox, offset){
|
|
|
|
|
checkbox = this.getCheckbox(editor, checkbox, offset)
|
|
|
|
|
if(checkbox){
|
|
|
|
|
checkbox.checked = !checkbox.checked
|
2023-10-18 18:44:24 +03:00
|
|
|
this.updateCheckboxes(editor, checkbox)
|
|
|
|
|
this.updateBranchStatus(editor, checkbox) }
|
2023-10-18 18:40:04 +03:00
|
|
|
return checkbox },
|
|
|
|
|
selectCheckbox: function(editor, checkbox, offset){
|
|
|
|
|
checkbox = this.getCheckbox(editor, checkbox, offset)
|
|
|
|
|
if(checkbox == null){
|
|
|
|
|
return }
|
|
|
|
|
var checkboxes = editor.get(checkbox)
|
|
|
|
|
.querySelector('.view')
|
|
|
|
|
.querySelectorAll('input[type=checkbox]')
|
|
|
|
|
if(checkboxes.length == 0){
|
|
|
|
|
return }
|
|
|
|
|
for(var c of checkboxes){
|
|
|
|
|
c.classList.remove('selected') }
|
|
|
|
|
checkbox.classList.add('selected')
|
|
|
|
|
editor.show(checkbox)
|
|
|
|
|
return checkbox },
|
|
|
|
|
nextCheckbox: function(editor, node='focused', offset=1){
|
|
|
|
|
node = this.selectCheckbox(editor, node, offset)
|
|
|
|
|
editor.focus(node)
|
|
|
|
|
return node },
|
|
|
|
|
prevCheckbox: function(editor, node='focused', offset=-1){
|
|
|
|
|
return this.nextCheckbox(editor, node, offset) },
|
2023-10-30 18:06:13 +03:00
|
|
|
// Status...
|
|
|
|
|
toggleStatus: function(editor, elem, status='next', patterns=this.status_patterns){
|
2023-10-18 19:48:00 +03:00
|
|
|
var node = editor.get(elem)
|
|
|
|
|
if(node == null){
|
|
|
|
|
return }
|
2023-10-26 18:11:12 +03:00
|
|
|
var data = editor.data(elem, false)
|
2023-10-18 19:48:00 +03:00
|
|
|
var text = node.querySelector('.code')
|
|
|
|
|
var value = text.value
|
|
|
|
|
var s = text.selectionStart
|
|
|
|
|
var e = text.selectionEnd
|
|
|
|
|
var l = text.value.length
|
2023-10-30 18:06:13 +03:00
|
|
|
|
|
|
|
|
var p = Object.entries(patterns)
|
|
|
|
|
for(var i=0; i<p.length; i++){
|
|
|
|
|
var [name, pattern] = p[i]
|
|
|
|
|
if(pattern.test(value)){
|
|
|
|
|
value = value.replace(pattern, '$1')
|
|
|
|
|
if(status != 'off'){
|
|
|
|
|
break } } }
|
|
|
|
|
if(status != 'off'){
|
|
|
|
|
// toggle specific status...
|
|
|
|
|
if(status != 'next'){
|
|
|
|
|
if(i == p.length
|
|
|
|
|
|| name != status){
|
|
|
|
|
value = status +' '+ value }
|
|
|
|
|
// next...
|
|
|
|
|
} else if(i != p.length-1){
|
|
|
|
|
// set next...
|
|
|
|
|
if(i+1 in p){
|
|
|
|
|
value = p[i+1][0] +' '+ value
|
|
|
|
|
// set first...
|
|
|
|
|
} else {
|
|
|
|
|
value = p[0][0] +' '+ value } } }
|
|
|
|
|
|
2023-10-18 19:48:00 +03:00
|
|
|
text.value = value
|
|
|
|
|
text.selectionStart = s + (value.length - l)
|
|
|
|
|
text.selectionEnd = e + (value.length - l)
|
|
|
|
|
editor.update(node)
|
2023-10-26 18:11:12 +03:00
|
|
|
editor.setUndo(
|
|
|
|
|
editor.path(node),
|
|
|
|
|
'update',
|
|
|
|
|
[editor.path(node),
|
|
|
|
|
data])
|
2023-10-18 19:48:00 +03:00
|
|
|
return node },
|
2023-10-30 18:06:13 +03:00
|
|
|
toggleDone: function(editor, elem){
|
|
|
|
|
return this.toggleStatus(editor, elem, 'DONE') },
|
|
|
|
|
toggleReject: function(editor, elem){
|
|
|
|
|
return this.toggleStatus(editor, elem, 'REJECT') },
|
2023-10-18 19:48:00 +03:00
|
|
|
|
2023-10-18 14:41:49 +03:00
|
|
|
__setup__: function(editor){
|
2023-10-18 18:44:24 +03:00
|
|
|
return this.updateAllStatus(editor) },
|
2023-10-18 20:04:39 +03:00
|
|
|
__pre_parse__: function(text, editor, elem){
|
2023-10-18 19:48:00 +03:00
|
|
|
// handle done..
|
2023-10-30 18:06:13 +03:00
|
|
|
var done = this.style(editor, elem, 'DONE')
|
|
|
|
|
var reject = this.style(editor, elem, 'REJECT')
|
|
|
|
|
for(var [n, p] of Object.entries(this.status_patterns)){
|
2023-10-18 20:04:39 +03:00
|
|
|
text = text
|
2023-10-30 18:06:13 +03:00
|
|
|
.replace(p,
|
|
|
|
|
n == 'DONE' ?
|
|
|
|
|
done
|
|
|
|
|
: reject) }
|
2023-10-18 20:04:39 +03:00
|
|
|
return text },
|
2023-10-26 18:11:12 +03:00
|
|
|
__update_checkboxes_timeout: undefined,
|
2023-10-18 20:04:39 +03:00
|
|
|
__parse__: function(text, editor, elem){
|
2023-10-26 18:11:12 +03:00
|
|
|
var res = text
|
2023-10-18 14:41:49 +03:00
|
|
|
// block checkboxes...
|
|
|
|
|
// NOTE: these are separate as we need to align block text
|
|
|
|
|
// to leading chekbox...
|
|
|
|
|
.replace(/^\s*(?<!\\)\[[_ ]\]\s*/m,
|
|
|
|
|
this.style(editor, elem, 'todo', '<input type="checkbox">'))
|
|
|
|
|
.replace(/^\s*(?<!\\)\[[Xx]\]\s*/m,
|
|
|
|
|
this.style(editor, elem, 'todo', '<input type="checkbox" checked>'))
|
|
|
|
|
// inline checkboxes...
|
|
|
|
|
.replace(/\s*(?<!\\)\[[_ ]\]\s*/gm,
|
|
|
|
|
this.style(editor, elem, 'check', '<input type="checkbox">'))
|
|
|
|
|
.replace(/\s*(?<!\\)\[[Xx]\]\s*/gm,
|
|
|
|
|
this.style(editor, elem, 'check', '<input type="checkbox" checked>'))
|
|
|
|
|
// completion...
|
|
|
|
|
// XXX add support for being like a todo checkbox...
|
2023-10-26 18:11:12 +03:00
|
|
|
.replace(/(?<!\\)\[[%]\]/gm, '<span class="completion"></span>')
|
|
|
|
|
// need to update status...
|
|
|
|
|
// XXX not sure if this is a good way to do this...
|
|
|
|
|
if(res != text && this.__update_checkboxes_timeout == null){
|
|
|
|
|
var that = this
|
|
|
|
|
this.__update_checkboxes_timeout = setTimeout(function(){
|
|
|
|
|
that.__update_checkboxes_timeout = undefined
|
|
|
|
|
that.updateAllStatus(editor) }, 200) }
|
|
|
|
|
return res },
|
2023-10-18 18:40:04 +03:00
|
|
|
__focusin__: function(evt, editor, elem){
|
|
|
|
|
elem.classList.contains('block')
|
|
|
|
|
&& this.selectCheckbox(editor, elem) },
|
|
|
|
|
__editedcode__: function(evt, editor, elem){
|
2023-10-18 18:44:24 +03:00
|
|
|
this.updateBranchStatus(editor, elem)
|
2023-10-18 18:40:04 +03:00
|
|
|
this.selectCheckbox(editor, elem) },
|
2023-10-18 14:41:49 +03:00
|
|
|
__click__: function(evt, editor, elem){
|
|
|
|
|
// toggle checkbox...
|
|
|
|
|
if(elem.type == 'checkbox'){
|
|
|
|
|
var node = editor.get(elem)
|
2023-10-18 18:44:24 +03:00
|
|
|
this.updateCheckboxes(editor, elem)
|
|
|
|
|
this.updateBranchStatus(editor, node)
|
2023-10-18 18:40:04 +03:00
|
|
|
this.selectCheckbox(editor, elem)
|
|
|
|
|
node.focus() }
|
2023-10-18 14:41:49 +03:00
|
|
|
return this },
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2023-10-14 02:42:29 +03:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
|
|
// XXX Hackish...
|
|
|
|
|
var syntax = {
|
|
|
|
|
__proto__: plugin,
|
|
|
|
|
|
|
|
|
|
update: function(){
|
|
|
|
|
window.hljs
|
|
|
|
|
&& hljs.highlightAll()
|
|
|
|
|
return this },
|
|
|
|
|
|
|
|
|
|
__setup__: function(editor){
|
|
|
|
|
return this.update() },
|
|
|
|
|
// XXX make a local update...
|
2023-10-14 22:49:02 +03:00
|
|
|
__editedcode__: function(evt, editor, elem){
|
|
|
|
|
return this.update(elem) },
|
|
|
|
|
__editedview__: function(evt, editor, elem){
|
|
|
|
|
// XXX should we also clear the syntax???
|
|
|
|
|
delete elem.dataset.highlighted
|
|
|
|
|
return this },
|
|
|
|
|
// XXX this removes highlighting, can we make it update live???
|
|
|
|
|
__focusin__: function(evt, editor, elem){
|
|
|
|
|
if(elem.nodeName == 'CODE'
|
|
|
|
|
&& elem.getAttribute('contenteditable') == 'true'){
|
|
|
|
|
elem.classList.remove('hljs') } },
|
|
|
|
|
__focusout__: function(evt, editor, elem){
|
|
|
|
|
if(elem.nodeName == 'CODE'
|
|
|
|
|
&& elem.getAttribute('contenteditable') == 'true'){
|
|
|
|
|
this.update(elem) }
|
|
|
|
|
return this },
|
2023-10-14 02:42:29 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
|
|
var tables = {
|
|
|
|
|
__proto__: plugin,
|
|
|
|
|
|
|
|
|
|
__parse__: function(text, editor, elem){
|
|
|
|
|
return text
|
|
|
|
|
.replace(/^\s*(?<!\\)\|\s*((.|\n)*)\s*\|\s*$/,
|
|
|
|
|
function(_, body){
|
|
|
|
|
return `<table><tr><td>${
|
|
|
|
|
body
|
|
|
|
|
.replace(/\s*\|\s*\n\s*\|\s*/gm, '</td></tr>\n<tr><td>')
|
|
|
|
|
.replace(/\s*\|\s*/gm, '</td><td>')
|
|
|
|
|
}</td></td></table>` }) },
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
|
|
var styling = {
|
|
|
|
|
__proto__: plugin,
|
|
|
|
|
|
|
|
|
|
__parse__: function(text, editor, elem){
|
|
|
|
|
return text
|
|
|
|
|
// markers...
|
2023-10-15 00:40:04 +03:00
|
|
|
.replace(/(\s*)(?<!\\)(FEATURE[:?]|Q:|Question:|Note:)(\s*)/gm,
|
2023-10-14 02:42:29 +03:00
|
|
|
'$1<b class="$2">$2</b>$3')
|
|
|
|
|
.replace(/(\s*)(?<!\\)(ASAP|BUG|FIX|HACK|STUB|WARNING|CAUTION)(\s*)/gm,
|
|
|
|
|
'$1<span class="highlight $2">$2</span>$3')
|
|
|
|
|
// elements...
|
|
|
|
|
.replace(/(\n|^)(?<!\\)---*\h*(\n|$)/m, '$1<hr>')
|
|
|
|
|
// basic styling...
|
|
|
|
|
.replace(/(?<!\\)\*(?=[^\s*])(([^*]|\\\*)*[^\s*])(?<!\\)\*/gm, '<b>$1</b>')
|
|
|
|
|
.replace(/(?<!\\)~(?=[^\s~])(([^~]|\\~)*[^\s~])(?<!\\)~/gm, '<s>$1</s>')
|
2023-10-18 14:41:49 +03:00
|
|
|
// XXX this can clash with '[_] .. [_]' checkboxes...
|
2023-10-14 02:42:29 +03:00
|
|
|
.replace(/(?<!\\)_(?=[^\s_])(([^_]|\\_)*[^\s_])(?<!\\)_/gm, '<i>$1</i>')
|
|
|
|
|
// code/quoting...
|
|
|
|
|
//.replace(/(?<!\\)`(?=[^\s])(([^`]|\\`)*[^\s])(?<!\\)`/gm, quote)
|
|
|
|
|
// XXX support "\==" in mark...
|
|
|
|
|
.replace(/(?<!\\)==(?=[^\s])(.*[^\s])(?<!\\)==/gm, '<mark>$1</mark>')
|
|
|
|
|
// links...
|
|
|
|
|
.replace(/(?<!\\)\[([^\]]*)\]\(([^)]*)\)/g, '<a href="$2">$1</a>')
|
|
|
|
|
.replace(/((?:https?:|ftps?:)[^\s]*)(\s*)/g, '<a href="$1">$1</a>$2') },
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
|
|
// XXX use ligatures for these???
|
|
|
|
|
var symbols = {
|
|
|
|
|
__proto__: plugin,
|
|
|
|
|
|
2023-10-20 19:00:35 +03:00
|
|
|
// XXX use a single regex with handler func to do these...
|
|
|
|
|
symbols: {
|
2023-10-21 15:53:09 +03:00
|
|
|
// XXX think these are better handled by ligatures...
|
|
|
|
|
//'>>': '»',
|
|
|
|
|
//'<<': '«',
|
|
|
|
|
//'->': '→',
|
|
|
|
|
//'<-': '←',
|
|
|
|
|
//'=>': '⇒',
|
|
|
|
|
//'<=': '⇐',
|
|
|
|
|
'(i)': '🛈',
|
|
|
|
|
'(c)': '©',
|
|
|
|
|
'/!\\': '⚠',
|
2023-10-20 19:00:35 +03:00
|
|
|
},
|
|
|
|
|
get symbols_pattern(){
|
2023-10-20 19:07:57 +03:00
|
|
|
return (this.symbols != null
|
|
|
|
|
&& Object.keys(this.symbols).length > 0) ?
|
|
|
|
|
new RegExp(`(?<!\\\\)(${
|
|
|
|
|
Object.keys(this.symbols)
|
|
|
|
|
.join('|')
|
|
|
|
|
.replace(/([\(\)\\\/])/g, '\\$1') })`, 'g')
|
|
|
|
|
: undefined },
|
2023-10-20 19:00:35 +03:00
|
|
|
|
2023-10-14 02:42:29 +03:00
|
|
|
__parse__: function(text, editor, elem){
|
2023-10-20 19:00:35 +03:00
|
|
|
var that = this
|
2023-10-20 19:07:57 +03:00
|
|
|
var p = this.symbols_pattern
|
|
|
|
|
text = p ?
|
|
|
|
|
text.replace(p,
|
2023-10-20 19:00:35 +03:00
|
|
|
function(m){
|
|
|
|
|
return that.symbols[m] })
|
2023-10-20 19:07:57 +03:00
|
|
|
: text
|
|
|
|
|
return text
|
2023-10-14 02:42:29 +03:00
|
|
|
.replace(/(?<!\\)---(?!-)/gm, '—')
|
|
|
|
|
.replace(/(?<!\\)--(?!-)/gm, '–') },
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
|
|
var escaping = {
|
|
|
|
|
__proto__: plugin,
|
|
|
|
|
|
|
|
|
|
__post_parse__: function(text, editor, elem){
|
|
|
|
|
return text
|
|
|
|
|
// quoting...
|
|
|
|
|
// NOTE: this must be last...
|
|
|
|
|
.replace(/(?<!\\)\\(.)/gm, '$1') },
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2023-10-13 22:14:36 +03:00
|
|
|
|
2023-09-27 15:05:34 +03:00
|
|
|
//---------------------------------------------------------------------
|
|
|
|
|
|
2023-10-23 04:26:00 +03:00
|
|
|
var JSONOutline = {
|
2023-10-24 17:35:28 +03:00
|
|
|
// Format:
|
|
|
|
|
// <json> ::= [
|
|
|
|
|
// {
|
|
|
|
|
// text: <text>,
|
|
|
|
|
// children: <json>,
|
|
|
|
|
// ...
|
|
|
|
|
// },
|
|
|
|
|
// ...
|
|
|
|
|
// ]
|
2023-10-23 04:26:00 +03:00
|
|
|
json: undefined,
|
|
|
|
|
|
2023-10-24 17:35:28 +03:00
|
|
|
// format:
|
|
|
|
|
// {
|
|
|
|
|
// <id>: <node>,
|
|
|
|
|
// ...
|
|
|
|
|
// }
|
|
|
|
|
__id_index: undefined,
|
|
|
|
|
|
|
|
|
|
// format:
|
|
|
|
|
// Map([
|
2023-10-25 04:27:13 +03:00
|
|
|
// [<node>, <parent>],
|
2023-10-24 17:35:28 +03:00
|
|
|
// ...
|
|
|
|
|
// ])
|
|
|
|
|
__nodes: undefined,
|
|
|
|
|
|
|
|
|
|
__path: undefined,
|
|
|
|
|
current: undefined,
|
|
|
|
|
|
|
|
|
|
__iter: function*(node, path, mode){
|
|
|
|
|
if(typeof(path) == 'string'){
|
|
|
|
|
mode = path
|
|
|
|
|
path = null }
|
|
|
|
|
path ??= []
|
|
|
|
|
yield [path, node]
|
|
|
|
|
if(mode == 'visible'
|
|
|
|
|
&& node.collapsed){
|
|
|
|
|
return }
|
|
|
|
|
var i = 0
|
|
|
|
|
for(var e of node.children ?? []){
|
|
|
|
|
yield* this.__iter(e, [...path, i++], mode) } },
|
|
|
|
|
// XXX revise...
|
|
|
|
|
nodes: function*(node, mode){
|
|
|
|
|
var i = 0
|
|
|
|
|
// all nodes..
|
|
|
|
|
if(node == null || node == 'all' || node == 'visible'){
|
|
|
|
|
for(var e of this.json){
|
|
|
|
|
yield* this.__iter(e, [i++], node) }
|
|
|
|
|
// single node...
|
|
|
|
|
} else {
|
|
|
|
|
var args = [...arguments]
|
|
|
|
|
// XXX revise...
|
|
|
|
|
if(['all', 'visible'].includes(args.at(-1))){
|
|
|
|
|
mode = args.pop() }
|
|
|
|
|
yield* this.__iter(
|
|
|
|
|
this.get(...args),
|
|
|
|
|
mode) } },
|
|
|
|
|
[Symbol.iterator]: function*(mode='all'){
|
|
|
|
|
for(var node of this.json){
|
|
|
|
|
for(var [_, n] of this.__iter(node, mode)){
|
|
|
|
|
yield n } } },
|
|
|
|
|
iter: function*(node, mode){
|
|
|
|
|
for(var [_, n] of this.nodes(...arguments)){
|
|
|
|
|
yield n } },
|
|
|
|
|
|
|
|
|
|
// XXX
|
2023-10-23 04:26:00 +03:00
|
|
|
path: function(){},
|
2023-10-24 17:35:28 +03:00
|
|
|
get: function(node, offset){
|
|
|
|
|
},
|
|
|
|
|
focus: function(node, offset){
|
|
|
|
|
return this.get(
|
|
|
|
|
this.__path = this.path(...arguments)) },
|
|
|
|
|
|
|
|
|
|
index: function(){},
|
|
|
|
|
at: function(index){},
|
2023-10-23 04:26:00 +03:00
|
|
|
|
|
|
|
|
indent: function(){},
|
|
|
|
|
shift: function(){},
|
|
|
|
|
show: function(){},
|
|
|
|
|
toggleCollapse: function(){},
|
|
|
|
|
remove: function(){},
|
|
|
|
|
clear: function(){},
|
|
|
|
|
|
2023-10-24 17:35:28 +03:00
|
|
|
crop: function(){},
|
2023-10-23 04:26:00 +03:00
|
|
|
uncrop: function(){},
|
|
|
|
|
|
|
|
|
|
parseBlockAttrs: function(){},
|
|
|
|
|
parse: function(){},
|
|
|
|
|
|
|
|
|
|
data: function(){},
|
|
|
|
|
load: function(){},
|
|
|
|
|
text: function(){},
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-24 17:35:28 +03:00
|
|
|
|
2023-10-26 01:02:27 +03:00
|
|
|
|
2023-10-09 01:32:46 +03:00
|
|
|
// XXX experiment with a concatinative model...
|
|
|
|
|
// .get(..) -> Outline (view)
|
2023-09-27 15:05:34 +03:00
|
|
|
var Outline = {
|
|
|
|
|
dom: undefined,
|
|
|
|
|
|
2023-09-27 15:41:29 +03:00
|
|
|
// config...
|
|
|
|
|
//
|
2023-09-29 15:38:17 +03:00
|
|
|
left_key_collapses: true,
|
|
|
|
|
right_key_expands: true,
|
2023-10-21 15:53:09 +03:00
|
|
|
change_interval: 1000,
|
2023-10-08 02:36:29 +03:00
|
|
|
tab_size: 4,
|
2023-10-11 07:16:18 +03:00
|
|
|
carot_jump_edge_then_block: false,
|
2023-11-01 19:07:37 +03:00
|
|
|
// XXX not sure what should the default be...
|
|
|
|
|
trim_block_text: false,
|
2023-09-27 15:41:29 +03:00
|
|
|
|
|
|
|
|
|
2023-10-18 14:41:49 +03:00
|
|
|
// Plugins...
|
|
|
|
|
//
|
2023-10-14 22:49:02 +03:00
|
|
|
// The order of plugins can be significant in the following cases:
|
|
|
|
|
// - parsing
|
|
|
|
|
// - event dropping
|
2023-10-14 02:42:29 +03:00
|
|
|
plugins: [
|
|
|
|
|
blocks,
|
|
|
|
|
quoted,
|
2023-10-18 14:41:49 +03:00
|
|
|
|
|
|
|
|
// NOTE: this needs to be before styling to prevent it from
|
|
|
|
|
// treating '[_] ... [_]' as italic...
|
|
|
|
|
tasks,
|
2023-10-14 02:42:29 +03:00
|
|
|
styling,
|
2023-10-22 23:28:04 +03:00
|
|
|
// XXX
|
|
|
|
|
//attributes,
|
2023-10-14 02:42:29 +03:00
|
|
|
tables,
|
|
|
|
|
symbols,
|
2023-10-18 14:58:08 +03:00
|
|
|
//syntax,
|
2023-10-14 02:42:29 +03:00
|
|
|
|
|
|
|
|
// keep this last...
|
|
|
|
|
// XXX revise -- should this be external???
|
|
|
|
|
escaping,
|
|
|
|
|
],
|
2023-10-14 22:49:02 +03:00
|
|
|
// NOTE: if a handler returns false it will break plugin execution...
|
|
|
|
|
// XXX is this the right way to go???
|
2023-10-14 02:42:29 +03:00
|
|
|
runPlugins: function(method, ...args){
|
|
|
|
|
for(var plugin of this.plugins){
|
2023-10-14 22:49:02 +03:00
|
|
|
if(method in plugin){
|
|
|
|
|
if(plugin[method](...args) === false){
|
|
|
|
|
return false } } }
|
|
|
|
|
return true },
|
2023-10-14 02:42:29 +03:00
|
|
|
threadPlugins: function(method, value, ...args){
|
|
|
|
|
for(var plugin of this.plugins){
|
|
|
|
|
method in plugin
|
|
|
|
|
&& (value = plugin[method](value, ...args)) }
|
|
|
|
|
return value },
|
|
|
|
|
|
|
|
|
|
|
2023-10-23 22:55:23 +03:00
|
|
|
get header(){
|
2023-10-29 15:31:23 +03:00
|
|
|
return this.dom?.querySelector('.header') },
|
2023-10-04 15:33:07 +03:00
|
|
|
get outline(){
|
2023-10-29 15:31:23 +03:00
|
|
|
return this.dom?.querySelector('.outline') },
|
2023-10-04 15:40:29 +03:00
|
|
|
get toolbar(){
|
2023-10-29 15:31:23 +03:00
|
|
|
return this.dom?.querySelector('.toolbar') },
|
2023-10-04 15:33:07 +03:00
|
|
|
|
2023-10-28 22:52:46 +03:00
|
|
|
get code(){
|
2023-10-29 15:31:23 +03:00
|
|
|
return this.dom?.querySelector('.code')?.value },
|
2023-10-28 22:52:46 +03:00
|
|
|
set code(value){
|
2023-10-29 15:31:23 +03:00
|
|
|
if(value == null){
|
|
|
|
|
return }
|
|
|
|
|
var c = this.dom?.querySelector('.code')
|
2023-10-28 22:52:46 +03:00
|
|
|
if(c){
|
|
|
|
|
c.value = value } },
|
|
|
|
|
|
2023-10-04 15:33:07 +03:00
|
|
|
|
2023-10-23 22:55:23 +03:00
|
|
|
path: function(node='focused', mode='index'){
|
2023-10-24 17:35:28 +03:00
|
|
|
if(['index', 'text', 'node', 'data'].includes(node)){
|
2023-10-23 22:55:23 +03:00
|
|
|
mode = node
|
|
|
|
|
node = 'focused' }
|
2023-10-23 04:26:00 +03:00
|
|
|
var outline = this.outline
|
|
|
|
|
var path = []
|
|
|
|
|
var node = this.get(node)
|
|
|
|
|
while(node != outline){
|
2023-10-23 22:55:23 +03:00
|
|
|
path.unshift(
|
|
|
|
|
mode == 'index' ?
|
|
|
|
|
this.get(node, 'siblings').indexOf(node)
|
|
|
|
|
: mode == 'text' ?
|
|
|
|
|
node.querySelector('.view').innerText
|
2023-10-24 17:35:28 +03:00
|
|
|
: mode == 'data' ?
|
|
|
|
|
this.data(node)
|
2023-10-23 22:55:23 +03:00
|
|
|
: node)
|
2023-10-23 04:26:00 +03:00
|
|
|
node = this.get(node, 'parent') }
|
|
|
|
|
return path },
|
|
|
|
|
|
2023-09-28 20:49:06 +03:00
|
|
|
//
|
2023-10-23 19:09:29 +03:00
|
|
|
// .get(<index>)[, <offset>]
|
|
|
|
|
// .get(<path>[, <offset>])
|
|
|
|
|
// .get(<id>[, <offset>)
|
|
|
|
|
// -> <node>
|
|
|
|
|
//
|
2023-09-28 20:49:06 +03:00
|
|
|
// .get('focused'[, <offset>])
|
|
|
|
|
// -> <node>
|
|
|
|
|
//
|
|
|
|
|
// .get('edited'[, <offset>])
|
|
|
|
|
// -> <node>
|
|
|
|
|
//
|
|
|
|
|
// .get('siblings')
|
|
|
|
|
// .get('focused', 'siblings')
|
|
|
|
|
// -> <nodes>
|
|
|
|
|
//
|
|
|
|
|
// .get('children')
|
|
|
|
|
// .get('focused', 'children')
|
|
|
|
|
// -> <nodes>
|
|
|
|
|
//
|
|
|
|
|
// .get('next')
|
|
|
|
|
// .get('focused', 'next')
|
|
|
|
|
// -> <node>
|
|
|
|
|
//
|
|
|
|
|
// .get('prev')
|
|
|
|
|
// .get('focused', 'prev')
|
|
|
|
|
// -> <node>
|
|
|
|
|
//
|
|
|
|
|
// .get('all')
|
|
|
|
|
// .get('visible')
|
|
|
|
|
// .get('editable')
|
|
|
|
|
// .get('selected')
|
2023-10-23 14:37:14 +03:00
|
|
|
// .get('viewport')
|
2023-09-28 20:49:06 +03:00
|
|
|
// .get('top')
|
|
|
|
|
// -> <nodes>
|
|
|
|
|
//
|
|
|
|
|
// XXX add support for node ID...
|
2023-10-03 16:32:29 +03:00
|
|
|
// XXX need to be able to get the next elem on same level...
|
2023-10-12 19:45:12 +03:00
|
|
|
get: function(node='focused', offset){
|
|
|
|
|
var that = this
|
|
|
|
|
offset =
|
|
|
|
|
offset == 'next' ?
|
|
|
|
|
1
|
|
|
|
|
: offset == 'prev' ?
|
|
|
|
|
-1
|
|
|
|
|
: offset
|
|
|
|
|
var outline = this.outline
|
|
|
|
|
|
2023-10-23 19:09:29 +03:00
|
|
|
// id...
|
|
|
|
|
if(typeof(node) == 'string' && node[0] == '#'){
|
|
|
|
|
node = outline.querySelector(node) }
|
|
|
|
|
|
2023-10-12 19:45:12 +03:00
|
|
|
// root nodes...
|
|
|
|
|
if(node == 'top'){
|
|
|
|
|
return [...outline.children] }
|
|
|
|
|
// groups defaulting to .outline as base...
|
2023-10-23 14:37:14 +03:00
|
|
|
if(['all', 'visible', 'editable', 'selected', 'viewport'].includes(node)){
|
2023-10-12 19:45:12 +03:00
|
|
|
return this.get(outline, node) }
|
|
|
|
|
// groups defaulting to .focused as base...
|
|
|
|
|
if(['parent', 'next', 'prev', 'children', 'siblings'].includes(node)){
|
|
|
|
|
return this.get('focused', node) }
|
|
|
|
|
// helpers...
|
|
|
|
|
var parent = function(node){
|
|
|
|
|
return node === outline ?
|
2023-10-13 22:14:36 +03:00
|
|
|
outline
|
|
|
|
|
: node.parentElement === outline ?
|
|
|
|
|
outline
|
|
|
|
|
: node.parentElement.parentElement }
|
2023-10-12 19:45:12 +03:00
|
|
|
var children = function(node){
|
|
|
|
|
return node === outline ?
|
|
|
|
|
[...node.children]
|
|
|
|
|
: [...node?.lastChild?.children] }
|
|
|
|
|
|
|
|
|
|
// single base node...
|
|
|
|
|
var edited
|
|
|
|
|
;[node, edited] =
|
|
|
|
|
typeof(node) == 'number' ?
|
|
|
|
|
[this.get('visible').at(node),
|
|
|
|
|
edited]
|
2023-10-23 04:26:00 +03:00
|
|
|
: node instanceof Array ?
|
|
|
|
|
[node
|
|
|
|
|
.reduce(function(res, i){
|
|
|
|
|
return that.get(res, 'children')[i] }, outline),
|
|
|
|
|
edited]
|
2023-10-12 19:45:12 +03:00
|
|
|
: (node == 'outline' || node == 'root') ?
|
|
|
|
|
[outline, edited]
|
|
|
|
|
: node == 'focused' ?
|
|
|
|
|
[outline.querySelector(`.block:focus`)
|
|
|
|
|
|| outline.querySelector(`.code:focus`)
|
|
|
|
|
|| outline.querySelector('.block.focused'),
|
|
|
|
|
edited]
|
|
|
|
|
: node == 'edited' ?
|
|
|
|
|
[outline.querySelector(`.code:focus`),
|
|
|
|
|
outline.querySelector(`.code:focus`)]
|
|
|
|
|
: [node , edited]
|
|
|
|
|
|
|
|
|
|
// get the .block...
|
|
|
|
|
if(node instanceof HTMLElement){
|
|
|
|
|
while(node !== outline
|
|
|
|
|
&& !node.classList.contains('block')){
|
|
|
|
|
node = node.parentElement } }
|
|
|
|
|
|
|
|
|
|
// no reference node...
|
|
|
|
|
if(node == null
|
|
|
|
|
|| typeof(node) == 'string'){
|
|
|
|
|
return undefined }
|
|
|
|
|
|
|
|
|
|
// parent...
|
|
|
|
|
if(offset == 'parent'){
|
|
|
|
|
return edited ?
|
|
|
|
|
parent(node).querySelector('.code')
|
|
|
|
|
: parent(node) }
|
|
|
|
|
|
|
|
|
|
// node groups...
|
|
|
|
|
var nodes =
|
|
|
|
|
typeof(offset) == 'number' ?
|
|
|
|
|
this.get('visible')
|
|
|
|
|
: offset == 'all' ?
|
|
|
|
|
[...node.querySelectorAll('.block')]
|
|
|
|
|
: offset == 'visible' ?
|
|
|
|
|
[...node.querySelectorAll('.block')]
|
|
|
|
|
.filter(function(e){
|
2023-10-25 15:04:41 +03:00
|
|
|
return e.querySelector('.view').offsetParent != null })
|
2023-10-23 14:37:14 +03:00
|
|
|
: offset == 'viewport' ?
|
|
|
|
|
[...node.querySelectorAll('.block')]
|
|
|
|
|
.filter(function(e){
|
2023-10-25 15:04:41 +03:00
|
|
|
return e.querySelector('.view').offsetParent != null
|
2023-10-23 14:37:14 +03:00
|
|
|
&& e.querySelector('.code').visibleInViewport() })
|
2023-10-12 19:45:12 +03:00
|
|
|
: offset == 'editable' ?
|
|
|
|
|
[...node.querySelectorAll('.block>.code')]
|
|
|
|
|
: offset == 'selected' ?
|
|
|
|
|
[...node.querySelectorAll('.block[selected]')]
|
|
|
|
|
.filter(function(e){
|
2023-10-25 15:04:41 +03:00
|
|
|
return e.querySelector('.view').offsetParent != null })
|
2023-10-12 19:45:12 +03:00
|
|
|
: offset == 'children' ?
|
|
|
|
|
children(node)
|
|
|
|
|
: offset == 'siblings' ?
|
|
|
|
|
children(parent(node))
|
|
|
|
|
: undefined
|
|
|
|
|
|
|
|
|
|
// get node by offset...
|
|
|
|
|
if(typeof(offset) == 'number'){
|
|
|
|
|
node = nodes.at(nodes.indexOf(node) + offset)
|
|
|
|
|
?? nodes[0]
|
|
|
|
|
edited = edited ?
|
|
|
|
|
node.querySelector('.code')
|
|
|
|
|
: edited
|
|
|
|
|
nodes = undefined }
|
|
|
|
|
|
|
|
|
|
return nodes !== undefined ?
|
|
|
|
|
edited ?
|
|
|
|
|
nodes
|
|
|
|
|
.map(function(){
|
|
|
|
|
return node.querySelector('.code') })
|
|
|
|
|
: nodes
|
|
|
|
|
: (edited
|
|
|
|
|
?? node) },
|
2023-09-28 20:49:06 +03:00
|
|
|
at: function(index, nodes='visible'){
|
|
|
|
|
return this.get(nodes).at(index) },
|
2023-10-11 07:31:06 +03:00
|
|
|
focus: function(node='focused', offset){
|
2023-10-23 04:26:00 +03:00
|
|
|
var elem = this.get(...arguments)
|
|
|
|
|
?? this.get(0)
|
2023-10-23 14:37:14 +03:00
|
|
|
if(elem){
|
2023-10-23 20:46:44 +03:00
|
|
|
var cur = this.get()
|
|
|
|
|
var blocks = this.get('visible')
|
2023-10-23 14:37:14 +03:00
|
|
|
elem.focus({preventScroll: true})
|
|
|
|
|
;(elem.classList.contains('code') ?
|
|
|
|
|
elem
|
|
|
|
|
: elem.querySelector('.code'))
|
|
|
|
|
.scrollIntoView({
|
|
|
|
|
block: 'nearest',
|
2023-10-23 20:46:44 +03:00
|
|
|
// smooth for long jumps and instant for short jumps...
|
|
|
|
|
behavior: (cur == null
|
|
|
|
|
|| Math.abs(blocks.indexOf(cur) - blocks.indexOf(elem)) > 2) ?
|
|
|
|
|
'smooth'
|
|
|
|
|
: 'instant'
|
2023-10-23 14:37:14 +03:00
|
|
|
}) }
|
2023-10-11 07:31:06 +03:00
|
|
|
return elem },
|
|
|
|
|
edit: function(node='focused', offset){
|
|
|
|
|
var elem = this.get(...arguments)
|
2023-10-23 14:37:14 +03:00
|
|
|
if(!elem.classList.contains('code')){
|
|
|
|
|
elem = elem.querySelector('.code') }
|
2023-10-11 07:31:06 +03:00
|
|
|
elem?.focus()
|
|
|
|
|
return elem },
|
2023-09-28 20:49:06 +03:00
|
|
|
|
2023-10-21 15:53:09 +03:00
|
|
|
// This will prevent spamming the .sync() by limiting calls to one
|
|
|
|
|
// per .change_interval
|
|
|
|
|
//
|
|
|
|
|
// XXX should we call plugin's __change__ live or every second???
|
|
|
|
|
__change_timeout: undefined,
|
|
|
|
|
__change_requested: false,
|
2023-10-26 18:11:12 +03:00
|
|
|
__change__: function(options={}){
|
2023-10-21 15:53:09 +03:00
|
|
|
var that = this
|
2023-10-26 18:11:12 +03:00
|
|
|
|
|
|
|
|
// handle undo...
|
|
|
|
|
options.undo
|
|
|
|
|
&& this.setUndo(...options.undo)
|
|
|
|
|
|
|
|
|
|
// long changes...
|
2023-10-21 15:53:09 +03:00
|
|
|
this.__change_requested = true
|
|
|
|
|
if(this.__change_timeout){
|
|
|
|
|
return this }
|
|
|
|
|
|
|
|
|
|
// do the action...
|
|
|
|
|
if(this.__change_requested){
|
|
|
|
|
this.sync()
|
2023-10-26 18:11:12 +03:00
|
|
|
this.runPlugins('__change__', this)
|
2023-10-21 15:53:09 +03:00
|
|
|
this.__change_requested = false }
|
|
|
|
|
|
|
|
|
|
this.__change_timeout = setTimeout(
|
|
|
|
|
function(){
|
|
|
|
|
that.__change_timeout = undefined
|
|
|
|
|
that.__change_requested
|
|
|
|
|
&& that.__change__() },
|
|
|
|
|
that.change_interval || 1000)
|
|
|
|
|
return this },
|
|
|
|
|
|
2023-10-22 13:55:41 +03:00
|
|
|
__block_attrs__: {
|
|
|
|
|
id: 'attr',
|
|
|
|
|
collapsed: 'attr',
|
|
|
|
|
focused: 'cls',
|
|
|
|
|
},
|
2023-10-26 18:11:12 +03:00
|
|
|
// NOTE: this does not internally handle undo as it would be too
|
|
|
|
|
// granular...
|
2023-10-22 13:55:41 +03:00
|
|
|
update: function(node='focused', data){
|
|
|
|
|
var node = this.get(node)
|
|
|
|
|
data ??= this.data(node, false)
|
|
|
|
|
|
2023-10-22 23:28:04 +03:00
|
|
|
var parsed = {}
|
|
|
|
|
if('text' in data){
|
2023-10-28 22:52:46 +03:00
|
|
|
var text = node.querySelector('.code')
|
|
|
|
|
var html = node.querySelector('.view')
|
2023-10-22 23:28:04 +03:00
|
|
|
if(this.__code2html__){
|
|
|
|
|
// NOTE: we are ignoring the .collapsed attr here
|
|
|
|
|
parsed = this.__code2html__(data.text, {...data})
|
2023-11-01 19:07:37 +03:00
|
|
|
html.innerHTML =
|
2023-11-02 00:06:39 +03:00
|
|
|
parsed.text == '' ?
|
2023-11-01 19:07:37 +03:00
|
|
|
parsed.text
|
|
|
|
|
// NOTE: adding a space here is done to prevent the browser
|
|
|
|
|
// from hiding the last newline...
|
|
|
|
|
: parsed.text + ' '
|
2023-10-22 23:28:04 +03:00
|
|
|
// heading...
|
2023-10-28 22:52:46 +03:00
|
|
|
this.__styles != null
|
|
|
|
|
&& node.classList.remove(...this.__styles)
|
2023-10-22 23:28:04 +03:00
|
|
|
parsed.style
|
|
|
|
|
&& node.classList.add(...parsed.style)
|
|
|
|
|
delete parsed.style
|
|
|
|
|
} else {
|
2023-11-01 19:07:37 +03:00
|
|
|
html.innerHTML =
|
2023-11-02 00:06:39 +03:00
|
|
|
data.text == '' ?
|
2023-11-01 19:07:37 +03:00
|
|
|
data.text
|
|
|
|
|
// NOTE: adding a space here is done to prevent the browser
|
|
|
|
|
// from hiding the last newline...
|
|
|
|
|
: data.text + ' ' }
|
2023-11-02 06:53:23 +03:00
|
|
|
text.value = data.text
|
|
|
|
|
text.updateSize() }
|
2023-10-22 23:28:04 +03:00
|
|
|
|
|
|
|
|
for(var [attr, value] of Object.entries({...data, ...parsed})){
|
|
|
|
|
if(attr == 'children' || attr == 'text'){
|
2023-10-22 13:55:41 +03:00
|
|
|
continue }
|
|
|
|
|
|
|
|
|
|
var type = this.__block_attrs__[attr]
|
|
|
|
|
if(type == 'cls'){
|
|
|
|
|
value ?
|
|
|
|
|
node.classList.add(attr)
|
|
|
|
|
: node.classList.remove(attr)
|
|
|
|
|
|
|
|
|
|
} else if(type == 'attr'
|
|
|
|
|
|| type == undefined){
|
|
|
|
|
typeof(value) == 'boolean'?
|
|
|
|
|
(value ?
|
|
|
|
|
node.setAttribute(attr, '')
|
|
|
|
|
: node.removeAttribute(attr))
|
2023-10-22 23:28:04 +03:00
|
|
|
: value != null ?
|
2023-10-22 13:55:41 +03:00
|
|
|
node.setAttribute(attr, value)
|
|
|
|
|
: node.removeAttribute(attr) } }
|
|
|
|
|
this.__change__()
|
|
|
|
|
return node },
|
|
|
|
|
|
2023-10-21 15:53:09 +03:00
|
|
|
// edit...
|
2023-10-26 01:02:27 +03:00
|
|
|
indent: function(node='focused', indent='in'){
|
2023-09-28 20:49:06 +03:00
|
|
|
// .indent(<indent>)
|
2023-10-26 01:02:27 +03:00
|
|
|
if(node === 'in' || node === 'out'){
|
2023-09-28 20:49:06 +03:00
|
|
|
indent = node
|
|
|
|
|
node = 'focused' }
|
|
|
|
|
var cur = this.get(node)
|
2023-10-13 22:19:16 +03:00
|
|
|
if(!cur){
|
2023-09-28 20:49:06 +03:00
|
|
|
return }
|
2023-10-26 01:20:57 +03:00
|
|
|
var prev = this.path(cur)
|
2023-09-28 20:49:06 +03:00
|
|
|
var siblings = this.get(node, 'siblings')
|
|
|
|
|
// deindent...
|
2023-10-26 01:02:27 +03:00
|
|
|
if(indent == 'out'){
|
2023-10-12 19:45:12 +03:00
|
|
|
var parent = this.get(node, 'parent')
|
2023-10-13 22:19:16 +03:00
|
|
|
if(parent != this.outline){
|
2023-10-12 19:45:12 +03:00
|
|
|
var children = siblings
|
|
|
|
|
.slice(siblings.indexOf(cur)+1)
|
2023-09-28 20:49:06 +03:00
|
|
|
parent.after(cur)
|
|
|
|
|
children.length > 0
|
2023-10-21 15:53:09 +03:00
|
|
|
&& cur.lastChild.append(...children)
|
2023-10-26 18:11:12 +03:00
|
|
|
this.__change__({undo: [
|
2023-10-26 01:02:27 +03:00
|
|
|
this.path(cur),
|
|
|
|
|
'indent',
|
2023-10-26 01:20:57 +03:00
|
|
|
['in'],
|
2023-10-26 18:11:12 +03:00
|
|
|
prev ]}) }
|
2023-09-28 20:49:06 +03:00
|
|
|
// indent...
|
|
|
|
|
} else {
|
|
|
|
|
var parent = siblings[siblings.indexOf(cur) - 1]
|
|
|
|
|
if(parent){
|
2023-10-21 15:53:09 +03:00
|
|
|
parent.lastChild.append(cur)
|
2023-10-26 18:11:12 +03:00
|
|
|
this.__change__({undo: [
|
2023-10-26 01:20:57 +03:00
|
|
|
this.path(cur),
|
|
|
|
|
'indent',
|
|
|
|
|
['out'],
|
2023-10-26 18:11:12 +03:00
|
|
|
prev ]})} }
|
2023-09-28 20:49:06 +03:00
|
|
|
return cur },
|
2023-10-15 00:03:22 +03:00
|
|
|
shift: function(node='focused', direction){
|
|
|
|
|
if(node == 'up' || node == 'down'){
|
|
|
|
|
direction = node
|
|
|
|
|
node = 'focused' }
|
2023-10-26 01:02:27 +03:00
|
|
|
if(direction == null
|
|
|
|
|
|| (direction !== 'up'
|
|
|
|
|
&& direction != 'down')){
|
2023-10-15 00:03:22 +03:00
|
|
|
return }
|
|
|
|
|
node = this.get(node)
|
|
|
|
|
var focused = node.classList.contains('focused')
|
|
|
|
|
var siblings = this.get(node, 'siblings')
|
|
|
|
|
var i = siblings.indexOf(node)
|
|
|
|
|
if(direction == 'up'
|
|
|
|
|
&& i > 0){
|
|
|
|
|
siblings[i-1].before(node)
|
|
|
|
|
} else if(direction == 'down'
|
|
|
|
|
&& i < siblings.length-1){
|
2023-10-26 01:20:57 +03:00
|
|
|
siblings[i+1].after(node) }
|
|
|
|
|
focused
|
|
|
|
|
&& this.focus()
|
2023-10-26 18:11:12 +03:00
|
|
|
this.__change__({undo: [
|
2023-10-26 01:02:27 +03:00
|
|
|
this.path(node),
|
|
|
|
|
'shift',
|
|
|
|
|
[direction == 'up' ?
|
|
|
|
|
'down'
|
2023-10-26 18:11:12 +03:00
|
|
|
: 'up'] ]})
|
2023-10-15 00:03:22 +03:00
|
|
|
return this },
|
2023-10-26 01:02:27 +03:00
|
|
|
// XXX make undo a bit more refined...
|
|
|
|
|
remove: function(node='focused'){
|
|
|
|
|
var elem = this.get(...arguments)
|
|
|
|
|
// XXX HACK...
|
|
|
|
|
var data = this.json()
|
|
|
|
|
var next
|
|
|
|
|
if(elem.classList.contains('focused')){
|
|
|
|
|
// XXX need to be able to get the next elem on same level...
|
|
|
|
|
this.toggleCollapse(elem, true)
|
|
|
|
|
next = elem === this.get(-1) ?
|
|
|
|
|
this.get(elem, 'prev')
|
|
|
|
|
: this.get(elem, 'next') }
|
|
|
|
|
elem?.remove()
|
|
|
|
|
next
|
|
|
|
|
&& this.focus(next)
|
2023-10-26 18:11:12 +03:00
|
|
|
this.__change__({undo: [
|
2023-10-26 01:02:27 +03:00
|
|
|
undefined,
|
|
|
|
|
'load',
|
2023-10-26 18:11:12 +03:00
|
|
|
// XXX HACK...
|
|
|
|
|
[data] ]})
|
2023-10-26 01:02:27 +03:00
|
|
|
return this },
|
|
|
|
|
clear: function(){
|
2023-10-26 18:11:12 +03:00
|
|
|
var data = this.json()
|
|
|
|
|
this.outline.innerText = ''
|
|
|
|
|
this.__change__({undo: [
|
2023-10-26 01:02:27 +03:00
|
|
|
undefined,
|
|
|
|
|
'load',
|
2023-10-26 18:11:12 +03:00
|
|
|
[data] ]})
|
2023-10-26 01:02:27 +03:00
|
|
|
return this },
|
|
|
|
|
|
|
|
|
|
// expand/collapse...
|
2023-09-28 20:49:06 +03:00
|
|
|
toggleCollapse: function(node='focused', state='next'){
|
|
|
|
|
var that = this
|
|
|
|
|
if(node == 'all'){
|
|
|
|
|
return this.get('all')
|
|
|
|
|
.map(function(node){
|
|
|
|
|
return that.toggleCollapse(node, state) }) }
|
|
|
|
|
// .toggleCollapse(<state>)
|
|
|
|
|
if(['next', true, false].includes(node)){
|
|
|
|
|
state = node
|
2023-10-04 03:00:57 +03:00
|
|
|
node = 'focused' }
|
|
|
|
|
node = this.get(node)
|
2023-09-28 20:49:06 +03:00
|
|
|
if(!node
|
|
|
|
|
// only nodes with children can be collapsed...
|
2023-10-12 19:45:12 +03:00
|
|
|
|| !node.querySelector('.block')){
|
2023-09-28 20:49:06 +03:00
|
|
|
return }
|
|
|
|
|
state = state == 'next' ?
|
2023-10-04 03:00:57 +03:00
|
|
|
node.getAttribute('collapsed') != ''
|
2023-09-28 20:49:06 +03:00
|
|
|
: state
|
|
|
|
|
if(state){
|
|
|
|
|
node.setAttribute('collapsed', '')
|
|
|
|
|
} else {
|
|
|
|
|
node.removeAttribute('collapsed')
|
|
|
|
|
for(var elem of [...node.querySelectorAll('textarea')]){
|
|
|
|
|
elem.updateSize() } }
|
2023-10-21 15:53:09 +03:00
|
|
|
this.__change__()
|
2023-09-28 20:49:06 +03:00
|
|
|
return node },
|
2023-10-26 01:02:27 +03:00
|
|
|
show: function(node='focused', offset){
|
|
|
|
|
var node = this.get(...arguments)
|
|
|
|
|
var outline = this.outline
|
|
|
|
|
var parent = node
|
|
|
|
|
var changes = false
|
|
|
|
|
do{
|
|
|
|
|
parent = parent.parentElement
|
|
|
|
|
changes = changes
|
|
|
|
|
|| parent.getAttribute('collapsed') == ''
|
|
|
|
|
parent.removeAttribute('collapsed')
|
|
|
|
|
} while(parent !== outline)
|
|
|
|
|
changes
|
|
|
|
|
&& this.__change__()
|
|
|
|
|
return node },
|
2023-10-07 17:06:54 +03:00
|
|
|
|
2023-10-23 04:26:00 +03:00
|
|
|
// crop...
|
2023-10-28 22:52:46 +03:00
|
|
|
// XXX the header links are not component-compatible...
|
2023-10-25 15:04:41 +03:00
|
|
|
crop: function(node='focused'){
|
|
|
|
|
this.dom.classList.add('crop')
|
|
|
|
|
for(var block of [...this.outline.querySelectorAll('[cropped]')]){
|
|
|
|
|
block.removeAttribute('cropped') }
|
|
|
|
|
this.get(...arguments).setAttribute('cropped', '')
|
2023-10-26 01:02:27 +03:00
|
|
|
// build header path...
|
2023-10-25 15:04:41 +03:00
|
|
|
this.header.innerHTML =
|
|
|
|
|
`<span class="path-item" onclick="editor.uncrop('all')">/</span> `
|
|
|
|
|
+ this.path(...arguments, 'text')
|
|
|
|
|
.slice(0, -1)
|
|
|
|
|
.map(function(s, i, {length}){
|
2023-11-02 22:54:55 +03:00
|
|
|
return `<span class="path-item" uncrop="${ length-i }">${
|
2023-10-28 22:52:46 +03:00
|
|
|
plugin.encode(s)
|
|
|
|
|
}</span> ` })
|
2023-10-25 15:04:41 +03:00
|
|
|
.join(' / ')
|
|
|
|
|
return this },
|
2023-10-26 01:02:27 +03:00
|
|
|
uncrop: function(count=1){
|
2023-10-25 15:04:41 +03:00
|
|
|
var outline = this.outline
|
|
|
|
|
var top = this.get(0)
|
|
|
|
|
for(var block of [...this.outline.querySelectorAll('[cropped]')]){
|
|
|
|
|
block.removeAttribute('cropped') }
|
|
|
|
|
// crop parent if available...
|
2023-10-26 01:02:27 +03:00
|
|
|
while(count != 'all'
|
|
|
|
|
&& count > 0
|
2023-10-25 15:04:41 +03:00
|
|
|
&& top !== outline){
|
|
|
|
|
top = this.get(top, 'parent')
|
2023-10-26 01:02:27 +03:00
|
|
|
count-- }
|
|
|
|
|
if(count == 'all' || top === outline){
|
2023-10-25 15:04:41 +03:00
|
|
|
this.dom.classList.remove('crop')
|
|
|
|
|
this.header.innerHTML = ''
|
|
|
|
|
} else {
|
|
|
|
|
this.crop(top) }
|
|
|
|
|
return this },
|
2023-10-23 04:26:00 +03:00
|
|
|
|
2023-10-26 01:02:27 +03:00
|
|
|
// undo...
|
|
|
|
|
// NOTE: calling .setUndo(..) will drop the redo stack, but this does
|
|
|
|
|
// not happen when calling a method via .undo(..)/.redo(..) as we
|
|
|
|
|
// are reassigning the stacks manually.
|
|
|
|
|
__undo_stack: undefined,
|
|
|
|
|
__redo_stack: undefined,
|
2023-10-26 01:20:57 +03:00
|
|
|
setUndo: function(path, action, args, next){
|
|
|
|
|
;(this.__undo_stack ??= []).push([path, action, args, next])
|
2023-10-26 01:02:27 +03:00
|
|
|
this.__redo_stack = undefined
|
|
|
|
|
return this },
|
2023-10-30 02:52:38 +03:00
|
|
|
mergeUndo: function(n, stack){
|
|
|
|
|
stack ??= this.__undo_stack
|
|
|
|
|
if(stack == null || stack.length == 0){
|
|
|
|
|
return this }
|
|
|
|
|
stack.push(
|
2023-10-30 02:56:03 +03:00
|
|
|
stack.splice(-n, n)
|
|
|
|
|
.map(function(e){
|
|
|
|
|
return typeof(e[1]) == 'string' ?
|
|
|
|
|
[e]
|
|
|
|
|
: e })
|
|
|
|
|
.flat())
|
2023-10-30 02:52:38 +03:00
|
|
|
return this },
|
2023-10-26 01:02:27 +03:00
|
|
|
clearUndo: function(){
|
|
|
|
|
this.__undo_stack = undefined
|
|
|
|
|
this.__redo_stack = undefined
|
|
|
|
|
return this },
|
|
|
|
|
__undo: function(from, to){
|
|
|
|
|
if(from == null
|
|
|
|
|
|| from.length == 0){
|
|
|
|
|
return [from, to] }
|
2023-10-30 02:52:38 +03:00
|
|
|
var actions = from.pop()
|
|
|
|
|
actions = typeof(actions[1]) == 'string' ?
|
|
|
|
|
[actions]
|
|
|
|
|
: actions
|
|
|
|
|
while(actions.length > 0){
|
|
|
|
|
var [path, action, args, next] = actions.pop()
|
|
|
|
|
var l = from.length
|
|
|
|
|
path != null
|
|
|
|
|
&& this.focus(path)
|
|
|
|
|
this[action](...args)
|
|
|
|
|
next != null ?
|
|
|
|
|
this.focus(next)
|
|
|
|
|
: this.focus() }
|
2023-10-26 01:02:27 +03:00
|
|
|
if(l < from.length){
|
|
|
|
|
to ??= []
|
|
|
|
|
to.push(
|
|
|
|
|
...from.splice(l, from.length)) }
|
|
|
|
|
if(from.length == 0){
|
|
|
|
|
from = undefined }
|
|
|
|
|
return [from, to] },
|
|
|
|
|
undo: function(){
|
|
|
|
|
;[this.__undo_stack, this.__redo_stack] =
|
|
|
|
|
this.__undo(this.__undo_stack, this.__redo_stack)
|
|
|
|
|
return this },
|
|
|
|
|
redo: function(){
|
|
|
|
|
;[this.__redo_stack] = this.__undo(this.__redo_stack)
|
|
|
|
|
return this },
|
|
|
|
|
|
2023-10-22 23:28:04 +03:00
|
|
|
// block render...
|
2023-10-09 18:44:56 +03:00
|
|
|
// NOTE: this is auto-populated by .__code2html__(..)
|
|
|
|
|
__styles: undefined,
|
2023-10-22 23:28:04 +03:00
|
|
|
__code2html__: function(code, elem={}){
|
2023-10-09 18:44:56 +03:00
|
|
|
var that = this
|
2023-10-14 02:42:29 +03:00
|
|
|
|
2023-10-09 23:58:32 +03:00
|
|
|
// only whitespace -> keep element blank...
|
|
|
|
|
if(code.trim() == ''){
|
2023-11-02 00:06:39 +03:00
|
|
|
elem.text = code
|
2023-10-09 23:58:32 +03:00
|
|
|
return elem }
|
2023-10-12 23:35:14 +03:00
|
|
|
|
|
|
|
|
// helpers...
|
2023-10-14 02:42:29 +03:00
|
|
|
var run = function(stage, text){
|
|
|
|
|
var meth = {
|
|
|
|
|
pre: '__pre_parse__',
|
|
|
|
|
main: '__parse__',
|
|
|
|
|
post: '__post_parse__',
|
|
|
|
|
}[stage]
|
|
|
|
|
return that.threadPlugins(meth, text, that, elem) }
|
|
|
|
|
|
2023-10-22 23:28:04 +03:00
|
|
|
elem = this.parseBlockAttrs(code, elem)
|
|
|
|
|
code = elem.text
|
|
|
|
|
|
2023-10-14 02:42:29 +03:00
|
|
|
// stage: pre...
|
|
|
|
|
var text = run('pre',
|
|
|
|
|
// pre-sanitize...
|
|
|
|
|
code.replace(/\x00/g, ''))
|
|
|
|
|
// split text into parsable and non-parsable sections...
|
|
|
|
|
var sections = text
|
2023-10-13 22:14:36 +03:00
|
|
|
// split fomat:
|
|
|
|
|
// [ text <match> <type> <body>, ... ]
|
2023-10-14 02:42:29 +03:00
|
|
|
.split(/(<(pre|code)(?:|\s[^>]*)>((?:\n|.)*)<\/\2>)/g)
|
|
|
|
|
// sort out the sections...
|
|
|
|
|
var parsable = []
|
|
|
|
|
var quoted = []
|
|
|
|
|
while(sections.length > 0){
|
|
|
|
|
var [section, match] = sections.splice(0, 4)
|
|
|
|
|
parsable.push(section)
|
|
|
|
|
quoted.push(match) }
|
|
|
|
|
// stage: main...
|
|
|
|
|
text = run('main',
|
|
|
|
|
// parse only the parsable sections...
|
|
|
|
|
parsable.join('\x00'))
|
|
|
|
|
.split(/\x00/g)
|
|
|
|
|
// merge the quoted sections back in...
|
|
|
|
|
.map(function(section){
|
|
|
|
|
return [section, quoted.shift() ?? ''] })
|
|
|
|
|
.flat()
|
|
|
|
|
.join('')
|
|
|
|
|
// stage: post...
|
|
|
|
|
elem.text = run('post', text)
|
2023-10-13 22:14:36 +03:00
|
|
|
|
2023-10-07 07:04:58 +03:00
|
|
|
return elem },
|
2023-10-15 00:03:22 +03:00
|
|
|
// output format...
|
2023-10-11 03:29:20 +03:00
|
|
|
__code2text__: function(code){
|
2023-10-14 22:49:02 +03:00
|
|
|
return code
|
|
|
|
|
.replace(/(\n\s*)-/g, '$1\\-') },
|
2023-10-11 03:29:20 +03:00
|
|
|
__text2code__: function(text){
|
2023-11-02 00:06:39 +03:00
|
|
|
text = text
|
|
|
|
|
.replace(/(\n\s*)\\-/g, '$1-')
|
|
|
|
|
return this.trim_block_text ?
|
|
|
|
|
text.trim()
|
|
|
|
|
: text },
|
2023-09-27 15:05:34 +03:00
|
|
|
|
2023-09-27 15:41:29 +03:00
|
|
|
// serialization...
|
2023-10-10 21:45:24 +03:00
|
|
|
data: function(elem, deep=true){
|
|
|
|
|
elem = this.get(elem)
|
2023-10-22 13:55:41 +03:00
|
|
|
// XXX move these to config...
|
|
|
|
|
var attrs = this.__block_attrs__
|
|
|
|
|
var cls_attrs = ['focused']
|
2023-10-10 21:45:24 +03:00
|
|
|
return {
|
2023-10-26 18:11:12 +03:00
|
|
|
text: elem.querySelector('.code').value,
|
2023-10-22 13:55:41 +03:00
|
|
|
...(Object.entries(attrs)
|
|
|
|
|
.reduce(function(res, [attr, type]){
|
|
|
|
|
if(type == 'attr'){
|
|
|
|
|
var val = elem.getAttribute(attr)
|
|
|
|
|
if(val != null){
|
|
|
|
|
res[attr] = val == '' ?
|
|
|
|
|
true
|
|
|
|
|
: val } }
|
|
|
|
|
if(type == 'cls'){
|
|
|
|
|
elem.classList.contains(attr)
|
|
|
|
|
&& (res[attr] = true) }
|
|
|
|
|
return res }, {})),
|
2023-10-10 21:45:24 +03:00
|
|
|
...(deep ?
|
|
|
|
|
{children: this.json(elem)}
|
|
|
|
|
: {}),
|
|
|
|
|
} },
|
2023-09-27 15:41:29 +03:00
|
|
|
json: function(node){
|
|
|
|
|
var that = this
|
2023-10-14 22:49:02 +03:00
|
|
|
var children = [...(node ?
|
|
|
|
|
node.lastChild.children
|
|
|
|
|
: this.outline.children)]
|
|
|
|
|
return children
|
2023-09-27 15:41:29 +03:00
|
|
|
.map(function(elem){
|
2023-10-12 19:45:12 +03:00
|
|
|
return that.data(elem) }) },
|
2023-10-08 02:36:29 +03:00
|
|
|
// XXX add option to customize indent size...
|
2023-09-28 20:49:06 +03:00
|
|
|
text: function(node, indent, level){
|
|
|
|
|
// .text(<indent>, <level>)
|
|
|
|
|
if(typeof(node) == 'string'){
|
|
|
|
|
;[node, indent=' ', level=''] = [undefined, ...arguments] }
|
2023-09-27 15:41:29 +03:00
|
|
|
node ??= this.json(node)
|
2023-09-28 20:49:06 +03:00
|
|
|
indent ??= ' '
|
|
|
|
|
level ??= ''
|
2023-10-07 17:06:54 +03:00
|
|
|
var text = []
|
2023-09-27 15:41:29 +03:00
|
|
|
for(var elem of node){
|
2023-10-07 17:06:54 +03:00
|
|
|
text.push(
|
|
|
|
|
level +'- '
|
2023-10-14 22:49:02 +03:00
|
|
|
+ this.__code2text__(elem.text)
|
2023-10-07 17:06:54 +03:00
|
|
|
.replace(/\n/g, '\n'+ level +' ')
|
2023-10-22 13:55:41 +03:00
|
|
|
// attrs...
|
|
|
|
|
+ (Object.keys(elem)
|
|
|
|
|
.reduce(function(res, attr){
|
|
|
|
|
return (attr == 'text'
|
|
|
|
|
|| attr == 'children') ?
|
|
|
|
|
res
|
|
|
|
|
: res
|
|
|
|
|
+ (elem[attr] ?
|
|
|
|
|
'\n'+level+' ' + `${ attr }:: ${ elem[attr] }`
|
|
|
|
|
: '') }, '')),
|
2023-10-07 17:06:54 +03:00
|
|
|
(elem.children
|
|
|
|
|
&& elem.children.length > 0) ?
|
|
|
|
|
this.text(elem.children || [], indent, level+indent)
|
|
|
|
|
: [] ) }
|
|
|
|
|
return text
|
|
|
|
|
.flat()
|
|
|
|
|
.join('\n') },
|
2023-09-27 15:41:29 +03:00
|
|
|
|
2023-10-22 23:28:04 +03:00
|
|
|
//
|
|
|
|
|
// Parse attrs...
|
|
|
|
|
// .parseBlockAttrs(<text>[, <elem>])
|
|
|
|
|
// -> <elem>
|
|
|
|
|
//
|
|
|
|
|
// Parse attrs keeping non-system attrs in .text...
|
|
|
|
|
// .parseBlockAttrs(<text>, true[, <elem>])
|
|
|
|
|
// -> <elem>
|
|
|
|
|
//
|
|
|
|
|
// Parse attrs keeping all attrs in .text...
|
|
|
|
|
// .parseBlockAttrs(<text>, 'all'[, <elem>])
|
|
|
|
|
// -> <elem>
|
|
|
|
|
//
|
|
|
|
|
parseBlockAttrs: function(text, keep=false, elem={}){
|
|
|
|
|
if(typeof(keep) == 'object'){
|
|
|
|
|
elem = keep
|
|
|
|
|
keep = typeof(elem) == 'boolean' ?
|
|
|
|
|
elem
|
|
|
|
|
: false }
|
|
|
|
|
var system = this.__block_attrs__
|
|
|
|
|
var clean = text
|
|
|
|
|
// XXX for some reason changing the first group into (?<= .. )
|
|
|
|
|
// still eats up the whitespace...
|
|
|
|
|
// ...putting the same pattern in a normal group and
|
|
|
|
|
// returning it works fine...
|
|
|
|
|
//.replace(/(?<=[\n\h]*)(?:(?:\n|^)\s*\w*\s*::\s*[^\n]*\s*)*$/,
|
2023-10-25 15:18:30 +03:00
|
|
|
.replace(/([\n\t ]*)(?:(?:\n|^)[\t ]*\w+[\t ]*::[\t ]*[^\n]+[\t ]*)+$/,
|
2023-10-22 23:28:04 +03:00
|
|
|
function(match, ws){
|
|
|
|
|
var attrs = match
|
|
|
|
|
.trim()
|
|
|
|
|
.split(/(?:[\t ]*::[\t ]*|[\t ]*\n[\t ]*)/g)
|
|
|
|
|
while(attrs.length > 0){
|
|
|
|
|
var [name, val] = attrs.splice(0, 2)
|
|
|
|
|
elem[name] =
|
|
|
|
|
val == 'true' ?
|
|
|
|
|
true
|
|
|
|
|
: val == 'false' ?
|
|
|
|
|
false
|
|
|
|
|
: val
|
|
|
|
|
// keep non-system attrs...
|
|
|
|
|
if(keep
|
|
|
|
|
&& !(name in system)){
|
|
|
|
|
ws += `\n${name}::${val}` } }
|
|
|
|
|
return ws })
|
|
|
|
|
elem.text = keep == 'all' ?
|
|
|
|
|
text
|
|
|
|
|
: clean
|
|
|
|
|
return elem },
|
2023-10-07 07:04:58 +03:00
|
|
|
parse: function(text){
|
2023-10-14 22:49:02 +03:00
|
|
|
var that = this
|
2023-10-07 17:06:54 +03:00
|
|
|
text = text
|
2023-10-29 15:53:05 +03:00
|
|
|
.replace(/^[ \t]*\n/, '')
|
2023-10-07 07:04:58 +03:00
|
|
|
text = ('\n' + text)
|
2023-10-29 15:53:05 +03:00
|
|
|
.split(/\n([ \t]*)(?:- |-\s*$)/gm)
|
2023-10-07 07:04:58 +03:00
|
|
|
.slice(1)
|
2023-10-08 02:36:29 +03:00
|
|
|
var tab = ' '.repeat(this.tab_size || 8)
|
2023-10-07 07:04:58 +03:00
|
|
|
var level = function(lst, prev_sep=undefined, parent=[]){
|
|
|
|
|
while(lst.length > 0){
|
2023-10-11 07:16:18 +03:00
|
|
|
sep = lst[0].replace(/\t/gm, tab)
|
2023-10-07 07:04:58 +03:00
|
|
|
// deindent...
|
|
|
|
|
if(prev_sep != null
|
|
|
|
|
&& sep.length < prev_sep.length){
|
|
|
|
|
break }
|
|
|
|
|
prev_sep ??= sep
|
|
|
|
|
// same level...
|
|
|
|
|
if(sep.length == prev_sep.length){
|
|
|
|
|
var [_, block] = lst.splice(0, 2)
|
2023-10-22 23:28:04 +03:00
|
|
|
var attrs = that.parseBlockAttrs(block)
|
|
|
|
|
attrs.text = that.__text2code__(attrs.text
|
|
|
|
|
// normalize indent...
|
|
|
|
|
.split(new RegExp('\n'+sep+' ', 'g'))
|
|
|
|
|
.join('\n'))
|
|
|
|
|
parent.push({
|
2023-10-22 13:55:41 +03:00
|
|
|
collapsed: false,
|
|
|
|
|
focused: false,
|
|
|
|
|
...attrs,
|
2023-10-07 07:04:58 +03:00
|
|
|
children: [],
|
|
|
|
|
})
|
|
|
|
|
// indent...
|
|
|
|
|
} else {
|
|
|
|
|
parent.at(-1).children = level(lst, sep) } }
|
|
|
|
|
return parent }
|
|
|
|
|
return level(text) },
|
|
|
|
|
|
2023-10-07 17:06:54 +03:00
|
|
|
// XXX should this handle children???
|
2023-10-07 07:04:58 +03:00
|
|
|
// XXX revise name...
|
|
|
|
|
Block: function(data={}, place=null){
|
2023-10-12 19:45:12 +03:00
|
|
|
var that = this
|
2023-10-07 07:04:58 +03:00
|
|
|
if(typeof(data) != 'object'){
|
|
|
|
|
place = data
|
|
|
|
|
data = {} }
|
2023-10-12 19:45:12 +03:00
|
|
|
|
|
|
|
|
// block...
|
2023-10-07 07:04:58 +03:00
|
|
|
var block = document.createElement('div')
|
2023-10-12 19:45:12 +03:00
|
|
|
block.classList.add('block')
|
2023-10-07 07:04:58 +03:00
|
|
|
block.setAttribute('tabindex', '0')
|
2023-10-25 17:37:02 +03:00
|
|
|
// XXX hack??
|
|
|
|
|
block.setAttribute('cropped', '')
|
2023-10-12 19:45:12 +03:00
|
|
|
// code...
|
|
|
|
|
var code = document.createElement('textarea')
|
2023-10-09 18:44:56 +03:00
|
|
|
.autoUpdateSize()
|
2023-10-12 19:45:12 +03:00
|
|
|
code.classList.add('code', 'text')
|
|
|
|
|
// view...
|
2023-10-07 17:06:54 +03:00
|
|
|
var html = document.createElement('span')
|
2023-10-12 19:45:12 +03:00
|
|
|
html.classList.add('view', 'text')
|
|
|
|
|
// children...
|
|
|
|
|
var children = document.createElement('div')
|
|
|
|
|
children.classList.add('children')
|
|
|
|
|
children.setAttribute('tabindex', '-1')
|
2023-11-02 06:53:23 +03:00
|
|
|
block.append(
|
|
|
|
|
code,
|
|
|
|
|
html,
|
|
|
|
|
children)
|
2023-10-22 23:28:04 +03:00
|
|
|
|
2023-10-08 02:36:29 +03:00
|
|
|
this.update(block, data)
|
2023-10-12 19:45:12 +03:00
|
|
|
|
2023-10-09 01:32:46 +03:00
|
|
|
// place...
|
2023-10-07 07:04:58 +03:00
|
|
|
var cur = this.get()
|
2023-10-09 01:32:46 +03:00
|
|
|
if(place && cur){
|
|
|
|
|
place = place == 'prev' ?
|
|
|
|
|
'before'
|
|
|
|
|
: place
|
2023-10-26 23:05:16 +03:00
|
|
|
// ... ...
|
|
|
|
|
// cur cur
|
|
|
|
|
// new new <- before the next after cur
|
|
|
|
|
// --- ---
|
|
|
|
|
// ... ...
|
2023-10-09 01:32:46 +03:00
|
|
|
;(place == 'next'
|
2023-10-26 23:05:16 +03:00
|
|
|
// has children (uncollapsed)...
|
|
|
|
|
&& (cur.querySelector('.block')?.offsetParent
|
|
|
|
|
// not last sibling...
|
|
|
|
|
|| cur !== this.get('siblings').at(-1))) ?
|
2023-10-09 01:32:46 +03:00
|
|
|
this.get(place).before(block)
|
2023-10-26 23:05:16 +03:00
|
|
|
// ...
|
|
|
|
|
// ---
|
|
|
|
|
// cur
|
|
|
|
|
// new <- next after cur
|
|
|
|
|
// ...
|
2023-10-09 18:44:56 +03:00
|
|
|
: (place == 'next'
|
2023-10-26 23:05:16 +03:00
|
|
|
// last sibling...
|
|
|
|
|
&& cur === this.get('siblings').at(-1)) ?
|
2023-10-09 18:44:56 +03:00
|
|
|
cur.after(block)
|
2023-10-09 01:32:46 +03:00
|
|
|
: (place == 'before' || place == 'after') ?
|
|
|
|
|
cur[place](block)
|
2023-10-30 02:52:38 +03:00
|
|
|
: undefined
|
|
|
|
|
|
|
|
|
|
this.setUndo(this.path(cur), 'remove', [this.path(block)]) }
|
2023-10-07 07:04:58 +03:00
|
|
|
return block },
|
2023-10-23 16:47:19 +03:00
|
|
|
// XXX see inside...
|
2023-10-04 15:33:07 +03:00
|
|
|
load: function(data){
|
2023-10-07 17:06:54 +03:00
|
|
|
var that = this
|
2023-10-07 07:04:58 +03:00
|
|
|
data = typeof(data) == 'string' ?
|
2023-10-23 04:26:00 +03:00
|
|
|
this.parse(data)
|
|
|
|
|
: data instanceof Array ?
|
|
|
|
|
data
|
|
|
|
|
: [data]
|
2023-10-06 17:53:28 +03:00
|
|
|
// generate dom...
|
2023-10-07 17:06:54 +03:00
|
|
|
var level = function(lst){
|
|
|
|
|
return lst
|
|
|
|
|
.map(function(data){
|
|
|
|
|
var elem = that.Block(data)
|
|
|
|
|
if((data.children || []).length > 0){
|
2023-10-12 19:45:12 +03:00
|
|
|
elem.lastChild
|
|
|
|
|
.append(...level(data.children)) }
|
2023-10-07 17:06:54 +03:00
|
|
|
return elem }) }
|
|
|
|
|
this
|
|
|
|
|
.clear()
|
|
|
|
|
.outline
|
|
|
|
|
.append(...level(data))
|
2023-10-09 01:32:46 +03:00
|
|
|
// update sizes of all the textareas (transparent)...
|
2023-10-28 22:52:46 +03:00
|
|
|
// NOTE: this is needed to make initial clicking into multi-line
|
|
|
|
|
// blocks place the cursor into the clicked location.
|
|
|
|
|
// ...this is done by expanding the textarea to the element
|
|
|
|
|
// size and enabling it to intercept clicks correctly...
|
2023-10-23 16:47:19 +03:00
|
|
|
setTimeout(function(){
|
|
|
|
|
for(var e of [...that.outline.querySelectorAll('textarea')]){
|
|
|
|
|
e.updateSize() } }, 0)
|
2023-10-23 14:37:14 +03:00
|
|
|
// restore focus...
|
|
|
|
|
this.focus()
|
2023-10-07 17:06:54 +03:00
|
|
|
return this },
|
|
|
|
|
|
|
|
|
|
sync: function(){
|
2023-10-28 22:52:46 +03:00
|
|
|
this.code = this.text()
|
2023-10-06 17:53:28 +03:00
|
|
|
return this },
|
2023-09-27 15:05:34 +03:00
|
|
|
|
2023-10-14 22:49:02 +03:00
|
|
|
|
|
|
|
|
// Actions...
|
|
|
|
|
prev: function(){},
|
|
|
|
|
next: function(){},
|
|
|
|
|
above: function(){},
|
|
|
|
|
below: function(){},
|
|
|
|
|
|
|
|
|
|
up: function(){},
|
|
|
|
|
down: function(){},
|
|
|
|
|
left: function(){},
|
|
|
|
|
right: function(){},
|
|
|
|
|
|
2023-10-12 23:35:14 +03:00
|
|
|
// XXX move the code here into methods/actions...
|
|
|
|
|
// XXX use keyboard.js...
|
2023-10-23 19:38:06 +03:00
|
|
|
__overtravel_timeout: undefined,
|
2023-09-27 15:05:34 +03:00
|
|
|
keyboard: {
|
|
|
|
|
// vertical navigation...
|
2023-10-11 07:16:18 +03:00
|
|
|
// XXX this is a bit hacky but it works -- the caret blinks at
|
|
|
|
|
// start/end of block before switching to next, would be
|
|
|
|
|
// nice po prevent this...
|
2023-09-27 15:05:34 +03:00
|
|
|
ArrowUp: function(evt){
|
2023-10-11 07:16:18 +03:00
|
|
|
var that = this
|
2023-10-23 19:38:06 +03:00
|
|
|
|
|
|
|
|
// overtravel...
|
|
|
|
|
var overtravel =
|
|
|
|
|
this.__overtravel_timeout != null
|
|
|
|
|
&& this.get() === this.get(0)
|
|
|
|
|
this.__overtravel_timeout != null
|
|
|
|
|
&& clearTimeout(this.__overtravel_timeout)
|
|
|
|
|
this.__overtravel_timeout = setTimeout(function(){
|
|
|
|
|
that.__overtravel_timeout = undefined }, 100)
|
|
|
|
|
if(overtravel){
|
|
|
|
|
return }
|
|
|
|
|
|
2023-09-28 20:49:06 +03:00
|
|
|
var edited = this.get('edited')
|
2023-09-27 15:05:34 +03:00
|
|
|
if(edited){
|
2023-10-14 22:49:02 +03:00
|
|
|
var line = edited.getTextGeometry().line
|
|
|
|
|
if(line == 0){
|
|
|
|
|
evt.preventDefault()
|
|
|
|
|
that.focus('edited', 'prev') }
|
2023-10-11 07:16:18 +03:00
|
|
|
} else {
|
|
|
|
|
evt.preventDefault()
|
2023-10-11 07:31:06 +03:00
|
|
|
this.focus('focused', -1) } },
|
2023-10-11 07:16:18 +03:00
|
|
|
ArrowDown: function(evt){
|
|
|
|
|
var that = this
|
2023-10-23 19:38:06 +03:00
|
|
|
|
|
|
|
|
// overtravel...
|
|
|
|
|
var overtravel =
|
|
|
|
|
this.__overtravel_timeout != null
|
|
|
|
|
&& this.get() === this.get(-1)
|
|
|
|
|
this.__overtravel_timeout != null
|
|
|
|
|
&& clearTimeout(this.__overtravel_timeout)
|
|
|
|
|
this.__overtravel_timeout = setTimeout(function(){
|
|
|
|
|
that.__overtravel_timeout = undefined }, 100)
|
|
|
|
|
if(overtravel){
|
|
|
|
|
return }
|
|
|
|
|
|
2023-09-28 20:49:06 +03:00
|
|
|
var edited = this.get('edited')
|
2023-09-27 15:05:34 +03:00
|
|
|
if(edited){
|
2023-10-14 22:49:02 +03:00
|
|
|
var {line, lines} = edited.getTextGeometry()
|
2023-10-23 16:47:19 +03:00
|
|
|
if(line == lines - 1){
|
2023-10-14 22:49:02 +03:00
|
|
|
evt.preventDefault()
|
|
|
|
|
that.focus('edited', 'next') }
|
2023-10-11 07:16:18 +03:00
|
|
|
} else {
|
|
|
|
|
evt.preventDefault()
|
2023-10-11 07:31:06 +03:00
|
|
|
this.focus('focused', 1) } },
|
2023-09-27 15:05:34 +03:00
|
|
|
// horizontal navigation / collapse...
|
|
|
|
|
ArrowLeft: function(evt){
|
2023-10-07 07:04:58 +03:00
|
|
|
var edited = this.get('edited')
|
|
|
|
|
if(edited){
|
|
|
|
|
// move caret to prev element...
|
|
|
|
|
if(edited.selectionStart == edited.selectionEnd
|
|
|
|
|
&& edited.selectionStart == 0){
|
|
|
|
|
evt.preventDefault()
|
2023-10-11 07:31:06 +03:00
|
|
|
edited = this.focus('edited', 'prev')
|
2023-10-07 07:04:58 +03:00
|
|
|
edited.selectionStart =
|
|
|
|
|
edited.selectionEnd = edited.value.length + 1 }
|
2023-09-27 15:05:34 +03:00
|
|
|
return }
|
2023-10-18 18:40:04 +03:00
|
|
|
if(evt.ctrlKey){
|
|
|
|
|
evt.preventDefault()
|
|
|
|
|
tasks.prevCheckbox(this)
|
|
|
|
|
return }
|
2023-09-29 15:38:17 +03:00
|
|
|
;((this.left_key_collapses
|
|
|
|
|
|| evt.shiftKey)
|
|
|
|
|
&& this.get().getAttribute('collapsed') == null
|
|
|
|
|
&& this.get('children').length > 0) ?
|
|
|
|
|
this.toggleCollapse(true)
|
2023-10-11 07:31:06 +03:00
|
|
|
: this.focus('parent') },
|
2023-09-27 15:05:34 +03:00
|
|
|
ArrowRight: function(evt){
|
2023-10-23 19:38:06 +03:00
|
|
|
var that = this
|
|
|
|
|
|
|
|
|
|
// overtravel...
|
|
|
|
|
var overtravel =
|
|
|
|
|
this.__overtravel_timeout != null
|
|
|
|
|
&& this.get() === this.get(-1)
|
|
|
|
|
this.__overtravel_timeout != null
|
|
|
|
|
&& clearTimeout(this.__overtravel_timeout)
|
|
|
|
|
this.__overtravel_timeout = setTimeout(function(){
|
|
|
|
|
that.__overtravel_timeout = undefined }, 100)
|
|
|
|
|
if(overtravel){
|
|
|
|
|
return }
|
|
|
|
|
|
2023-10-07 07:04:58 +03:00
|
|
|
var edited = this.get('edited')
|
|
|
|
|
if(edited){
|
|
|
|
|
// move caret to next element...
|
|
|
|
|
if(edited.selectionStart == edited.selectionEnd
|
|
|
|
|
&& edited.selectionStart == edited.value.length){
|
|
|
|
|
evt.preventDefault()
|
2023-10-11 07:31:06 +03:00
|
|
|
edited = this.focus('edited', 'next')
|
2023-10-07 07:04:58 +03:00
|
|
|
edited.selectionStart =
|
|
|
|
|
edited.selectionEnd = 0 }
|
2023-09-27 15:05:34 +03:00
|
|
|
return }
|
2023-10-18 18:40:04 +03:00
|
|
|
if(evt.ctrlKey){
|
|
|
|
|
evt.preventDefault()
|
|
|
|
|
tasks.nextCheckbox(this)
|
|
|
|
|
return }
|
2023-09-29 15:38:17 +03:00
|
|
|
if(this.right_key_expands){
|
2023-09-28 20:49:06 +03:00
|
|
|
this.toggleCollapse(false)
|
2023-10-12 19:45:12 +03:00
|
|
|
this.focus('next')
|
2023-09-27 15:05:34 +03:00
|
|
|
} else {
|
|
|
|
|
evt.shiftKey ?
|
2023-09-28 20:49:06 +03:00
|
|
|
this.toggleCollapse(false)
|
2023-10-12 19:45:12 +03:00
|
|
|
: this.focus('next') } },
|
2023-09-27 15:05:34 +03:00
|
|
|
|
2023-10-22 23:57:19 +03:00
|
|
|
Home: function(evt){
|
2023-10-23 00:14:47 +03:00
|
|
|
if(this.get('edited')
|
|
|
|
|
&& !evt.ctrlKey){
|
2023-10-22 23:57:19 +03:00
|
|
|
return }
|
2023-10-23 00:14:47 +03:00
|
|
|
evt.preventDefault()
|
2023-10-22 23:57:19 +03:00
|
|
|
this.focus(0) },
|
|
|
|
|
End: function(evt){
|
2023-10-23 00:14:47 +03:00
|
|
|
if(this.get('edited')
|
|
|
|
|
&& !evt.ctrlKey){
|
2023-10-22 23:57:19 +03:00
|
|
|
return }
|
2023-10-23 00:14:47 +03:00
|
|
|
evt.preventDefault()
|
2023-10-22 23:57:19 +03:00
|
|
|
this.focus(-1) },
|
2023-10-15 00:03:22 +03:00
|
|
|
PageUp: function(evt){
|
2023-10-23 14:51:27 +03:00
|
|
|
var that = this
|
2023-10-26 01:02:27 +03:00
|
|
|
if(this.get('edited')){
|
2023-10-23 14:51:27 +03:00
|
|
|
return }
|
|
|
|
|
if(evt.shiftKey
|
|
|
|
|
|| evt.ctrlKey){
|
2023-10-15 00:03:22 +03:00
|
|
|
evt.preventDefault()
|
2023-10-23 14:51:27 +03:00
|
|
|
this.shift('up')
|
|
|
|
|
} else {
|
2023-10-23 15:34:24 +03:00
|
|
|
var viewport = that.get('viewport')
|
|
|
|
|
viewport[0] === that.get(0) ?
|
|
|
|
|
that.focus(0)
|
|
|
|
|
: that.focus(
|
|
|
|
|
viewport[0], 'prev') } },
|
2023-10-15 00:03:22 +03:00
|
|
|
PageDown: function(evt){
|
2023-10-23 14:51:27 +03:00
|
|
|
var that = this
|
2023-10-26 01:02:27 +03:00
|
|
|
if(this.get('edited')){
|
2023-10-23 14:51:27 +03:00
|
|
|
return }
|
|
|
|
|
if(evt.shiftKey
|
|
|
|
|
|| evt.ctrlKey){
|
2023-10-15 00:03:22 +03:00
|
|
|
evt.preventDefault()
|
2023-10-23 14:51:27 +03:00
|
|
|
this.shift('down')
|
|
|
|
|
} else {
|
2023-10-23 15:34:24 +03:00
|
|
|
var viewport = that.get('viewport')
|
|
|
|
|
viewport.at(-1) === that.get(-1) ?
|
|
|
|
|
that.focus(-1)
|
|
|
|
|
: that.focus(
|
|
|
|
|
that.get('viewport').at(-1), 'next') } },
|
2023-10-15 00:03:22 +03:00
|
|
|
|
2023-10-22 23:57:19 +03:00
|
|
|
// indent..
|
2023-09-27 15:05:34 +03:00
|
|
|
Tab: function(evt){
|
|
|
|
|
evt.preventDefault()
|
2023-09-29 02:47:07 +03:00
|
|
|
var edited = this.get('edited')
|
2023-10-12 19:45:12 +03:00
|
|
|
var node = this.show(
|
2023-10-26 01:02:27 +03:00
|
|
|
this.indent(evt.shiftKey ?
|
|
|
|
|
'out'
|
|
|
|
|
: 'in'))
|
2023-10-12 19:45:12 +03:00
|
|
|
// keep focus in node...
|
2023-09-29 02:47:07 +03:00
|
|
|
;(edited ?
|
|
|
|
|
edited
|
2023-09-27 15:05:34 +03:00
|
|
|
: node)?.focus() },
|
|
|
|
|
|
|
|
|
|
// edit mode...
|
|
|
|
|
O: function(evt){
|
2023-10-15 00:03:22 +03:00
|
|
|
if(!this.get('edited')){
|
2023-09-27 15:05:34 +03:00
|
|
|
evt.preventDefault()
|
2023-10-15 00:03:22 +03:00
|
|
|
this.edit(
|
|
|
|
|
this.Block('before')) } },
|
2023-09-27 15:05:34 +03:00
|
|
|
o: function(evt){
|
2023-10-15 00:03:22 +03:00
|
|
|
if(!this.get('edited')){
|
2023-09-27 15:05:34 +03:00
|
|
|
evt.preventDefault()
|
2023-10-15 00:03:22 +03:00
|
|
|
this.edit(
|
|
|
|
|
this.Block('next')) } },
|
2023-09-27 15:05:34 +03:00
|
|
|
Enter: function(evt){
|
2023-10-15 00:03:22 +03:00
|
|
|
var edited = this.get('edited')
|
|
|
|
|
if(edited){
|
|
|
|
|
if(evt.ctrlKey
|
|
|
|
|
|| evt.shiftKey){
|
2023-11-02 06:53:23 +03:00
|
|
|
var that = this
|
|
|
|
|
// NOTE: setTimeout(..) because we need the input of
|
|
|
|
|
// the key...
|
|
|
|
|
setTimeout(function(){
|
|
|
|
|
that.update(edited) }, 0)
|
2023-10-15 00:03:22 +03:00
|
|
|
return }
|
2023-10-29 16:16:21 +03:00
|
|
|
// split text...
|
2023-10-15 00:03:22 +03:00
|
|
|
evt.preventDefault()
|
|
|
|
|
var a = edited.selectionStart
|
|
|
|
|
var b = edited.selectionEnd
|
2023-10-30 02:52:38 +03:00
|
|
|
// position 0: focus empty node above...
|
|
|
|
|
if(a == 0){
|
|
|
|
|
this.Block('prev')
|
|
|
|
|
this.edit('prev')
|
|
|
|
|
// focus new node...
|
|
|
|
|
} else {
|
|
|
|
|
var prev = edited.value.slice(0, a)
|
|
|
|
|
var next = edited.value.slice(b)
|
|
|
|
|
edited.value = prev
|
|
|
|
|
this.Block({text: next}, 'next')
|
2023-10-29 16:16:21 +03:00
|
|
|
edited = this.edit('next')
|
|
|
|
|
edited.selectionStart = 0
|
2023-10-30 02:52:38 +03:00
|
|
|
edited.selectionEnd = 0
|
|
|
|
|
this.mergeUndo(2) }
|
2023-09-27 15:05:34 +03:00
|
|
|
return }
|
2023-10-15 00:03:22 +03:00
|
|
|
// view -> edit...
|
2023-09-27 15:05:34 +03:00
|
|
|
evt.preventDefault()
|
2023-10-15 00:03:22 +03:00
|
|
|
this.edit() },
|
2023-09-27 15:05:34 +03:00
|
|
|
Escape: function(evt){
|
2023-10-23 04:26:00 +03:00
|
|
|
if(this.get('edited')){
|
|
|
|
|
this.focus()
|
|
|
|
|
} else {
|
2023-10-26 01:02:27 +03:00
|
|
|
this.uncrop() } },
|
|
|
|
|
s_Escape: function(evt){
|
|
|
|
|
if(this.get('edited')){
|
|
|
|
|
this.focus()
|
|
|
|
|
} else {
|
|
|
|
|
this.uncrop('all') } },
|
2023-10-23 04:26:00 +03:00
|
|
|
c: function(evt){
|
|
|
|
|
if(!this.get('edited')){
|
|
|
|
|
this.crop() } },
|
2023-10-26 01:02:27 +03:00
|
|
|
c_z: function(evt){
|
|
|
|
|
if(!this.get('edited')){
|
|
|
|
|
evt.preventDefault()
|
|
|
|
|
this.undo() } },
|
|
|
|
|
c_s_z: function(evt){
|
|
|
|
|
if(!this.get('edited')){
|
|
|
|
|
evt.preventDefault()
|
|
|
|
|
this.redo() } },
|
|
|
|
|
U: function(evt){
|
|
|
|
|
if(!this.get('edited')){
|
|
|
|
|
this.redo() } },
|
|
|
|
|
u: function(evt){
|
|
|
|
|
if(!this.get('edited')){
|
|
|
|
|
this.undo() } },
|
2023-10-15 00:03:22 +03:00
|
|
|
|
2023-09-27 15:05:34 +03:00
|
|
|
Delete: function(evt){
|
2023-10-15 00:03:22 +03:00
|
|
|
var edited = this.get('edited')
|
|
|
|
|
if(edited){
|
|
|
|
|
if(edited.selectionStart == edited.value.length){
|
|
|
|
|
var next = this.get('edited', 'next')
|
|
|
|
|
// can't reclaim nested children...
|
|
|
|
|
if(this.get(next, 'children').length > 0){
|
|
|
|
|
return }
|
|
|
|
|
// do not delete past the top element...
|
|
|
|
|
if(this.get(0).querySelector('.code') === next){
|
|
|
|
|
return }
|
|
|
|
|
evt.preventDefault()
|
|
|
|
|
var i = edited.value.length
|
|
|
|
|
edited.value += next.value
|
|
|
|
|
edited.selectionStart = i
|
|
|
|
|
edited.selectionEnd = i
|
|
|
|
|
this.remove(next) }
|
2023-09-27 15:05:34 +03:00
|
|
|
return }
|
2023-10-03 16:32:29 +03:00
|
|
|
this.remove() },
|
2023-10-15 00:03:22 +03:00
|
|
|
Backspace: function(evt){
|
|
|
|
|
var edited = this.get('edited')
|
|
|
|
|
if(edited
|
2023-10-15 01:16:24 +03:00
|
|
|
&& edited.selectionEnd == 0
|
2023-10-15 00:03:22 +03:00
|
|
|
// can't reclaim nested children...
|
|
|
|
|
&& this.get(edited, 'children').length == 0){
|
|
|
|
|
var prev = this.get('edited', 'prev')
|
|
|
|
|
// do not delete past the bottom element...
|
|
|
|
|
if(this.get(-1).querySelector('.code') === prev){
|
|
|
|
|
return }
|
|
|
|
|
evt.preventDefault()
|
|
|
|
|
var i = prev.value.length
|
|
|
|
|
prev.value += edited.value
|
|
|
|
|
this.edit(prev)
|
|
|
|
|
prev.selectionStart = i
|
|
|
|
|
prev.selectionEnd = i
|
|
|
|
|
this.remove(edited)
|
|
|
|
|
return } },
|
2023-09-30 17:27:28 +03:00
|
|
|
|
2023-10-30 18:06:13 +03:00
|
|
|
a_s: function(evt){
|
|
|
|
|
// toggle done...
|
|
|
|
|
evt.preventDefault()
|
|
|
|
|
tasks.toggleStatus(this) },
|
2023-10-28 02:42:47 +03:00
|
|
|
a_x: function(evt){
|
2023-10-18 19:48:00 +03:00
|
|
|
// toggle done...
|
2023-10-26 01:02:27 +03:00
|
|
|
evt.preventDefault()
|
|
|
|
|
tasks.toggleDone(this) },
|
2023-10-30 18:06:13 +03:00
|
|
|
a_r: function(evt){
|
|
|
|
|
// toggle done...
|
|
|
|
|
evt.preventDefault()
|
|
|
|
|
tasks.toggleReject(this) },
|
2023-10-18 19:48:00 +03:00
|
|
|
|
2023-10-27 14:47:56 +03:00
|
|
|
// selection...
|
|
|
|
|
// XXX need more work...
|
|
|
|
|
// - should we select the .block or .text???
|
|
|
|
|
// - we should remember the first state and apply it (a-la FAR)
|
|
|
|
|
// and not simply toggle on/off per node...
|
2023-10-28 02:42:47 +03:00
|
|
|
Shift: function(evt){
|
|
|
|
|
if(this.get('edited')){
|
|
|
|
|
return }
|
|
|
|
|
// XXX set selection mode
|
|
|
|
|
// ...need to reset this when shift key is released...
|
|
|
|
|
// one way to do this is to save a press id and reset
|
|
|
|
|
// it each call -- if the id has changed since lass s-up
|
|
|
|
|
// is pressed then reset mode...
|
|
|
|
|
},
|
2023-10-27 14:47:56 +03:00
|
|
|
s_ArrowUp: function(evt){
|
|
|
|
|
if(this.get('edited')){
|
|
|
|
|
return }
|
|
|
|
|
var elem = this.get()
|
|
|
|
|
elem.hasAttribute('selected') ?
|
|
|
|
|
elem.removeAttribute('selected')
|
|
|
|
|
: elem.setAttribute('selected', '')
|
|
|
|
|
this.keyboard.ArrowUp.call(this, evt) },
|
|
|
|
|
s_ArrowDown: function(evt){
|
|
|
|
|
if(this.get('edited')){
|
|
|
|
|
return }
|
|
|
|
|
var elem = this.get()
|
|
|
|
|
elem.hasAttribute('selected') ?
|
|
|
|
|
elem.removeAttribute('selected')
|
|
|
|
|
: elem.setAttribute('selected', '')
|
|
|
|
|
this.keyboard.ArrowDown.call(this, evt) },
|
2023-10-28 02:42:47 +03:00
|
|
|
c_d: function(evt){
|
|
|
|
|
if(this.get('edited')){
|
|
|
|
|
return }
|
|
|
|
|
evt.preventDefault()
|
|
|
|
|
for(var e of this.get('selected')){
|
|
|
|
|
e.removeAttribute('selected') } },
|
|
|
|
|
c_a: function(evt){
|
|
|
|
|
if(this.get('edited')){
|
|
|
|
|
return }
|
|
|
|
|
evt.preventDefault()
|
|
|
|
|
for(var e of this.get('all')){
|
|
|
|
|
e.setAttribute('selected', '') } },
|
2023-10-27 14:47:56 +03:00
|
|
|
|
2023-10-18 19:48:00 +03:00
|
|
|
// toggle checkbox...
|
2023-09-30 17:27:28 +03:00
|
|
|
' ': function(evt){
|
|
|
|
|
if(this.get('edited') != null){
|
|
|
|
|
return }
|
|
|
|
|
evt.preventDefault()
|
2023-10-18 19:48:00 +03:00
|
|
|
tasks.toggleCheckbox(this) },
|
2023-09-27 15:05:34 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
setup: function(dom){
|
|
|
|
|
var that = this
|
|
|
|
|
this.dom = dom
|
2023-10-04 15:40:29 +03:00
|
|
|
|
|
|
|
|
// outline...
|
2023-10-04 15:33:07 +03:00
|
|
|
var outline = this.outline
|
2023-09-27 15:05:34 +03:00
|
|
|
// update stuff already in DOM...
|
2023-10-03 16:12:19 +03:00
|
|
|
for(var elem of [...outline.querySelectorAll('textarea')]){
|
2023-09-27 15:05:34 +03:00
|
|
|
elem.autoUpdateSize() }
|
2023-10-08 17:42:44 +03:00
|
|
|
// click...
|
2023-10-18 23:30:15 +03:00
|
|
|
// XXX revise...
|
|
|
|
|
// XXX tap support...
|
|
|
|
|
outline.addEventListener('mousedown',
|
|
|
|
|
function(evt){
|
|
|
|
|
var elem = evt.target
|
2023-10-19 00:08:21 +03:00
|
|
|
// place the cursor where the user clicked in code/text...
|
2023-10-18 23:50:01 +03:00
|
|
|
if(elem.classList.contains('code')
|
|
|
|
|
&& document.activeElement !== elem){
|
|
|
|
|
evt.preventDefault()
|
2023-10-18 23:30:15 +03:00
|
|
|
var view = that.get(elem).querySelector('.view')
|
|
|
|
|
var c = getCharOffset(view, evt.clientX, evt.clientY)
|
2023-10-18 23:50:01 +03:00
|
|
|
if(c == null){
|
|
|
|
|
elem.focus()
|
|
|
|
|
elem.selectionStart = elem.value.length
|
|
|
|
|
elem.selectionEnd = elem.value.length
|
|
|
|
|
} else {
|
2023-10-18 23:30:15 +03:00
|
|
|
var m = getMarkdownOffset(elem.value, view.innerText, c)
|
|
|
|
|
elem.focus()
|
|
|
|
|
elem.selectionStart = c + m
|
|
|
|
|
elem.selectionEnd = c + m } } })
|
2023-10-08 17:42:44 +03:00
|
|
|
outline.addEventListener('click',
|
|
|
|
|
function(evt){
|
|
|
|
|
var elem = evt.target
|
2023-10-09 19:37:08 +03:00
|
|
|
|
2023-10-14 02:42:29 +03:00
|
|
|
// prevent focusing parent by clicking between blocks...
|
2023-10-12 19:45:12 +03:00
|
|
|
if(elem.classList.contains('children')){
|
|
|
|
|
return }
|
|
|
|
|
|
2023-10-13 22:14:36 +03:00
|
|
|
// empty outline -> create new eleemnt...
|
|
|
|
|
if(elem.classList.contains('outline')
|
|
|
|
|
&& elem.children.length == 0){
|
|
|
|
|
// create new eleemnt and edit it...
|
|
|
|
|
var block = that.Block()
|
|
|
|
|
that.outline.append(block)
|
|
|
|
|
that.edit(block)
|
|
|
|
|
return }
|
|
|
|
|
|
2023-10-09 18:58:24 +03:00
|
|
|
// expand/collapse
|
2023-10-14 22:49:02 +03:00
|
|
|
if(elem.classList.contains('view')){
|
2023-10-09 19:37:08 +03:00
|
|
|
// click: left of elem (outside)
|
|
|
|
|
if(evt.offsetX < 0){
|
|
|
|
|
// XXX item menu?
|
|
|
|
|
|
|
|
|
|
// click: right of elem (outside)
|
|
|
|
|
} else if(elem.offsetWidth < evt.offsetX){
|
2023-10-12 03:24:34 +03:00
|
|
|
that.toggleCollapse(that.get(elem))
|
2023-10-09 19:37:08 +03:00
|
|
|
|
|
|
|
|
// click inside element...
|
|
|
|
|
} else {
|
|
|
|
|
// XXX
|
2023-10-11 07:16:18 +03:00
|
|
|
} }
|
2023-10-09 19:37:08 +03:00
|
|
|
|
2023-10-11 07:16:18 +03:00
|
|
|
// edit of focus...
|
2023-10-18 23:30:15 +03:00
|
|
|
// NOTE: this is useful if element text is hidden but the
|
2023-10-11 07:16:18 +03:00
|
|
|
// frame is still visible...
|
2023-10-14 22:49:02 +03:00
|
|
|
if(elem.classList.contains('block')){
|
2023-10-12 19:45:12 +03:00
|
|
|
elem.querySelector('.code').focus() }
|
2023-10-11 07:16:18 +03:00
|
|
|
|
2023-10-23 14:37:14 +03:00
|
|
|
// focus viewport...
|
|
|
|
|
// XXX this does not work because by this point there is
|
|
|
|
|
// no focused element...
|
|
|
|
|
if(elem === outline){
|
|
|
|
|
var cur = that.get()
|
|
|
|
|
var viewport = that.get('viewport')
|
|
|
|
|
if(!viewport.includes(cur)){
|
|
|
|
|
var visible = that.get('visible')
|
|
|
|
|
var i = visible.indexOf(cur)
|
|
|
|
|
var v = visible.indexOf(viewport[0])
|
|
|
|
|
i < v ?
|
|
|
|
|
that.focus(viewport[0])
|
|
|
|
|
: that.focus(viewport.at(-1)) } }
|
|
|
|
|
|
2023-10-14 02:51:20 +03:00
|
|
|
that.runPlugins('__click__', evt, that, elem) })
|
2023-10-13 22:14:36 +03:00
|
|
|
// keyboard handling...
|
2023-10-03 16:12:19 +03:00
|
|
|
outline.addEventListener('keydown',
|
2023-09-27 15:05:34 +03:00
|
|
|
function(evt){
|
2023-10-10 21:45:24 +03:00
|
|
|
var elem = evt.target
|
2023-10-14 22:49:02 +03:00
|
|
|
if(that.runPlugins('__keydown__', evt, that, evt.target) !== true){
|
2023-10-13 22:14:36 +03:00
|
|
|
return }
|
2023-10-10 21:45:24 +03:00
|
|
|
// handle keyboard...
|
2023-10-26 01:02:27 +03:00
|
|
|
var keys = []
|
|
|
|
|
evt.ctrlKey
|
|
|
|
|
&& keys.push('c_' + evt.key)
|
|
|
|
|
evt.ctrlKey && evt.altKey
|
|
|
|
|
&& keys.push('c_a_' + evt.key)
|
|
|
|
|
evt.ctrlKey && evt.shiftKey
|
|
|
|
|
&& keys.push('c_s_' + evt.key)
|
|
|
|
|
evt.altKey && evt.ctrlKey && evt.shiftKey
|
|
|
|
|
&& keys.push('c_a_s_' + evt.key)
|
|
|
|
|
evt.altKey
|
|
|
|
|
&& keys.push('a_' + evt.key)
|
|
|
|
|
evt.altKey && evt.shiftKey
|
|
|
|
|
&& keys.push('a_s_' + evt.key)
|
|
|
|
|
evt.shiftKey
|
|
|
|
|
&& keys.push('s_' + evt.key)
|
|
|
|
|
keys.push(evt.key)
|
|
|
|
|
for(var k of keys){
|
|
|
|
|
if(k in that.keyboard){
|
|
|
|
|
that.keyboard[k].call(that, evt)
|
|
|
|
|
break } } })
|
2023-10-13 22:14:36 +03:00
|
|
|
// update code block...
|
|
|
|
|
outline.addEventListener('keyup',
|
|
|
|
|
function(evt){
|
2023-10-23 22:11:10 +03:00
|
|
|
var elem = evt.target
|
|
|
|
|
// update element state...
|
|
|
|
|
if(elem.classList.contains('code')){
|
|
|
|
|
// NOTE: for some reason setting the timeout here to 0
|
|
|
|
|
// makes FF sometimes not see the updated text...
|
|
|
|
|
setTimeout(function(){
|
2023-11-02 06:53:23 +03:00
|
|
|
that.update(elem.parentElement) }, 0) }
|
2023-10-23 22:11:10 +03:00
|
|
|
that.runPlugins('__keyup__', evt, that, elem) })
|
2023-10-14 22:49:02 +03:00
|
|
|
|
2023-09-27 15:05:34 +03:00
|
|
|
// toggle view/code of nodes...
|
2023-10-03 16:12:19 +03:00
|
|
|
outline.addEventListener('focusin',
|
2023-09-27 15:05:34 +03:00
|
|
|
function(evt){
|
2023-10-12 19:45:12 +03:00
|
|
|
var elem = evt.target
|
|
|
|
|
|
2023-10-14 22:49:02 +03:00
|
|
|
// ignore children container...
|
2023-10-12 19:45:12 +03:00
|
|
|
if(elem.classList.contains('children')){
|
|
|
|
|
return }
|
|
|
|
|
|
2023-10-03 16:12:19 +03:00
|
|
|
// handle focus...
|
2023-10-23 14:37:14 +03:00
|
|
|
if(elem !== that.outline){
|
|
|
|
|
for(var e of [...that.dom.querySelectorAll('.focused')]){
|
|
|
|
|
e.classList.remove('focused') }
|
|
|
|
|
that.get('focused')?.classList?.add('focused') }
|
2023-10-03 16:12:19 +03:00
|
|
|
// textarea...
|
2023-10-12 19:45:12 +03:00
|
|
|
if(elem.classList.contains('code')){
|
2023-10-26 01:02:27 +03:00
|
|
|
elem.dataset.original = elem.value
|
2023-10-12 19:45:12 +03:00
|
|
|
elem.updateSize() }
|
|
|
|
|
|
2023-10-14 22:49:02 +03:00
|
|
|
// XXX do we need this???
|
|
|
|
|
that.runPlugins('__focusin__', evt, that, elem) })
|
2023-10-03 16:12:19 +03:00
|
|
|
outline.addEventListener('focusout',
|
2023-09-27 15:05:34 +03:00
|
|
|
function(evt){
|
2023-10-14 22:49:02 +03:00
|
|
|
var elem = evt.target
|
2023-10-23 14:37:14 +03:00
|
|
|
// update code...
|
2023-10-14 22:49:02 +03:00
|
|
|
if(elem.classList.contains('code')){
|
2023-10-26 01:02:27 +03:00
|
|
|
var block = that.get(elem)
|
2023-10-22 23:28:04 +03:00
|
|
|
// clean out attrs...
|
2023-11-02 00:06:39 +03:00
|
|
|
elem.value =
|
|
|
|
|
that.trim_block_text ?
|
|
|
|
|
that.parseBlockAttrs(elem.value).text.trim()
|
|
|
|
|
: that.parseBlockAttrs(elem.value).text
|
2023-10-22 23:28:04 +03:00
|
|
|
that.update(block)
|
2023-10-26 01:02:27 +03:00
|
|
|
// undo...
|
|
|
|
|
if(elem.value != elem.dataset.original){
|
|
|
|
|
that.setUndo(
|
|
|
|
|
that.path(elem),
|
|
|
|
|
'update',
|
|
|
|
|
[that.path(elem), {
|
|
|
|
|
...that.data(elem),
|
|
|
|
|
text: elem.dataset.original,
|
|
|
|
|
}])
|
|
|
|
|
delete elem.dataset.original }
|
2023-10-18 14:58:08 +03:00
|
|
|
// give the browser a chance to update the DOM...
|
|
|
|
|
// XXX revise...
|
|
|
|
|
setTimeout(function(){
|
|
|
|
|
that.runPlugins('__editedcode__', evt, that, elem) }, 0) }
|
2023-10-14 22:49:02 +03:00
|
|
|
|
|
|
|
|
that.runPlugins('__focusout__', evt, that, elem) })
|
2023-10-07 17:06:54 +03:00
|
|
|
// update .code...
|
|
|
|
|
outline.addEventListener('change',
|
|
|
|
|
function(evt){
|
2023-10-22 13:55:41 +03:00
|
|
|
that.__change__() })
|
2023-09-27 15:05:34 +03:00
|
|
|
|
2023-11-02 22:54:55 +03:00
|
|
|
// header...
|
|
|
|
|
var header = this.header
|
|
|
|
|
header.addEventListener('click',
|
|
|
|
|
function(evt){
|
|
|
|
|
var elem = evt.target
|
|
|
|
|
if(elem.classList.contains('path-item')){
|
|
|
|
|
that.uncrop(elem.getAttribute('uncrop') ?? 'all') } })
|
|
|
|
|
|
2023-10-04 15:40:29 +03:00
|
|
|
// toolbar...
|
|
|
|
|
var toolbar = this.toolbar
|
|
|
|
|
if(toolbar){
|
|
|
|
|
// handle return of focus when clicking toolbar...
|
|
|
|
|
var focus_textarea
|
|
|
|
|
var cahceNodeType = function(){
|
|
|
|
|
// NOTE: for some reason .activeElement returns an element
|
|
|
|
|
// that is not in the DOM after the action is done...
|
|
|
|
|
focus_textarea = document.activeElement.nodeName == 'TEXTAREA' }
|
|
|
|
|
var refocusNode = function(){
|
|
|
|
|
focus_textarea ?
|
2023-10-23 14:37:14 +03:00
|
|
|
editor.get().querySelector('.code').focus()
|
2023-10-11 07:31:06 +03:00
|
|
|
: editor.focus()
|
2023-10-04 15:40:29 +03:00
|
|
|
focus_textarea = undefined }
|
|
|
|
|
// cache the focused node type before focus changes...
|
|
|
|
|
toolbar.addEventListener('mousedown', cahceNodeType)
|
|
|
|
|
// refocus the node after we are done...
|
|
|
|
|
toolbar.addEventListener('click', refocusNode) }
|
|
|
|
|
|
2023-10-07 17:06:54 +03:00
|
|
|
// code...
|
|
|
|
|
var code = this.code
|
|
|
|
|
if(code){
|
2023-10-12 19:45:12 +03:00
|
|
|
var t = Date.now()
|
2023-10-28 22:52:46 +03:00
|
|
|
this.load(code
|
2023-10-11 03:29:20 +03:00
|
|
|
.replace(/</g, '<')
|
2023-10-12 19:45:12 +03:00
|
|
|
.replace(/>/g, '>'))
|
2023-10-19 16:59:06 +03:00
|
|
|
console.log(`Parse: ${Date.now() - t}ms`) }
|
2023-10-26 01:02:27 +03:00
|
|
|
this.clearUndo()
|
2023-10-07 17:06:54 +03:00
|
|
|
|
2023-10-14 02:42:29 +03:00
|
|
|
this.runPlugins('__setup__', this)
|
2023-10-23 04:26:00 +03:00
|
|
|
|
|
|
|
|
// autofocus...
|
|
|
|
|
if(this.dom.getAttribute('autofocus') != null){
|
|
|
|
|
this.focus() }
|
2023-10-13 22:14:36 +03:00
|
|
|
|
2023-09-27 15:05:34 +03:00
|
|
|
return this },
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2023-10-28 22:52:46 +03:00
|
|
|
//---------------------------------------------------------------------
|
|
|
|
|
// Custom element...
|
|
|
|
|
|
2023-10-29 15:31:23 +03:00
|
|
|
|
2023-10-28 22:52:46 +03:00
|
|
|
window.customElements.define('outline-editor',
|
|
|
|
|
window.OutlineEditor =
|
2023-10-29 15:31:23 +03:00
|
|
|
Object.assign(
|
|
|
|
|
function(){
|
|
|
|
|
var obj = Reflect.construct(HTMLElement, [...arguments], OutlineEditor)
|
|
|
|
|
|
|
|
|
|
var shadow = obj.attachShadow({mode: 'open'})
|
|
|
|
|
|
|
|
|
|
var style = document.createElement('link');
|
|
|
|
|
style.setAttribute('rel', 'stylesheet');
|
|
|
|
|
style.setAttribute('href', 'editor.css');
|
|
|
|
|
|
|
|
|
|
// XXX it is not rational to have this...
|
|
|
|
|
var editor = obj.dom = document.createElement('div')
|
|
|
|
|
editor.classList.add('editor')
|
|
|
|
|
|
|
|
|
|
var header = document.createElement('div')
|
|
|
|
|
header.classList.add('header')
|
|
|
|
|
|
|
|
|
|
var outline = document.createElement('div')
|
|
|
|
|
outline.classList.add('outline')
|
|
|
|
|
outline.setAttribute('tabindex', '0')
|
|
|
|
|
|
|
|
|
|
//var toolbar = document.createElement('div')
|
|
|
|
|
//toolbar.classList.add('toolbar')
|
|
|
|
|
|
2023-11-01 19:07:37 +03:00
|
|
|
// XXX can't yet get rid of the editor element here...
|
|
|
|
|
// - handling autofocus of host vs. shadow???
|
|
|
|
|
// - CSS not working correctly yet...
|
|
|
|
|
// ...is this feasible???
|
2023-10-29 15:31:23 +03:00
|
|
|
editor.append(
|
|
|
|
|
style,
|
|
|
|
|
header,
|
|
|
|
|
outline)
|
|
|
|
|
shadow.append(editor)
|
|
|
|
|
|
2023-10-31 17:38:55 +03:00
|
|
|
console.log('SETUP')
|
2023-10-29 15:31:23 +03:00
|
|
|
obj.setup(editor)
|
|
|
|
|
|
|
|
|
|
return obj },
|
|
|
|
|
// constructor stuff...
|
|
|
|
|
{
|
|
|
|
|
observedAttributes: [
|
|
|
|
|
'value',
|
2023-10-31 17:38:55 +03:00
|
|
|
|
|
|
|
|
'session-storage',
|
|
|
|
|
'local-storage',
|
2023-10-29 15:31:23 +03:00
|
|
|
],
|
|
|
|
|
|
|
|
|
|
prototype: Object.assign(
|
|
|
|
|
{
|
|
|
|
|
__proto__: HTMLElement.prototype,
|
2023-10-28 22:52:46 +03:00
|
|
|
|
2023-10-29 15:31:23 +03:00
|
|
|
// XXX HACK these are copies from Outline, use
|
|
|
|
|
// object.mixin(...) instead...
|
|
|
|
|
get header(){
|
|
|
|
|
return this.dom?.querySelector('.header') },
|
|
|
|
|
set header(val){},
|
|
|
|
|
get outline(){
|
|
|
|
|
return this.dom?.querySelector('.outline') },
|
|
|
|
|
set outline(val){},
|
|
|
|
|
get toolbar(){
|
|
|
|
|
return this.dom?.querySelector('.toolbar') },
|
|
|
|
|
set toolbar(val){},
|
|
|
|
|
|
2023-11-01 19:07:37 +03:00
|
|
|
// NOTE: this is here to break recursion of trying to set
|
|
|
|
|
// html's value both in .code that is called both when
|
|
|
|
|
// setting .value and from .attributeChangedCallback(..)
|
|
|
|
|
get __code(){
|
|
|
|
|
return this.code },
|
|
|
|
|
set __code(value){
|
|
|
|
|
if(value == null){
|
|
|
|
|
return }
|
|
|
|
|
// XXX is this the right way to do this???
|
|
|
|
|
this.__sessionStorage
|
|
|
|
|
&& (sessionStorage[this.__sessionStorage] = value)
|
|
|
|
|
this.__localStorage
|
|
|
|
|
&& (localStorage[this.__localStorage] = value) },
|
2023-10-28 22:52:46 +03:00
|
|
|
get code(){
|
2023-10-29 15:31:23 +03:00
|
|
|
return this.hasAttribute('value') ?
|
|
|
|
|
this.getAttribute('value')
|
2023-10-30 03:51:33 +03:00
|
|
|
: HTMLElement.decode(this.innerHTML) },
|
2023-10-28 22:52:46 +03:00
|
|
|
set code(value){
|
2023-10-29 15:31:23 +03:00
|
|
|
if(value == null){
|
|
|
|
|
return }
|
2023-10-28 22:52:46 +03:00
|
|
|
// XXX this can break in conjunction with .attributeChangedCallback(..)
|
2023-10-29 15:31:23 +03:00
|
|
|
if(this.hasAttribute('value')){
|
|
|
|
|
this.setAttribute('value', value)
|
2023-10-28 22:52:46 +03:00
|
|
|
} else {
|
2023-10-31 17:38:55 +03:00
|
|
|
this.innerHTML = HTMLElement.encode(value) }
|
2023-11-01 19:07:37 +03:00
|
|
|
this.__code = value },
|
2023-10-28 22:52:46 +03:00
|
|
|
|
2023-10-29 15:31:23 +03:00
|
|
|
// XXX do we need this???
|
|
|
|
|
// ...rename .code -> .value ???
|
2023-10-28 22:52:46 +03:00
|
|
|
get value(){
|
2023-10-29 15:31:23 +03:00
|
|
|
return this.code },
|
2023-10-28 22:52:46 +03:00
|
|
|
set value(value){
|
2023-10-29 15:31:23 +03:00
|
|
|
this.code = value },
|
2023-10-28 22:52:46 +03:00
|
|
|
|
|
|
|
|
connectedCallback: function(){
|
|
|
|
|
var that = this
|
|
|
|
|
// load the data...
|
|
|
|
|
setTimeout(function(){
|
2023-10-29 15:31:23 +03:00
|
|
|
that.load(that.code) }, 0) },
|
|
|
|
|
|
2023-11-02 06:53:23 +03:00
|
|
|
// XXX do we need to before == after check???
|
2023-10-29 15:31:23 +03:00
|
|
|
attributeChangedCallback(name, before, after){
|
2023-10-31 17:38:55 +03:00
|
|
|
if(name == 'local-storage'){
|
|
|
|
|
this.__localStorage = after
|
2023-11-02 06:53:23 +03:00
|
|
|
// NOTE: we setting .code here because we will
|
|
|
|
|
// .load(..) at .setup(..)
|
|
|
|
|
sessionStorage[after]
|
|
|
|
|
&& (this.code = sessionStorage[after]) }
|
2023-10-31 17:38:55 +03:00
|
|
|
|
2023-11-02 06:53:23 +03:00
|
|
|
if(name == 'session-storage'){
|
2023-10-31 17:38:55 +03:00
|
|
|
this.__sessionStorage = after
|
2023-11-02 06:53:23 +03:00
|
|
|
sessionStorage[after]
|
|
|
|
|
&& (this.code = sessionStorage[after]) }
|
2023-10-31 17:38:55 +03:00
|
|
|
|
2023-11-02 06:53:23 +03:00
|
|
|
// NOTE: if other sources are active but unset this
|
|
|
|
|
// should provide the default, otherwise it will
|
|
|
|
|
// get overwritten by the value in .code by .load(..)
|
|
|
|
|
if(name == 'value'){
|
2023-11-01 19:07:37 +03:00
|
|
|
// see notes for .__code
|
2023-11-02 06:53:23 +03:00
|
|
|
this.__code = after }
|
2023-10-28 22:52:46 +03:00
|
|
|
},
|
2023-10-29 15:31:23 +03:00
|
|
|
|
2023-10-28 22:52:46 +03:00
|
|
|
},
|
2023-10-29 15:31:23 +03:00
|
|
|
// XXX this will fail due to all the getters/setters -- use object.mixin(..)...
|
|
|
|
|
Outline),
|
|
|
|
|
}))
|
2023-10-28 22:52:46 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2023-09-27 15:05:34 +03:00
|
|
|
/**********************************************************************
|
|
|
|
|
* vim:set ts=4 sw=4 : */
|