diff --git a/experiments/outline-editor/editor.css b/experiments/outline-editor/editor.css index dba4769..e0706e6 100755 --- a/experiments/outline-editor/editor.css +++ b/experiments/outline-editor/editor.css @@ -27,7 +27,7 @@ font-size: var(--font-size); /*text-size-adjust: none;*/ - text-size-adjust: 150%; + text-size-adjust: 180%; } .editor { diff --git a/experiments/outline-editor/editor.js b/experiments/outline-editor/editor.js index c2f7100..e186d2d 100755 --- a/experiments/outline-editor/editor.js +++ b/experiments/outline-editor/editor.js @@ -673,7 +673,6 @@ var JSONOutline = { at: function(index){}, indent: function(){}, - deindent: function(){}, shift: function(){}, show: function(){}, toggleCollapse: function(){}, @@ -692,6 +691,7 @@ var JSONOutline = { } + // XXX experiment with a concatinative model... // .get(..) -> Outline (view) var Outline = { @@ -1035,9 +1035,9 @@ var Outline = { return node }, // edit... - indent: function(node='focused', indent=true){ + indent: function(node='focused', indent='in'){ // .indent() - if(node === true || node === false){ + if(node === 'in' || node === 'out'){ indent = node node = 'focused' } var cur = this.get(node) @@ -1045,7 +1045,7 @@ var Outline = { return } var siblings = this.get(node, 'siblings') // deindent... - if(!indent){ + if(indent == 'out'){ var parent = this.get(node, 'parent') if(parent != this.outline){ var children = siblings @@ -1053,21 +1053,29 @@ var Outline = { parent.after(cur) children.length > 0 && cur.lastChild.append(...children) + this.setUndo( + this.path(cur), + 'indent', + ['in']) this.__change__() } // indent... } else { var parent = siblings[siblings.indexOf(cur) - 1] if(parent){ parent.lastChild.append(cur) + this.setUndo( + this.path(cur), + 'indent', + ['out']) this.__change__()} } return cur }, - deindent: function(node='focused', indent=false){ - return this.indent(node, indent) }, shift: function(node='focused', direction){ if(node == 'up' || node == 'down'){ direction = node node = 'focused' } - if(direction == null){ + if(direction == null + || (direction !== 'up' + && direction != 'down')){ return } node = this.get(node) var focused = node.classList.contains('focused') @@ -1083,22 +1091,46 @@ var Outline = { siblings[i+1].after(node) focused && this.focus() } + this.setUndo( + this.path(node), + 'shift', + [direction == 'up' ? + 'down' + : 'up']) this.__change__() return this }, - 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 }, + // 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) + // XXX HACK... + this.setUndo( + undefined, + 'load', + [data]) + this.__change__() + return this }, + clear: function(){ + this.setUndo( + undefined, + 'load', + [this.json()]) + this.outline.innerText = '' + this.__change__() + return this }, + + // expand/collapse... toggleCollapse: function(node='focused', state='next'){ var that = this if(node == 'all'){ @@ -1125,24 +1157,20 @@ var Outline = { elem.updateSize() } } this.__change__() return node }, - remove: function(node='focused', offset){ - var elem = this.get(...arguments) - 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) - this.__change__() - return this }, - clear: function(){ - this.outline.innerText = '' - this.__change__() - return this }, + 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 }, // crop... crop: function(node='focused'){ @@ -1150,6 +1178,7 @@ var Outline = { for(var block of [...this.outline.querySelectorAll('[cropped]')]){ block.removeAttribute('cropped') } this.get(...arguments).setAttribute('cropped', '') + // build header path... this.header.innerHTML = `/ ` + this.path(...arguments, 'text') @@ -1158,27 +1187,63 @@ var Outline = { return `${s} ` }) .join(' / ') return this }, - uncrop: function(mode=1){ + uncrop: function(count=1){ var outline = this.outline var top = this.get(0) for(var block of [...this.outline.querySelectorAll('[cropped]')]){ block.removeAttribute('cropped') } // crop parent if available... - while(mode != 'all' - && mode > 0 + while(count != 'all' + && count > 0 && top !== outline){ top = this.get(top, 'parent') - mode-- } - if(mode == 'all' || top === outline){ + count-- } + if(count == 'all' || top === outline){ this.dom.classList.remove('crop') this.header.innerHTML = '' } else { this.crop(top) } return this }, + // 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, + setUndo: function(path, action, args){ + ;(this.__undo_stack ??= []).push([path, action, args]) + this.__redo_stack = undefined + return this }, + 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] } + var [path, action, args] = from.pop() + var l = from.length + path != null + && this.focus(path) + this[action](...args) + 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 }, + // block render... - // XXX need a way to filter input text... - // use-case: hidden attributes... // NOTE: this is auto-populated by .__code2html__(..) __styles: undefined, __code2html__: function(code, elem={}){ @@ -1454,7 +1519,7 @@ var Outline = { .clear() .outline .append(...level(data)) - //* XXX do we actually need this??? + /* XXX do we actually need this??? // update sizes of all the textareas (transparent)... setTimeout(function(){ for(var e of [...that.outline.querySelectorAll('textarea')]){ @@ -1609,8 +1674,7 @@ var Outline = { this.focus(-1) }, PageUp: function(evt){ var that = this - var edited = this.get('edited') - if(edited){ + if(this.get('edited')){ return } if(evt.shiftKey || evt.ctrlKey){ @@ -1624,8 +1688,7 @@ var Outline = { viewport[0], 'prev') } }, PageDown: function(evt){ var that = this - var edited = this.get('edited') - if(edited){ + if(this.get('edited')){ return } if(evt.shiftKey || evt.ctrlKey){ @@ -1643,7 +1706,9 @@ var Outline = { evt.preventDefault() var edited = this.get('edited') var node = this.show( - this.indent(!evt.shiftKey)) + this.indent(evt.shiftKey ? + 'out' + : 'in')) // keep focus in node... ;(edited ? edited @@ -1686,12 +1751,29 @@ var Outline = { if(this.get('edited')){ this.focus() } else { - evt.shiftKey ? - this.uncrop('all') - : this.uncrop() } }, + this.uncrop() } }, + s_Escape: function(evt){ + if(this.get('edited')){ + this.focus() + } else { + this.uncrop('all') } }, c: function(evt){ if(!this.get('edited')){ this.crop() } }, + 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() } }, Delete: function(evt){ var edited = this.get('edited') @@ -1731,11 +1813,10 @@ var Outline = { this.remove(edited) return } }, - d: function(evt){ + c_d: function(evt){ // toggle done... - if(evt.ctrlKey){ - evt.preventDefault() - tasks.toggleDone(this) } }, + evt.preventDefault() + tasks.toggleDone(this) }, // toggle checkbox... ' ': function(evt){ @@ -1835,8 +1916,26 @@ var Outline = { if(that.runPlugins('__keydown__', evt, that, evt.target) !== true){ return } // handle keyboard... - evt.key in that.keyboard - && that.keyboard[evt.key].call(that, evt) }) + 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 } } }) // update code block... outline.addEventListener('keyup', function(evt){ @@ -1866,6 +1965,7 @@ var Outline = { that.get('focused')?.classList?.add('focused') } // textarea... if(elem.classList.contains('code')){ + elem.dataset.original = elem.value elem.updateSize() } // XXX do we need this??? @@ -1875,10 +1975,20 @@ var Outline = { var elem = evt.target // update code... if(elem.classList.contains('code')){ - var block = elem.parentElement + var block = that.get(elem) // clean out attrs... elem.value = that.parseBlockAttrs(elem.value).text that.update(block) + // 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 } // give the browser a chance to update the DOM... // XXX revise... setTimeout(function(){ @@ -1917,6 +2027,7 @@ var Outline = { .replace(/</g, '<') .replace(/>/g, '>')) console.log(`Parse: ${Date.now() - t}ms`) } + this.clearUndo() this.runPlugins('__setup__', this) diff --git a/experiments/outline-editor/index.html b/experiments/outline-editor/index.html index ffaf296..4afc7aa 100755 --- a/experiments/outline-editor/index.html +++ b/experiments/outline-editor/index.html @@ -51,9 +51,8 @@ var setup = function(){ - BUG: mobile browsers behave quite chaotically ignoring parts of the styling... - - ## ToDo: - - undo - collapsed:: true - - edit stack (position, action, ...) + - copy/paste nodes/trees + - FEATURE: read-only mode - auto-shift done blocks to the end of siblings... (option?) - ...or should this be `sort:: done` -- i.e. sort children by done status?? - codeblock as a block @@ -64,8 +63,6 @@ var setup = function(){ code ``` - _bullet should be either in the middle of the block or at the first line of code (preferred)..._ - - copy/paste nodes/trees - - FEATURE: read-only mode - export html - embed css - cleanup html @@ -90,10 +87,6 @@ var setup = function(){ - each child node will copy the template and allow editing of only fields - not clear how to handle template changes... - JSON API - - `.path(..)` - - `.get(..)` - - `.at(..)` - - ... - cli - Q: do we use \\t for indent? (option???) - Q: persistent empty first/last node (a button to create a new node)? @@ -111,6 +104,7 @@ var setup = function(){ - empty item height is a bit off... - search? - _...not sure if search should be internal or external yet..._ + - DONE undo - DONE crop: make path clickable - DONE Q: crop: should we control crop via "crop-in"/"crop-out" instead of crop/uncrop?? - _crop-in/crop-out seems more natural..._