From 45d8ef11d3ff2d481205f1ac6a7fe8a387ee9440 Mon Sep 17 00:00:00 2001 From: "Alex A. Naanou" Date: Thu, 28 Sep 2023 20:49:06 +0300 Subject: [PATCH] refactoring mostly done... Signed-off-by: Alex A. Naanou --- experiments/outline-editor/editor.js | 399 ++++++++++++++------------ experiments/outline-editor/generic.js | 12 + 2 files changed, 231 insertions(+), 180 deletions(-) diff --git a/experiments/outline-editor/editor.js b/experiments/outline-editor/editor.js index 9363213..c26bb2e 100755 --- a/experiments/outline-editor/editor.js +++ b/experiments/outline-editor/editor.js @@ -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('textarea') .autoUpdateSize()) - var cur = getFocused() - || getEditable()?.parentElement + var cur = this.get() place && cur && cur[place](block) return block }, - // XXX - get: function(){ - }, - focused: function(offset, selector){ - }, - edited: function(offset){ - return this.focused(offset, 'textarea')}, + // + // .get([]) + // .get('focused'[, ]) + // -> + // + // .get('edited'[, ]) + // -> + // + // .get('siblings') + // .get('focused', 'siblings') + // -> + // + // .get('children') + // .get('focused', 'children') + // -> + // + // .get('next') + // .get('focused', 'next') + // -> + // + // .get('prev') + // .get('focused', 'prev') + // -> + // + // .get('all') + // .get('visible') + // .get('editable') + // .get('selected') + // .get('top') + // -> + // + // 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'){}, edit: function(node='focused'){}, - indent: function(node='focused'){}, - collapse: function(node='focused'){}, + + indent: function(node='focused', indent=true){ + // .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() + 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... __code2html__: function(code){ @@ -66,43 +239,49 @@ var Outline = { children: that.json(elem) }] }) .flat() }, - text: function(node, indent=''){ + text: function(node, indent, level){ + // .text(, ) + if(typeof(node) == 'string'){ + ;[node, indent=' ', level=''] = [undefined, ...arguments] } node ??= this.json(node) + indent ??= ' ' + level ??= '' var text = '' for(var elem of node){ text += - indent + level +'- ' + elem.text - .replace(/\n/g, '\n '+indent) + .replace(/\n/g, '\n'+ level +' ') +'\n' - + this.text(elem.children || [], indent+' ') } + + this.text(elem.children || [], indent, level+indent) } return text }, // XXX use .__code2html__(..) load: function(){}, + // XXX add scrollIntoView(..) to nav... keyboard: { // vertical navigation... ArrowUp: function(evt){ - var action = getFocused - var edited = this.dom.querySelector('.editor textarea:focus') + var state = 'focused' + var edited = this.get('edited') if(edited){ - if(!atLine(0)){ + if(!atLine(edited, 0)){ return } - action = getEditable } + state = 'edited' } evt.preventDefault() - action(-1)?.focus() }, + this.get(state, -1)?.focus() }, ArrowDown: function(evt, offset=1){ - var action = getFocused - var edited = this.dom.querySelector('.editor textarea:focus') + var state = 'focused' + var edited = this.get('edited') if(edited){ - if(!atLine(-1)){ + if(!atLine(edited, -1)){ return } //window.getSelection() - action = getEditable } + state = 'edited' } evt.preventDefault() - action(1)?.focus() }, + this.get(state, 1)?.focus() }, // horizontal navigation / collapse... // 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... return } if(this.left_key_expands){ - toggleCollapse(true) - getFocused('parent')?.focus() + this.toggleCollapse(true) + this.get('parent')?.focus() } else { evt.shiftKey ? - toggleCollapse(true) - : getFocused('parent')?.focus() } }, + this.toggleCollapse(true) + : this.get('parent')?.focus() } }, ArrowRight: function(evt){ if(this.dom.querySelector('.editor textarea:focus')){ // XXX if at end of element move to next... return } if(this.right_key_collapses){ - toggleCollapse(false) - var child = getFocused('child') + this.toggleCollapse(false) + var child = this.get('children')[0] child?.focus() if(!child){ - getFocused(1)?.focus() } + this.get('next')?.focus() } } else { evt.shiftKey ? - toggleCollapse(false) - : getFocused('child')?.focus() } }, + this.toggleCollapse(false) + : this.get('children')[0]?.focus() } }, // indent... Tab: function(evt){ evt.preventDefault() - var editable = getEditable() - var node = indentNode(!evt.shiftKey) + var editable = this.get('editable') + var node = this.indent(!evt.shiftKey) ;(editable ? editable : node)?.focus() }, @@ -161,14 +340,15 @@ var Outline = { evt.preventDefault() evt.target.nodeName == 'TEXTAREA' ? this.Block('after')?.querySelector('textarea')?.focus() - : getFocused()?.querySelector('textarea')?.focus() }, + : this.get()?.querySelector('textarea')?.focus() }, Escape: function(evt){ this.dom.querySelector('textarea:focus')?.parentElement?.focus() }, Delete: function(evt){ if(evt.target.isContentEditable){ return } - var next = getFocused(1) - getFocused()?.remove() + this.toggleCollapse(true) + var next = this.get('next') + this.get()?.remove() 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() - 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 : */ diff --git a/experiments/outline-editor/generic.js b/experiments/outline-editor/generic.js index 4d2eb95..38dc095 100755 --- a/experiments/outline-editor/generic.js +++ b/experiments/outline-editor/generic.js @@ -15,6 +15,18 @@ HTMLTextAreaElement.prototype.autoUpdateSize = function(){ that.updateSize() }) 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 }, +}) + /**********************************************************************