refactoring mostly done...

Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
Alex A. Naanou 2023-09-28 20:49:06 +03:00
parent 8f83ea587c
commit 45d8ef11d3
2 changed files with 231 additions and 180 deletions

View File

@ -5,6 +5,24 @@
**********************************************************************/ **********************************************************************/
//---------------------------------------------------------------------
// 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 }
//--------------------------------------------------------------------- //---------------------------------------------------------------------
@ -25,24 +43,179 @@ var Outline = {
document.createElement('span'), document.createElement('span'),
document.createElement('textarea') document.createElement('textarea')
.autoUpdateSize()) .autoUpdateSize())
var cur = getFocused() var cur = this.get()
|| getEditable()?.parentElement
place && cur place && cur
&& cur[place](block) && cur[place](block)
return block }, return block },
// XXX //
get: function(){ // .get([<offset>])
}, // .get('focused'[, <offset>])
focused: function(offset, selector){ // -> <node>
}, //
edited: function(offset){ // .get('edited'[, <offset>])
return this.focused(offset, 'textarea')}, // -> <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) },
focus: function(node='focused'){}, focus: function(node='focused'){},
edit: function(node='focused'){}, edit: function(node='focused'){},
indent: function(node='focused'){},
collapse: function(node='focused'){}, 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 },
// block serialization... // block serialization...
__code2html__: function(code){ __code2html__: function(code){
@ -66,43 +239,49 @@ var Outline = {
children: that.json(elem) children: that.json(elem)
}] }) }] })
.flat() }, .flat() },
text: function(node, indent=''){ text: function(node, indent, level){
// .text(<indent>, <level>)
if(typeof(node) == 'string'){
;[node, indent=' ', level=''] = [undefined, ...arguments] }
node ??= this.json(node) node ??= this.json(node)
indent ??= ' '
level ??= ''
var text = '' var text = ''
for(var elem of node){ for(var elem of node){
text += text +=
indent level
+'- ' +'- '
+ elem.text + elem.text
.replace(/\n/g, '\n '+indent) .replace(/\n/g, '\n'+ level +' ')
+'\n' +'\n'
+ this.text(elem.children || [], indent+' ') } + this.text(elem.children || [], indent, level+indent) }
return text }, return text },
// XXX use .__code2html__(..) // XXX use .__code2html__(..)
load: function(){}, load: function(){},
// XXX add scrollIntoView(..) to nav...
keyboard: { keyboard: {
// vertical navigation... // vertical navigation...
ArrowUp: function(evt){ ArrowUp: function(evt){
var action = getFocused var state = 'focused'
var edited = this.dom.querySelector('.editor textarea:focus') var edited = this.get('edited')
if(edited){ if(edited){
if(!atLine(0)){ if(!atLine(edited, 0)){
return } return }
action = getEditable } state = 'edited' }
evt.preventDefault() evt.preventDefault()
action(-1)?.focus() }, this.get(state, -1)?.focus() },
ArrowDown: function(evt, offset=1){ ArrowDown: function(evt, offset=1){
var action = getFocused var state = 'focused'
var edited = this.dom.querySelector('.editor textarea:focus') var edited = this.get('edited')
if(edited){ if(edited){
if(!atLine(-1)){ if(!atLine(edited, -1)){
return } return }
//window.getSelection() //window.getSelection()
action = getEditable } state = 'edited' }
evt.preventDefault() evt.preventDefault()
action(1)?.focus() }, this.get(state, 1)?.focus() },
// horizontal navigation / collapse... // horizontal navigation / collapse...
// XXX if at start/end of element move to prev/next... // XXX if at start/end of element move to prev/next...
@ -111,32 +290,32 @@ var Outline = {
// XXX if at end of element move to next... // XXX if at end of element move to next...
return } return }
if(this.left_key_expands){ if(this.left_key_expands){
toggleCollapse(true) this.toggleCollapse(true)
getFocused('parent')?.focus() this.get('parent')?.focus()
} else { } else {
evt.shiftKey ? evt.shiftKey ?
toggleCollapse(true) this.toggleCollapse(true)
: getFocused('parent')?.focus() } }, : this.get('parent')?.focus() } },
ArrowRight: function(evt){ ArrowRight: function(evt){
if(this.dom.querySelector('.editor textarea:focus')){ if(this.dom.querySelector('.editor textarea:focus')){
// XXX if at end of element move to next... // XXX if at end of element move to next...
return } return }
if(this.right_key_collapses){ if(this.right_key_collapses){
toggleCollapse(false) this.toggleCollapse(false)
var child = getFocused('child') var child = this.get('children')[0]
child?.focus() child?.focus()
if(!child){ if(!child){
getFocused(1)?.focus() } this.get('next')?.focus() }
} else { } else {
evt.shiftKey ? evt.shiftKey ?
toggleCollapse(false) this.toggleCollapse(false)
: getFocused('child')?.focus() } }, : this.get('children')[0]?.focus() } },
// indent... // indent...
Tab: function(evt){ Tab: function(evt){
evt.preventDefault() evt.preventDefault()
var editable = getEditable() var editable = this.get('editable')
var node = indentNode(!evt.shiftKey) var node = this.indent(!evt.shiftKey)
;(editable ? ;(editable ?
editable editable
: node)?.focus() }, : node)?.focus() },
@ -161,14 +340,15 @@ var Outline = {
evt.preventDefault() evt.preventDefault()
evt.target.nodeName == 'TEXTAREA' ? evt.target.nodeName == 'TEXTAREA' ?
this.Block('after')?.querySelector('textarea')?.focus() this.Block('after')?.querySelector('textarea')?.focus()
: getFocused()?.querySelector('textarea')?.focus() }, : this.get()?.querySelector('textarea')?.focus() },
Escape: function(evt){ Escape: function(evt){
this.dom.querySelector('textarea:focus')?.parentElement?.focus() }, this.dom.querySelector('textarea:focus')?.parentElement?.focus() },
Delete: function(evt){ Delete: function(evt){
if(evt.target.isContentEditable){ if(evt.target.isContentEditable){
return } return }
var next = getFocused(1) this.toggleCollapse(true)
getFocused()?.remove() var next = this.get('next')
this.get()?.remove()
next?.focus() }, next?.focus() },
}, },
@ -211,146 +391,5 @@ var Outline = {
//---------------------------------------------------------------------
var getFocused = function(offset=0, selector='[tabindex]'){
var focused = document.querySelector(`.editor ${selector}:focus`)
|| (selector != 'textarea' ?
getEditable()?.parentElement
: null)
if(offset == 0){
return focused }
if(offset == 'parent'){
if(!focused){
return document.querySelector(`.editor ${selector}`) }
var elem = focused.parentElement
return elem.classList.contains('editor') ?
undefined
: elem }
if(offset == 'child'){
if(!focused){
return document.querySelector(`.editor ${selector}`) }
return focused.querySelector('div') }
if(offset == 'children'){
if(!focused){
return [] }
return [...focused.children]
.filter(function(elem){
return elem.getAttribute('tabindex') }) }
if(offset == 'siblings'){
if(!focused){
return [] }
return [...focused.parentElement.children]
.filter(function(elem){
return elem.getAttribute('tabindex') }) }
var focusable = [...document.querySelectorAll(`.editor ${selector}`)]
.filter(function(e){
return e.offsetParent != null })
if(offset == 'all'){
return focusable }
// offset from focused...
if(focused){
var i = focusable.indexOf(focused) + offset
i = i < 0 ?
focusable.length + i
: i % focusable.length
return focusable[i]
// nothing focused -> forst/last...
} else {
return focusable[offset > 0 ? 0 : focusable.length-1] } }
// XXX would also be nice to make the move only if at first/last line/char
// XXX would be nice to keep the cursor at roughly the same left offset...
var getEditable = function(offset){
return getFocused(offset, 'textarea') }
var indentNode = function(indent=true){
var cur = getFocused()
if(!cur){
return }
var siblings = getFocused('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 }
var toggleCollapse = function(node, state='next'){
if(node == 'all'){
return getFocused('all')
.map(function(node){
return toggleCollapse(node, state) }) }
// toggleCollapse(<state>)
if(!(node instanceof HTMLElement) && node != null){
state = node
node = null }
node ??= getFocused()
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()
//updateTextareaSize(elem)
}
}
return node }
// XXX add reference node...
// XXX do a caret api...
// XXX this works only on the current text node...
// XXX only for text areas...
var atLine = function(index){
// XXX add support for range...
var elem = getEditable()
var text = elem.value
var lines = text.split(/\n/g).length
var offset = elem.selectionStart
var line = text.slice(0, offset).split(/\n/g).length
//console.log('---', line, 'of', lines, '---', offset, sel)
// XXX STUB index handling...
if(index == -1 && line == lines){
return true
} else if(index == 0 && line == 1){
return true
}
return false
}
// XXX add scrollIntoView(..) to nav...
/********************************************************************** /**********************************************************************
* vim:set ts=4 sw=4 : */ * vim:set ts=4 sw=4 : */

View File

@ -15,6 +15,18 @@ HTMLTextAreaElement.prototype.autoUpdateSize = function(){
that.updateSize() }) that.updateSize() })
return this } return this }
Object.defineProperty(HTMLTextAreaElement.prototype, 'caretLine', {
enumerable: false,
get: function(){
var offset = this.selectionStart
return offset != null ?
this.value
.slice(0, offset)
.split(/\n/g)
.length
: undefined },
})
/********************************************************************** /**********************************************************************