2023-09-27 15:05:34 +03:00
|
|
|
/**********************************************************************
|
|
|
|
|
*
|
|
|
|
|
*
|
|
|
|
|
*
|
|
|
|
|
**********************************************************************/
|
|
|
|
|
|
|
|
|
|
|
2023-09-28 20:49:06 +03:00
|
|
|
//---------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
// XXX do a caret api...
|
|
|
|
|
|
|
|
|
|
// XXX only for text areas...
|
|
|
|
|
var atLine = function(elem, index){
|
|
|
|
|
// XXX add support for range...
|
|
|
|
|
var text = elem.value
|
|
|
|
|
var lines = text.split(/\n/g).length
|
|
|
|
|
var line = elem.caretLine
|
|
|
|
|
|
|
|
|
|
// XXX STUB index handling...
|
|
|
|
|
if((index == -1 && line == lines)
|
|
|
|
|
|| (index == 0 && line == 1)){
|
|
|
|
|
return true }
|
|
|
|
|
return false }
|
|
|
|
|
|
|
|
|
|
|
2023-09-27 15:05:34 +03:00
|
|
|
|
|
|
|
|
//---------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
var Outline = {
|
|
|
|
|
dom: undefined,
|
|
|
|
|
|
2023-09-27 15:41:29 +03:00
|
|
|
// config...
|
|
|
|
|
//
|
|
|
|
|
left_key_expands: false,
|
|
|
|
|
right_key_collapses: true,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// XXX revise name...
|
|
|
|
|
Block: function(place=none){
|
|
|
|
|
var block = document.createElement('div')
|
|
|
|
|
block.setAttribute('tabindex', '0')
|
|
|
|
|
block.append(
|
|
|
|
|
document.createElement('span'),
|
|
|
|
|
document.createElement('textarea')
|
|
|
|
|
.autoUpdateSize())
|
2023-09-28 20:49:06 +03:00
|
|
|
var cur = this.get()
|
2023-09-27 15:41:29 +03:00
|
|
|
place && cur
|
|
|
|
|
&& cur[place](block)
|
|
|
|
|
return block },
|
|
|
|
|
|
2023-09-28 20:49:06 +03:00
|
|
|
//
|
|
|
|
|
// .get([<offset>])
|
|
|
|
|
// .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')
|
|
|
|
|
// .get('top')
|
|
|
|
|
// -> <nodes>
|
|
|
|
|
//
|
|
|
|
|
// XXX add support for node ID...
|
|
|
|
|
get: function(node='focused', offset){
|
|
|
|
|
var that = this
|
|
|
|
|
|
|
|
|
|
// shorthands...
|
|
|
|
|
if(node == 'next'){
|
|
|
|
|
return this.get('focused', 1) }
|
|
|
|
|
if(node == 'prev' || node == 'previous'){
|
|
|
|
|
return this.get('focused', -1) }
|
|
|
|
|
|
|
|
|
|
// node lists...
|
|
|
|
|
var NO_NODES = {}
|
|
|
|
|
var nodes =
|
|
|
|
|
node == 'all' ?
|
|
|
|
|
[...this.dom.querySelectorAll('[tabindex]')]
|
|
|
|
|
: node == 'visible' ?
|
|
|
|
|
[...this.dom.querySelectorAll('[tabindex]')]
|
|
|
|
|
.filter(function(e){
|
|
|
|
|
return e.offsetParent != null })
|
|
|
|
|
: node == 'editable' ?
|
|
|
|
|
[...this.dom.querySelectorAll('[tabindex]>textarea')]
|
|
|
|
|
: node == 'selected' ?
|
|
|
|
|
[...this.dom.querySelectorAll('[tabindex].selected')]
|
|
|
|
|
: node == 'top' ?
|
|
|
|
|
[...this.dom.children]
|
|
|
|
|
.filter(function(elem){
|
|
|
|
|
return elem.getAttribute('tabindex') != null })
|
|
|
|
|
: ['siblings', 'children'].includes(node) ?
|
|
|
|
|
this.get('focused', node)
|
|
|
|
|
: node instanceof Array ?
|
|
|
|
|
node
|
|
|
|
|
: NO_NODES
|
|
|
|
|
if(nodes !== NO_NODES){
|
|
|
|
|
return offset == null ?
|
|
|
|
|
nodes
|
|
|
|
|
: typeof(offset) == 'number' ?
|
|
|
|
|
nodes.at(offset)
|
|
|
|
|
: nodes
|
|
|
|
|
.map(function(elem){
|
|
|
|
|
return that.get(elem, offset) }) }
|
|
|
|
|
|
|
|
|
|
// single base node...
|
|
|
|
|
node =
|
|
|
|
|
typeof(node) == 'number' ?
|
|
|
|
|
this.at(node)
|
|
|
|
|
: node == 'focused' ?
|
|
|
|
|
(this.dom.querySelector(`[tabindex]:focus`)
|
|
|
|
|
|| this.dom.querySelector(`textarea:focus`)?.parentElement)
|
|
|
|
|
: node == 'parent' ?
|
|
|
|
|
this.get('focused')?.parentElement
|
|
|
|
|
: node
|
|
|
|
|
var edited
|
|
|
|
|
if(node == 'edited'){
|
|
|
|
|
edited = this.dom.querySelector(`textarea:focus`)
|
|
|
|
|
node = edited?.parentElement }
|
|
|
|
|
|
|
|
|
|
if(!node || typeof(node) == 'string'){
|
|
|
|
|
return undefined }
|
|
|
|
|
|
|
|
|
|
// children...
|
|
|
|
|
if(offset == 'children'){
|
|
|
|
|
return [...node.children]
|
|
|
|
|
.filter(function(elem){
|
|
|
|
|
return elem.getAttribute('tabindex') != null }) }
|
|
|
|
|
|
|
|
|
|
// siblings...
|
|
|
|
|
if(offset == 'siblings'){
|
|
|
|
|
return [...node.parentElement.children]
|
|
|
|
|
.filter(function(elem){
|
|
|
|
|
return elem.getAttribute('tabindex') != null }) }
|
|
|
|
|
|
|
|
|
|
// offset...
|
|
|
|
|
if(typeof(offset) == 'number'){
|
|
|
|
|
nodes = this.get('visible')
|
|
|
|
|
var i = nodes.indexOf(node) + offset
|
|
|
|
|
i = i < 0 ?
|
|
|
|
|
nodes.length + i
|
|
|
|
|
: i % nodes.length
|
|
|
|
|
node = nodes[i]
|
|
|
|
|
edited = edited
|
|
|
|
|
&& node.querySelector('textarea') }
|
|
|
|
|
return edited
|
|
|
|
|
|| node },
|
|
|
|
|
at: function(index, nodes='visible'){
|
|
|
|
|
return this.get(nodes).at(index) },
|
2023-09-27 15:05:34 +03:00
|
|
|
|
2023-09-27 15:41:29 +03:00
|
|
|
focus: function(node='focused'){},
|
|
|
|
|
edit: function(node='focused'){},
|
2023-09-28 20:49:06 +03:00
|
|
|
|
|
|
|
|
indent: function(node='focused', indent=true){
|
|
|
|
|
// .indent(<indent>)
|
|
|
|
|
if(node === true || node === false){
|
|
|
|
|
indent = node
|
|
|
|
|
node = 'focused' }
|
|
|
|
|
var cur = this.get(node)
|
|
|
|
|
if(!cur){
|
|
|
|
|
return }
|
|
|
|
|
var siblings = this.get(node, 'siblings')
|
|
|
|
|
// deindent...
|
|
|
|
|
if(!indent){
|
|
|
|
|
var parent = cur.parentElement
|
|
|
|
|
if(!parent.classList.contains('.editor')){
|
|
|
|
|
var children = siblings.slice(siblings.indexOf(cur)+1)
|
|
|
|
|
parent.after(cur)
|
|
|
|
|
children.length > 0
|
|
|
|
|
&& cur.append(...children) }
|
|
|
|
|
// indent...
|
|
|
|
|
} else {
|
|
|
|
|
var parent = siblings[siblings.indexOf(cur) - 1]
|
|
|
|
|
if(parent){
|
|
|
|
|
parent.append(cur) } }
|
|
|
|
|
return cur },
|
|
|
|
|
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
|
|
|
|
|
node = null }
|
|
|
|
|
node ??= this.get()
|
|
|
|
|
if(!node
|
|
|
|
|
// only nodes with children can be collapsed...
|
|
|
|
|
|| !node.querySelector('[tabindex]')){
|
|
|
|
|
return }
|
|
|
|
|
state = state == 'next' ?
|
|
|
|
|
!node.getAttribute('collapsed')
|
|
|
|
|
: state
|
|
|
|
|
if(state){
|
|
|
|
|
node.setAttribute('collapsed', '')
|
|
|
|
|
} else {
|
|
|
|
|
node.removeAttribute('collapsed')
|
|
|
|
|
for(var elem of [...node.querySelectorAll('textarea')]){
|
|
|
|
|
elem.updateSize() } }
|
|
|
|
|
return node },
|
2023-09-27 15:05:34 +03:00
|
|
|
|
|
|
|
|
// block serialization...
|
|
|
|
|
__code2html__: function(code){
|
|
|
|
|
return code },
|
|
|
|
|
__html2code__: function(html){
|
|
|
|
|
return html },
|
|
|
|
|
|
2023-09-27 15:41:29 +03:00
|
|
|
// serialization...
|
|
|
|
|
json: function(node){
|
|
|
|
|
var that = this
|
|
|
|
|
node ??= this.dom
|
|
|
|
|
return [...node.children]
|
|
|
|
|
.map(function(elem){
|
|
|
|
|
return elem.nodeName != 'DIV' ?
|
|
|
|
|
[]
|
|
|
|
|
: [{
|
|
|
|
|
text: that.__html2code__ ?
|
|
|
|
|
that.__html2code__(elem.querySelector('span').innerHTML)
|
|
|
|
|
: elem.querySelector('span').innerHTML,
|
|
|
|
|
collapsed: elem.getAttribute('collapsed') != null,
|
|
|
|
|
children: that.json(elem)
|
|
|
|
|
}] })
|
|
|
|
|
.flat() },
|
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-09-27 15:41:29 +03:00
|
|
|
var text = ''
|
|
|
|
|
for(var elem of node){
|
|
|
|
|
text +=
|
2023-09-28 20:49:06 +03:00
|
|
|
level
|
2023-09-27 15:41:29 +03:00
|
|
|
+'- '
|
|
|
|
|
+ elem.text
|
2023-09-28 20:49:06 +03:00
|
|
|
.replace(/\n/g, '\n'+ level +' ')
|
2023-09-27 15:41:29 +03:00
|
|
|
+'\n'
|
2023-09-28 20:49:06 +03:00
|
|
|
+ this.text(elem.children || [], indent, level+indent) }
|
2023-09-27 15:41:29 +03:00
|
|
|
return text },
|
|
|
|
|
|
|
|
|
|
// XXX use .__code2html__(..)
|
|
|
|
|
load: function(){},
|
2023-09-27 15:05:34 +03:00
|
|
|
|
2023-09-28 20:49:06 +03:00
|
|
|
// XXX add scrollIntoView(..) to nav...
|
2023-09-27 15:05:34 +03:00
|
|
|
keyboard: {
|
|
|
|
|
// vertical navigation...
|
|
|
|
|
ArrowUp: function(evt){
|
2023-09-28 20:49:06 +03:00
|
|
|
var state = 'focused'
|
|
|
|
|
var edited = this.get('edited')
|
2023-09-27 15:05:34 +03:00
|
|
|
if(edited){
|
2023-09-28 20:49:06 +03:00
|
|
|
if(!atLine(edited, 0)){
|
2023-09-27 15:05:34 +03:00
|
|
|
return }
|
2023-09-28 20:49:06 +03:00
|
|
|
state = 'edited' }
|
2023-09-27 15:05:34 +03:00
|
|
|
evt.preventDefault()
|
2023-09-28 20:49:06 +03:00
|
|
|
this.get(state, -1)?.focus() },
|
2023-09-27 15:05:34 +03:00
|
|
|
ArrowDown: function(evt, offset=1){
|
2023-09-28 20:49:06 +03:00
|
|
|
var state = 'focused'
|
|
|
|
|
var edited = this.get('edited')
|
2023-09-27 15:05:34 +03:00
|
|
|
if(edited){
|
2023-09-28 20:49:06 +03:00
|
|
|
if(!atLine(edited, -1)){
|
2023-09-27 15:05:34 +03:00
|
|
|
return }
|
|
|
|
|
//window.getSelection()
|
2023-09-28 20:49:06 +03:00
|
|
|
state = 'edited' }
|
2023-09-27 15:05:34 +03:00
|
|
|
evt.preventDefault()
|
2023-09-28 20:49:06 +03:00
|
|
|
this.get(state, 1)?.focus() },
|
2023-09-27 15:05:34 +03:00
|
|
|
|
|
|
|
|
// horizontal navigation / collapse...
|
|
|
|
|
// XXX if at start/end of element move to prev/next...
|
|
|
|
|
ArrowLeft: function(evt){
|
|
|
|
|
if(this.dom.querySelector('.editor textarea:focus')){
|
|
|
|
|
// XXX if at end of element move to next...
|
|
|
|
|
return }
|
2023-09-27 15:41:29 +03:00
|
|
|
if(this.left_key_expands){
|
2023-09-28 20:49:06 +03:00
|
|
|
this.toggleCollapse(true)
|
|
|
|
|
this.get('parent')?.focus()
|
2023-09-27 15:05:34 +03:00
|
|
|
} else {
|
|
|
|
|
evt.shiftKey ?
|
2023-09-28 20:49:06 +03:00
|
|
|
this.toggleCollapse(true)
|
|
|
|
|
: this.get('parent')?.focus() } },
|
2023-09-27 15:05:34 +03:00
|
|
|
ArrowRight: function(evt){
|
|
|
|
|
if(this.dom.querySelector('.editor textarea:focus')){
|
|
|
|
|
// XXX if at end of element move to next...
|
|
|
|
|
return }
|
2023-09-27 15:41:29 +03:00
|
|
|
if(this.right_key_collapses){
|
2023-09-28 20:49:06 +03:00
|
|
|
this.toggleCollapse(false)
|
|
|
|
|
var child = this.get('children')[0]
|
2023-09-27 15:05:34 +03:00
|
|
|
child?.focus()
|
|
|
|
|
if(!child){
|
2023-09-28 20:49:06 +03:00
|
|
|
this.get('next')?.focus() }
|
2023-09-27 15:05:34 +03:00
|
|
|
} else {
|
|
|
|
|
evt.shiftKey ?
|
2023-09-28 20:49:06 +03:00
|
|
|
this.toggleCollapse(false)
|
|
|
|
|
: this.get('children')[0]?.focus() } },
|
2023-09-27 15:05:34 +03:00
|
|
|
|
|
|
|
|
// indent...
|
|
|
|
|
Tab: function(evt){
|
|
|
|
|
evt.preventDefault()
|
2023-09-29 02:47:07 +03:00
|
|
|
var edited = this.get('edited')
|
2023-09-28 20:49:06 +03:00
|
|
|
var node = this.indent(!evt.shiftKey)
|
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){
|
|
|
|
|
if(evt.target.nodeName != 'TEXTAREA'){
|
|
|
|
|
evt.preventDefault()
|
2023-09-27 15:41:29 +03:00
|
|
|
this.Block('before')?.querySelector('textarea')?.focus() } },
|
2023-09-27 15:05:34 +03:00
|
|
|
o: function(evt){
|
|
|
|
|
if(evt.target.nodeName != 'TEXTAREA'){
|
|
|
|
|
evt.preventDefault()
|
2023-09-27 15:41:29 +03:00
|
|
|
this.Block('after')?.querySelector('textarea')?.focus() } },
|
2023-09-27 15:05:34 +03:00
|
|
|
Enter: function(evt){
|
|
|
|
|
/*if(evt.target.isContentEditable){
|
|
|
|
|
// XXX create new node...
|
|
|
|
|
return }
|
|
|
|
|
//*/
|
|
|
|
|
if(evt.ctrlKey
|
|
|
|
|
|| evt.shiftKey){
|
|
|
|
|
return }
|
|
|
|
|
evt.preventDefault()
|
|
|
|
|
evt.target.nodeName == 'TEXTAREA' ?
|
2023-09-27 15:41:29 +03:00
|
|
|
this.Block('after')?.querySelector('textarea')?.focus()
|
2023-09-28 20:49:06 +03:00
|
|
|
: this.get()?.querySelector('textarea')?.focus() },
|
2023-09-27 15:05:34 +03:00
|
|
|
Escape: function(evt){
|
|
|
|
|
this.dom.querySelector('textarea:focus')?.parentElement?.focus() },
|
|
|
|
|
Delete: function(evt){
|
|
|
|
|
if(evt.target.isContentEditable){
|
|
|
|
|
return }
|
2023-09-28 20:49:06 +03:00
|
|
|
this.toggleCollapse(true)
|
|
|
|
|
var next = this.get('next')
|
|
|
|
|
this.get()?.remove()
|
2023-09-27 15:05:34 +03:00
|
|
|
next?.focus() },
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
setup: function(dom){
|
|
|
|
|
var that = this
|
|
|
|
|
this.dom = dom
|
|
|
|
|
// update stuff already in DOM...
|
|
|
|
|
for(var elem of [...dom.querySelectorAll('.editor textarea')]){
|
|
|
|
|
elem.autoUpdateSize() }
|
|
|
|
|
|
|
|
|
|
// heboard handling...
|
|
|
|
|
dom.addEventListener('keydown',
|
|
|
|
|
function(evt){
|
|
|
|
|
evt.key in that.keyboard
|
|
|
|
|
&& that.keyboard[evt.key].call(that, evt) })
|
|
|
|
|
|
|
|
|
|
// toggle view/code of nodes...
|
|
|
|
|
dom.addEventListener('focusin',
|
|
|
|
|
function(evt){
|
|
|
|
|
var node = evt.target
|
|
|
|
|
if(node.nodeName == 'TEXTAREA'
|
|
|
|
|
&& node?.previousElementSibling?.nodeName == 'SPAN'){
|
|
|
|
|
node.value =
|
|
|
|
|
that.__html2code__ ?
|
|
|
|
|
that.__html2code__(node.previousElementSibling.innerHTML)
|
|
|
|
|
: node.previousElementSibling.innerHTML
|
|
|
|
|
node.updateSize() } })
|
|
|
|
|
dom.addEventListener('focusout',
|
|
|
|
|
function(evt){
|
|
|
|
|
var node = evt.target
|
|
|
|
|
if(node.nodeName == 'TEXTAREA'
|
|
|
|
|
&& node?.previousElementSibling?.nodeName == 'SPAN'){
|
|
|
|
|
node.previousElementSibling.innerHTML =
|
|
|
|
|
that.__code2html__ ?
|
|
|
|
|
that.__code2html__(node.value)
|
|
|
|
|
: node.value } })
|
|
|
|
|
|
|
|
|
|
return this },
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**********************************************************************
|
|
|
|
|
* vim:set ts=4 sw=4 : */
|