From 21c386f3dcf533605c0fb31277306d146c775aca Mon Sep 17 00:00:00 2001 From: "Alex A. Naanou" Date: Mon, 9 Oct 2023 01:32:46 +0300 Subject: [PATCH] added inline checkboxes (request by XYZ) + lots of tweaks and fixes... Signed-off-by: Alex A. Naanou --- experiments/outline-editor/editor.css | 62 +++++++--- experiments/outline-editor/editor.js | 164 +++++++++++++------------- experiments/outline-editor/index.html | 11 +- 3 files changed, 137 insertions(+), 100 deletions(-) diff --git a/experiments/outline-editor/editor.css b/experiments/outline-editor/editor.css index 368b941..901d8e8 100755 --- a/experiments/outline-editor/editor.css +++ b/experiments/outline-editor/editor.css @@ -1,6 +1,7 @@ :root { --font-size: 5mm; + --item-padding: 0.2em; --button-size: 2em; font-family: sans-serif; @@ -36,14 +37,16 @@ } .editor .outline [tabindex]>span, .editor .outline [tabindex]>textarea { - --padding: 0.2em; - display: block; width: 100%; /* XXX this is a tiny bit off and using textarea's height here is off too... */ min-height: 1em; - padding: var(--padding); + padding-top: var(--item-padding); + padding-bottom: var(--item-padding); + padding-left: 0; + padding-right: 0; margin: 0; + box-sizing: border-box; font-family: sans-serif; font-size: var(--font-size); @@ -53,7 +56,7 @@ border: none; } .editor .outline [tabindex]>textarea { - height: calc(2 * var(--padding) + 1em); + height: calc(2 * var(--item-padding) + 1em); overflow: hidden; resize: none; } @@ -130,6 +133,15 @@ .editor .outline .heading-6>textarea { font-weight: bold; } +.editor .outline .heading-1>span, +.editor .outline .heading-1>textarea, +.editor .outline .heading-2>span, +.editor .outline .heading-2>textarea, +.editor .outline .heading-3>span, +.editor .outline .heading-3>textarea { + border-bottom: solid 1px rgba(0,0,0,0.05); +} + .editor .outline .heading-1>span, .editor .outline .heading-1>textarea { font-size: 2.5em; @@ -155,16 +167,20 @@ font-size: 1em; } -/* XXX EXPERIMENTAL -- not sure about this... */ +/* XXX needs to be in the middle of the first span but with universal size... */ .editor .outline .list-item:before, -.editor .outline .list>[tabindex]:before { +.editor .outline .list>[tabindex]>span:before { + --size: 0.5rem; + display: inline-block; position: absolute; content: ""; - top: 0.6em; - left: -0.8em; - width: 0.5em; - height: 0.5em; + top: calc(0.6em + var(--item-padding)); + left: calc(var(--size) * -2); + width: var(--size); + height: var(--size); + margin-top: calc(var(--size) / -2); + background: silver; } @@ -172,15 +188,31 @@ background: yellow; } +.editor.hide-comments .outline .comment { + display: none; +} .editor .outline .comment>span { color: silver; } -.editor .outline [tabindex]>span>input[type=checkbox] { - --width: 3em; +.editor .outline [tabindex]>span>input[type=checkbox].check, +.editor .outline [tabindex]>span>input[type=checkbox].todo { + --size: 1.5rem; + --height: calc(var(--font-size) + 2 * var(--item-padding)); - height: 1em; - width: var(--width); - margin-left: calc(-1 * var(--width)); + top: calc(0.6em + var(--item-padding)); + height: var(--size); + width: var(--size); + margin-top: calc(var(--size) / -2); + + /* NOTE: this appears to be needed for the em sizes above to work correctly */ + font-size: 1em; +} +.editor .outline [tabindex]>span>input[type=checkbox].todo { + position: absolute; + margin-left: calc(-1 * var(--size) - var(--item-padding)); +} +.editor .outline [tabindex]>span>input[type=checkbox].check { + transform: translateY(calc(2 * var(--item-padding))); } diff --git a/experiments/outline-editor/editor.js b/experiments/outline-editor/editor.js index 47c1d75..a8d21c4 100755 --- a/experiments/outline-editor/editor.js +++ b/experiments/outline-editor/editor.js @@ -27,46 +27,8 @@ var atLine = function(elem, index){ //--------------------------------------------------------------------- -var Node = { - dom: undefined, - document: undefined, - - get: function(){}, - - get root(){}, - get parent(){}, - get children(){}, - get next(){}, - get prev(){}, - - focus: function(){}, - edit: function(){}, - - indent: function(){ }, - deindent: function(){ }, - toggleCollapse: function(){ }, - - remove: function(){}, - - json: function(){}, - text: function(){}, - - load: function(){}, -} - -var NodeGroup = { - __proto__: Node, -} - -// XXX should this be Page or root?? -var Root = { - __proto__: NodeGroup, -} - - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -// XXX might be a good idea to do a view-action model... +// XXX experiment with a concatinative model... +// .get(..) -> Outline (view) var Outline = { dom: undefined, @@ -219,9 +181,10 @@ var Outline = { // XXX should this handle children??? update: function(node='focused', data){ var node = this.get(node) - data.collapsed ? - node.setAttribute('collapsed', '') - : node.removeAttribute('collapsed') + typeof(data.collapsed) == 'boolean' + && (data.collapsed ? + node.setAttribute('collapsed', '') + : node.removeAttribute('collapsed')) if(data.text){ var text = node.querySelector('textarea') var html = node.querySelector('span') @@ -231,7 +194,7 @@ var Outline = { html.innerHTML = parsed.text // heading... parsed.style ? - node.classList.add(parsed.style) + node.classList.add(...parsed.style) : node.classList.remove(...this.__styles__) } else { html.innerHTML = data.text } @@ -306,9 +269,10 @@ var Outline = { return this }, // block serialization... - // XXX STUB... // XXX shouild we support headings + other formatting per block??? - // XXX these should be symetrical -- now one returns text the other an object... + // XXX split this up into a generic handler + plugins... + // XXX need a way to filter input text... + // use-case: hidden attributes... __styles__: [ 'heading-1', 'heading-2', @@ -324,11 +288,13 @@ var Outline = { } var heading = function(level){ return function(_, text){ - elem.style = 'heading-'+level + elem.style ??= [] + elem.style.push('heading-'+level) return text } } var style = function(style){ return function(_, text){ - elem.style = style + elem.style ??= [] + elem.style.push(style) return text } } elem.text = code // hidden attributes... @@ -342,33 +308,37 @@ var Outline = { // id... .replace(/(\n|^)\s*id::\s*(.*)\s*(\n|$)/, function(_, value){ - elem.collapsed = value.trim() == 'true' + elem.id = value.trim() return '' }) // markdown... - // ToDo... - .replace(/^TODO\s*(.*)$/, ' $1') - .replace(/^DONE\s*(.*)$/, ' $1') // style: headings... - .replace(/^######\s*(.*)$/, style('heading-6')) - .replace(/^#####\s*(.*)$/, style('heading-5')) - .replace(/^####\s*(.*)$/, style('heading-4')) - .replace(/^###\s*(.*)$/, style('heading-3')) - .replace(/^##\s*(.*)$/, style('heading-2')) - .replace(/^#\s*(.*)$/, style('heading-1')) + .replace(/^######\s*(.*)$/m, style('heading-6')) + .replace(/^#####\s*(.*)$/m, style('heading-5')) + .replace(/^####\s*(.*)$/m, style('heading-4')) + .replace(/^###\s*(.*)$/m, style('heading-3')) + .replace(/^##\s*(.*)$/m, style('heading-2')) + .replace(/^#\s*(.*)$/m, style('heading-1')) // style: list... - .replace(/^[-\*]\s+(.*)$/, style('list-item')) - .replace(/^\s*(.*):\s*$/, style('list')) + .replace(/^[-\*]\s+(.*)$/m, style('list-item')) + .replace(/^\s*(.*):\s*$/m, style('list')) // style: misc... - .replace(/^((\/\/|;)\s+.*)$/, style('comment')) - .replace(/^XXX\s+(.*)$/, style('XXX')) - .replace(/^(.*)\s*XXX$/, style('XXX')) + .replace(/^((\/\/|;)\s+.*)$/m, style('comment')) + .replace(/^XXX\s+(.*)$/m, style('XXX')) + .replace(/^(.*)\s*XXX$/m, style('XXX')) // basic styling... // XXX these are quite naive... - .replace(/\*(.*)\*/g, '$1') - .replace(/~([^~]*)~/g, '$1') - .replace(/_([^_]*)_/g, '$1') + .replace(/\*(.*)\*/gm, '$1') + .replace(/~([^~]*)~/gm, '$1') + .replace(/_([^_]*)_/gm, '$1') // elements... - .replace(/(\n|^)---*\h*(\n|$)/, '$1
') + .replace(/(\n|^)---*\h*(\n|$)/m, '$1
') + // ToDo... + .replace(/^TODO\s*(.*)$/m, '$1') + .replace(/^DONE\s*(.*)$/m, '$1') + // checkboxes... + // XXX these can not be clicked (yet)... + .replace(/\[ \]/gm, '') + .replace(/\[[X]\]/gm, '') return elem }, // serialization... @@ -460,9 +430,19 @@ var Outline = { var html = document.createElement('span') block.append(text, html) this.update(block, data) + // place... var cur = this.get() - place && cur - && cur[place](block) + if(place && cur){ + place = place == 'prev' ? + 'before' + : place + ;(place == 'next' + && (cur.querySelector('[tabindex]') + || cur.nextElementSibling)) ? + this.get(place).before(block) + : (place == 'before' || place == 'after') ? + cur[place](block) + : undefined } return block }, load: function(data){ var that = this @@ -481,6 +461,9 @@ var Outline = { .clear() .outline .append(...level(data)) + // update sizes of all the textareas (transparent)... + for(var e of [...this.outline.querySelectorAll('textarea')]){ + e.updateSize() } return this }, sync: function(){ @@ -569,25 +552,31 @@ var Outline = { O: function(evt){ if(evt.target.nodeName != 'TEXTAREA'){ evt.preventDefault() - this.Block('before')?.querySelector('textarea')?.focus() } }, + this.Block('before') + ?.querySelector('textarea') + ?.focus() } }, o: function(evt){ if(evt.target.nodeName != 'TEXTAREA'){ evt.preventDefault() - this.Block('after')?.querySelector('textarea')?.focus() } }, + this.Block('next') + ?.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() - : this.get()?.querySelector('textarea')?.focus() }, + this.Block('next') + ?.querySelector('textarea') + ?.focus() + : this.get() + ?.querySelector('textarea') + ?.focus() }, Escape: function(evt){ - this.outline.querySelector('textarea:focus')?.parentElement?.focus() }, + this.outline.querySelector('textarea:focus') + ?.parentElement + ?.focus() }, Delete: function(evt){ if(this.get('edited')){ return } @@ -620,14 +609,27 @@ var Outline = { outline.addEventListener('click', function(evt){ var elem = evt.target - // toggle checkbox... - if(elem.nodeName == 'INPUT' && elem.type == 'checkbox'){ + // todo: toggle checkbox... + if(elem.classList.contains('todo')){ var node = elem.parentElement.parentElement var text = node.querySelector('textarea') text.value = elem.checked ? text.value.replace(/^\s*TODO(\s*)/, 'DONE$1') - : text.value.replace(/^\s*DONE(\s*)/, 'TODO$1') } }) + : text.value.replace(/^\s*DONE(\s*)/, 'TODO$1') } + // check: toggle checkbox... + if(elem.classList.contains('check')){ + var node = elem.parentElement.parentElement + var text = node.querySelector('textarea') + var i = [...node.querySelectorAll('.check')].indexOf(elem) + var to = elem.checked ? + '[X]' + : '[ ]' + var toggle = function(m){ + return i-- == 0 ? + to + : m } + text.value = text.value.replace(/\[[X ]\]/g, toggle) } }) // heboard handling... outline.addEventListener('keydown', function(evt){ diff --git a/experiments/outline-editor/index.html b/experiments/outline-editor/index.html index 0a6d57b..2910f3a 100755 --- a/experiments/outline-editor/index.html +++ b/experiments/outline-editor/index.html @@ -1,3 +1,4 @@ + @@ -27,8 +28,7 @@ var setup = function(){
- # Outline editor prototype -- ## TODO - - editor: enter on an expanded parent node should create child (currently next sibling) +- ## ToDo - editor: bksapce/del at start/end of a block should join it with prev/next - editor: pressing enter in text edit mode should split text into two blocks - editor: caret @@ -51,6 +51,7 @@ var setup = function(){ - indent/deindent - edit node - copy/paste nodes/trees + - markdown tables - ~serialize~/deserialize - ~add optional text styling to nodes~ - @@ -71,12 +72,14 @@ var setup = function(){ - // C-style comment - ; ASM-style comment - XXX Highlight - - line + - Line - --- - - basic inline *bold*, _italic_ and ~striked~ + - Basic inline *bold*, _italic_ and ~striked~ - To do items - TODO undone item - DONE done item + _(clicking the checkbox updates the item)_ + - Inline [X] checkboxes [ ] - A collapsed:: true - a