357 lines
9.3 KiB
JavaScript
Raw Normal View History

/**********************************************************************
*
*
*
**********************************************************************/
//---------------------------------------------------------------------
var Outline = {
dom: undefined,
// 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())
var cur = getFocused()
|| getEditable()?.parentElement
place && cur
&& cur[place](block)
return block },
// XXX
get: function(){
},
focused: function(offset, selector){
},
edited: function(offset){
return this.focused(offset, 'textarea')},
focus: function(node='focused'){},
edit: function(node='focused'){},
indent: function(node='focused'){},
collapse: function(node='focused'){},
// block serialization...
__code2html__: function(code){
return code },
__html2code__: function(html){
return html },
// 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() },
text: function(node, indent=''){
node ??= this.json(node)
var text = ''
for(var elem of node){
text +=
indent
+'- '
+ elem.text
.replace(/\n/g, '\n '+indent)
+'\n'
+ this.text(elem.children || [], indent+' ') }
return text },
// XXX use .__code2html__(..)
load: function(){},
keyboard: {
// vertical navigation...
ArrowUp: function(evt){
var action = getFocused
var edited = this.dom.querySelector('.editor textarea:focus')
if(edited){
if(!atLine(0)){
return }
action = getEditable }
evt.preventDefault()
action(-1)?.focus() },
ArrowDown: function(evt, offset=1){
var action = getFocused
var edited = this.dom.querySelector('.editor textarea:focus')
if(edited){
if(!atLine(-1)){
return }
//window.getSelection()
action = getEditable }
evt.preventDefault()
action(1)?.focus() },
// 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 }
if(this.left_key_expands){
toggleCollapse(true)
getFocused('parent')?.focus()
} else {
evt.shiftKey ?
toggleCollapse(true)
: getFocused('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')
child?.focus()
if(!child){
getFocused(1)?.focus() }
} else {
evt.shiftKey ?
toggleCollapse(false)
: getFocused('child')?.focus() } },
// indent...
Tab: function(evt){
evt.preventDefault()
var editable = getEditable()
var node = indentNode(!evt.shiftKey)
;(editable ?
editable
: node)?.focus() },
// edit mode...
O: function(evt){
if(evt.target.nodeName != 'TEXTAREA'){
evt.preventDefault()
this.Block('before')?.querySelector('textarea')?.focus() } },
o: function(evt){
if(evt.target.nodeName != 'TEXTAREA'){
evt.preventDefault()
this.Block('after')?.querySelector('textarea')?.focus() } },
Enter: function(evt){
/*if(evt.target.isContentEditable){
// XXX create new node...
return }
//*/
if(evt.ctrlKey
|| evt.shiftKey){
return }
evt.preventDefault()
evt.target.nodeName == 'TEXTAREA' ?
this.Block('after')?.querySelector('textarea')?.focus()
: getFocused()?.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()
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 },
}
//---------------------------------------------------------------------
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 : */