Compare commits

...

4 Commits

Author SHA1 Message Date
ea00679b9f minor refactoring...
Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
2023-10-14 02:51:20 +03:00
9a1d851112 refactoring...
Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
2023-10-14 02:42:29 +03:00
34105a355c bugfix...
Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
2023-10-13 22:19:16 +03:00
4690bc0770 lots of fixes + started major refactoring...
Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
2023-10-13 22:14:36 +03:00
3 changed files with 502 additions and 163 deletions

View File

@ -54,13 +54,23 @@
display: block; display: block;
position: relative; position: relative;
/* XXX do a better calculation... */
width: calc(100% - var(--button-size) - var(--outline-padding) * 2); width: calc(100% - var(--button-size) - var(--outline-padding) * 2);
padding-left: var(--outline-padding); padding: 1em var(--outline-padding);
padding-right: var(--outline-padding); padding-bottom: 1.2em
} }
/* virtual empty block... */
.editor .outline:empty:after {
content: "Empty";
display: block;
font-style: italic;
color: rgba(0,0,0,0.2);
}
.editor .outline:empty:hover:after {
}
.editor .outline .block { .editor .outline .block {
position: relative; position: relative;
outline: none; outline: none;
@ -110,6 +120,7 @@
pointer-events: none; pointer-events: none;
} }
/* block hover... */ /* block hover... */
.editor .outline:empty:hover:after,
.editor .outline .block:hover>.view { .editor .outline .block:hover>.view {
background: linear-gradient( background: linear-gradient(
90deg, 90deg,
@ -127,6 +138,7 @@
} }
/* clickable things in view */ /* clickable things in view */
.editor .outline .block>.view a, .editor .outline .block>.view a,
.editor .outline .block>.view pre,
.editor .outline .block>.view input { .editor .outline .block>.view input {
pointer-events: auto; pointer-events: auto;
} }
@ -138,7 +150,7 @@ editor .outline .block:focus {
outline: none; outline: none;
} }
.editor .outline .block:focus>.text { .editor .outline .block:focus>.text {
background: rgba(0,0,0,0.1); background: rgba(0,0,0,0.07);
} }
.editor .outline .block.focused:not(:focus)>.text { .editor .outline .block.focused:not(:focus)>.text {
background: rgba(0,0,0,0.01); background: rgba(0,0,0,0.01);
@ -453,6 +465,11 @@ editor .outline .block:focus {
-1 * var(--checkbox-size) -1 * var(--checkbox-size)
- var(--checkbox-margin)); - var(--checkbox-margin));
} }
/* status... */
.editor .outline .block>.view .completion[completion]:before {
content: "(" attr(completion) ")";
color: gray;
}
/*---------------------------------------------------------- Code ---*/ /*---------------------------------------------------------- Code ---*/
@ -464,11 +481,13 @@ editor .outline .block:focus {
font-family: monospace; font-family: monospace;
background: rgba(0,0,0,0.07); background: rgba(0,0,0,0.07);
border-radius: 0.2em; border-radius: 0.2em;
outline: none;
} }
.editor .outline .block>.view pre>code { .editor .outline .block>.view pre>code {
display: block; display: block;
padding: 0.6em 0.6em; padding: 0.6em 0.6em;
padding-bottom: 0.8em; padding-bottom: 0.8em;
outline: none;
} }

View File

@ -25,6 +25,303 @@ var atLine = function(elem, index){
//---------------------------------------------------------------------
var plugin = {
// XXX make this more generic...
style: function(editor, elem, style, code=undefined){
style = [style].flat()
editor.__styles = [...new Set([
...(editor.__styles ?? []),
...style,
])]
return function(_, text){
elem.style ??= []
elem.style.push(...style)
return code
?? text } },
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
var attributes = {
__proto__: plugin,
__pre_parse__: function(text, editor, elem){
return text
// hidden attributes...
// XXX make this generic...
// collapsed...
.replace(/(\n|^)\s*collapsed::\s*(.*)\s*(\n|$)/,
function(_, value){
elem.collapsed = value.trim() == 'true'
return '' })
// id...
.replace(/(\n|^)\s*id::\s*(.*)\s*(\n|$)/,
function(_, value){
elem.id = value.trim()
return '' }) },
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
var blocks = {
__proto__: plugin,
__pre_parse__: function(text, editor, elem){
return text
// markdown...
// style: headings...
.replace(/^(?<!\\)######\s+(.*)$/m, this.style(editor, elem, 'heading-6'))
.replace(/^(?<!\\)#####\s+(.*)$/m, this.style(editor, elem, 'heading-5'))
.replace(/^(?<!\\)####\s+(.*)$/m, this.style(editor, elem, 'heading-4'))
.replace(/^(?<!\\)###\s+(.*)$/m, this.style(editor, elem, 'heading-3'))
.replace(/^(?<!\\)##\s+(.*)$/m, this.style(editor, elem, 'heading-2'))
.replace(/^(?<!\\)#\s+(.*)$/m, this.style(editor, elem, 'heading-1'))
// style: list...
//.replace(/^(?<!\\)[-\*]\s+(.*)$/m, style('list-item'))
.replace(/^\s*(.*)(?<!\\):\s*$/m, this.style(editor, elem, 'list'))
.replace(/^\s*(.*)(?<!\\)#\s*$/m, this.style(editor, elem, 'numbered-list'))
// style: misc...
.replace(/^\s*(?<!\\)>\s+(.*)$/m, this.style(editor, elem, 'quote'))
.replace(/^\s*(?<!\\)((\/\/|;)\s+.*)$/m, this.style(editor, elem, 'comment'))
.replace(/^\s*(?<!\\)NOTE:?\s*(.*)$/m, this.style(editor, elem, 'NOTE'))
.replace(/^\s*(?<!\\)XXX\s+(.*)$/m, this.style(editor, elem, 'XXX'))
.replace(/^(.*)\s*(?<!\\)XXX$/m, this.style(editor, elem, 'XXX')) } ,
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// XXX add actions...
var quoted = {
__proto__: plugin,
encode: function(text){
return text
.replace(/(?<!\\)&/g, '&amp;')
.replace(/(?<!\\)</g, '&lt;')
.replace(/(?<!\\)>/g, '&gt;')
.replace(/\\(?!`)/g, '\\\\') },
// can be used in:
// <string>.replace(quoted.pattern, quoted.handler)
quote_pattern: /(?<!\\)`(?=[^\s])(([^`]|\\`)*[^\s])(?<!\\)`/gm,
quote: function(_, code){
return `<code>${ this.encode(code) }</code>` },
pre_pattern: /(?<!\\)```(.*\s*\n)((\n|.)*?)\h*(?<!\\)```/g,
pre: function(_, language, code){
language = language.trim()
language = language ?
'language-'+language
: language
return `<pre>`
+`<code contenteditable="true" class="${language}">${
this.encode(code)
}</code>`
+`</pre>` },
map: function(text, func){
return text.replace(this.pre_pattern, func) },
replace: function(text, index, updated){
return this.map(text,
function(match, language, code){
return index-- != 0 ?
match
: ('```'+language
+ (typeof(updated) == 'function' ?
updated(code)
: updated)
+'```') }) },
toHTML: function(text){
return this.map(text, this.handler) },
__pre_parse__: function(text, editor, elem){
return text
.replace(this.pre_pattern, this.pre.bind(this))
.replace(this.quote_pattern, this.quote.bind(this)) },
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// XXX Hackish...
var syntax = {
__proto__: plugin,
update: function(){
window.hljs
&& hljs.highlightAll()
return this },
__setup__: function(editor){
return this.update() },
// XXX make a local update...
__changed__: function(evt, editor, node){
return this.update() },
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
var tables = {
__proto__: plugin,
__parse__: function(text, editor, elem){
return text
.replace(/^\s*(?<!\\)\|\s*((.|\n)*)\s*\|\s*$/,
function(_, body){
return `<table><tr><td>${
body
.replace(/\s*\|\s*\n\s*\|\s*/gm, '</td></tr>\n<tr><td>')
.replace(/\s*\|\s*/gm, '</td><td>')
}</td></td></table>` }) },
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
var styling = {
__proto__: plugin,
__parse__: function(text, editor, elem){
return text
// markers...
.replace(/(\s*)(?<!\\)(FEATURE:|Q:|Question:|Note:)(\s*)/gm,
'$1<b class="$2">$2</b>$3')
.replace(/(\s*)(?<!\\)(ASAP|BUG|FIX|HACK|STUB|WARNING|CAUTION)(\s*)/gm,
'$1<span class="highlight $2">$2</span>$3')
// elements...
.replace(/(\n|^)(?<!\\)---*\h*(\n|$)/m, '$1<hr>')
// basic styling...
// XXX revise...
.replace(/(?<!\\)\*(?=[^\s*])(([^*]|\\\*)*[^\s*])(?<!\\)\*/gm, '<b>$1</b>')
.replace(/(?<!\\)~(?=[^\s~])(([^~]|\\~)*[^\s~])(?<!\\)~/gm, '<s>$1</s>')
.replace(/(?<!\\)_(?=[^\s_])(([^_]|\\_)*[^\s_])(?<!\\)_/gm, '<i>$1</i>')
// code/quoting...
//.replace(/(?<!\\)`(?=[^\s])(([^`]|\\`)*[^\s])(?<!\\)`/gm, quote)
// XXX support "\==" in mark...
.replace(/(?<!\\)==(?=[^\s])(.*[^\s])(?<!\\)==/gm, '<mark>$1</mark>')
// links...
.replace(/(?<!\\)\[([^\]]*)\]\(([^)]*)\)/g, '<a href="$2">$1</a>')
.replace(/((?:https?:|ftps?:)[^\s]*)(\s*)/g, '<a href="$1">$1</a>$2') },
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// XXX use ligatures for these???
var symbols = {
__proto__: plugin,
__parse__: function(text, editor, elem){
return text
// characters...
.replace(/(?<!\\)\(i\)/gm, '🛈')
.replace(/(?<!\\)\(c\)/gm, '©')
.replace(/(?<!\\)\/!\\/gm, '⚠')
.replace(/(?<!\\)---(?!-)/gm, '&mdash;')
.replace(/(?<!\\)--(?!-)/gm, '&ndash;') },
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
var escaping = {
__proto__: plugin,
__post_parse__: function(text, editor, elem){
return text
// quoting...
// NOTE: this must be last...
.replace(/(?<!\\)\\(.)/gm, '$1') },
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// XXX add actions...
var tasks = {
__proto__: plugin,
updateStatus: function(editor, node){
node = editor.get(node)
if(node == null){
return this }
var state = node
.querySelector('.view')
.querySelector('.completion')
if(state){
var c =
((node.querySelectorAll('input[type=checkbox]:checked').length
/ node.querySelectorAll('input[type=checkbox]').length)
* 100)
.toFixed(0)
!isNaN(c)
&& state.setAttribute('completion', c +'%') }
return this },
updateBranch: function(editor, node){
if(!node){
return this }
var outline = editor.outline
var p = node
while(p !== outline){
this.updateStatus(editor, p)
p = editor.get(p, 'parent') }
return this },
updateAll: function(editor){
for(var e of [...editor.outline.querySelectorAll('.block>.view .completion')]){
this.updateStatus(editor, e) }
return this },
__setup__: function(editor){
return this.updateAll(editor) },
__parse__: function(text, editor, elem){
return text
// block checkboxes...
// NOTE: these are separate as we need to align block text
// to leading chekbox...
.replace(/^\s*(?<!\\)\[[_ ]\]\s*/m,
this.style(editor, elem, 'todo', '<input type="checkbox">'))
.replace(/^\s*(?<!\\)\[[Xx]\]\s*/m,
this.style(editor, elem, 'todo', '<input type="checkbox" checked>'))
// inline checkboxes...
.replace(/\s*(?<!\\)\[[_ ]\]\s*/gm,
this.style(editor, elem, 'check', '<input type="checkbox">'))
.replace(/\s*(?<!\\)\[[Xx]\]\s*/gm,
this.style(editor, elem, 'check', '<input type="checkbox" checked>'))
// completion...
// XXX add support for being like a todo checkbox...
.replace(/(?<!\\)\[[%]\]/gm, '<span class="completion"></span>') },
__changed__: function(evt, editor, node){
return this.updateBranch(editor, node) },
__click__: function(evt, editor, elem){
// toggle checkbox...
if(elem.type == 'checkbox'){
var node = editor.get(elem)
var text = node.querySelector('.code')
// get the checkbox order...
var i = [...node.querySelectorAll('input[type=checkbox]')].indexOf(elem)
var to = elem.checked ?
'[X]'
: '[_]'
var toggle = function(m){
return i-- == 0 ?
to
: m }
text.value = text.value.replace(/\[[Xx_]\]/g, toggle)
// update status...
this.updateBranch(editor, node) }
return this },
}
//--------------------------------------------------------------------- //---------------------------------------------------------------------
// XXX experiment with a concatinative model... // XXX experiment with a concatinative model...
@ -41,6 +338,32 @@ var Outline = {
carot_jump_edge_then_block: false, carot_jump_edge_then_block: false,
plugins: [
attributes,
blocks,
quoted,
styling,
tables,
symbols,
syntax,
tasks,
// keep this last...
// XXX revise -- should this be external???
escaping,
],
runPlugins: function(method, ...args){
for(var plugin of this.plugins){
method in plugin
&& plugin[method](...args) }
return this },
threadPlugins: function(method, value, ...args){
for(var plugin of this.plugins){
method in plugin
&& (value = plugin[method](value, ...args)) }
return value },
get code(){ get code(){
return this.dom.querySelector('.code') }, return this.dom.querySelector('.code') },
get outline(){ get outline(){
@ -101,12 +424,13 @@ var Outline = {
// groups defaulting to .focused as base... // groups defaulting to .focused as base...
if(['parent', 'next', 'prev', 'children', 'siblings'].includes(node)){ if(['parent', 'next', 'prev', 'children', 'siblings'].includes(node)){
return this.get('focused', node) } return this.get('focused', node) }
// helpers... // helpers...
var parent = function(node){ var parent = function(node){
return node === outline ? return node === outline ?
node outline
: node?.parentElement?.parentElement } : node.parentElement === outline ?
outline
: node.parentElement.parentElement }
var children = function(node){ var children = function(node){
return node === outline ? return node === outline ?
[...node.children] [...node.children]
@ -230,13 +554,13 @@ var Outline = {
indent = node indent = node
node = 'focused' } node = 'focused' }
var cur = this.get(node) var cur = this.get(node)
if(!cur){ if(!cur){
return } return }
var siblings = this.get(node, 'siblings') var siblings = this.get(node, 'siblings')
// deindent... // deindent...
if(!indent){ if(!indent){
var parent = this.get(node, 'parent') var parent = this.get(node, 'parent')
if(!parent.classList.contains('.outline')){ if(parent != this.outline){
var children = siblings var children = siblings
.slice(siblings.indexOf(cur)+1) .slice(siblings.indexOf(cur)+1)
parent.after(cur) parent.after(cur)
@ -312,115 +636,50 @@ var Outline = {
var elem = { var elem = {
collapsed: false, collapsed: false,
} }
// only whitespace -> keep element blank... // only whitespace -> keep element blank...
if(code.trim() == ''){ if(code.trim() == ''){
elem.text = '' elem.text = ''
return elem } return elem }
// helpers... // helpers...
var style = function(style, code=undefined){ var run = function(stage, text){
style = [style].flat() var meth = {
that.__styles = [...new Set([ pre: '__pre_parse__',
...(that.__styles ?? []), main: '__parse__',
...style, post: '__post_parse__',
])] }[stage]
return function(_, text){ return that.threadPlugins(meth, text, that, elem) }
elem.style ??= []
elem.style.push(...style) // stage: pre...
return code var text = run('pre',
?? text } } // pre-sanitize...
var quoteText = function(text){ code.replace(/\x00/g, ''))
return text // split text into parsable and non-parsable sections...
.replace(/(?<!\\)&/g, '&amp;') var sections = text
.replace(/(?<!\\)</g, '&lt;') // split fomat:
.replace(/(?<!\\)>/g, '&gt;') // [ text <match> <type> <body>, ... ]
.replace(/\\(?!`)/g, '\\\\') } .split(/(<(pre|code)(?:|\s[^>]*)>((?:\n|.)*)<\/\2>)/g)
var quote = function(_, code){ // sort out the sections...
return `<code>${quoteText(code)}</code>` } var parsable = []
var pre = function(_, language, code){ var quoted = []
language = language ? while(sections.length > 0){
'language-'+language var [section, match] = sections.splice(0, 4)
: language parsable.push(section)
return `<pre><code class="${language}">${ quoteText(code) }</code></pre>` } quoted.push(match) }
var table = function(_, body){ // stage: main...
return `<table><tr><td>${ text = run('main',
body // parse only the parsable sections...
.replace(/\s*\|\s*\n\s*\|\s*/gm, '</td></tr>\n<tr><td>') parsable.join('\x00'))
.replace(/\s*\|\s*/gm, '</td><td>') .split(/\x00/g)
}</td></td></table>` } // merge the quoted sections back in...
.map(function(section){
return [section, quoted.shift() ?? ''] })
.flat()
.join('')
// stage: post...
elem.text = run('post', text)
elem.text = code
// hidden attributes...
// XXX make this generic...
// collapsed...
.replace(/(\n|^)\s*collapsed::\s*(.*)\s*(\n|$)/,
function(_, value){
elem.collapsed = value.trim() == 'true'
return '' })
// id...
.replace(/(\n|^)\s*id::\s*(.*)\s*(\n|$)/,
function(_, value){
elem.id = value.trim()
return '' })
// markdown...
// style: headings...
.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+(.*)$/m, style('list-item'))
.replace(/^\s*(.*)(?<!\\):\s*$/m, style('list'))
.replace(/^\s*(.*)(?<!\\)#\s*$/m, style('numbered-list'))
// style: misc...
.replace(/^\s*(?<!\\)>\s+(.*)$/m, style('quote'))
.replace(/^\s*(?<!\\)((\/\/|;)\s+.*)$/m, style('comment'))
.replace(/^\s*(?<!\\)NOTE:?\s*(.*)$/m, style('NOTE'))
.replace(/^\s*(?<!\\)XXX\s+(.*)$/m, style('XXX'))
.replace(/^(.*)\s*(?<!\\)XXX$/m, style('XXX'))
.replace(/(\s*)(?<!\\)(ASAP|BUG|FIX|HACK|STUB|WARNING|CAUTION)(\s*)/gm,
'$1<span class="highlight $2">$2</span>$3')
// elements...
.replace(/(\n|^)(?<!\\)---*\h*(\n|$)/m, '$1<hr>')
// ToDo...
// NOTE: these are separate as we need to align block text
// to leading chekbox...
.replace(/^\s*(?<!\\)\[[_ ]\]\s*/m,
style('todo', '<input type="checkbox">'))
.replace(/^\s*(?<!\\)\[[Xx]\]\s*/m,
style('todo', '<input type="checkbox" checked>'))
// inline checkboxes...
.replace(/\s*(?<!\\)\[[_ ]\]\s*/gm,
style('check', '<input type="checkbox">'))
.replace(/\s*(?<!\\)\[[Xx]\]\s*/gm,
style('check', '<input type="checkbox" checked>'))
// tables...
.replace(/^\s*(?<!\\)\|\s*((.|\n)*)\s*\|\s*$/, table)
// basic styling...
// XXX revise...
.replace(/(?<!\\)\*(?=[^\s*])(([^*]|\\\*)*[^\s*])(?<!\\)\*/gm, '<b>$1</b>')
.replace(/(?<!\\)~(?=[^\s~])(([^~]|\\~)*[^\s~])(?<!\\)~/gm, '<s>$1</s>')
.replace(/(?<!\\)_(?=[^\s_])(([^_]|\\_)*[^\s_])(?<!\\)_/gm, '<i>$1</i>')
// code/quoting...
.replace(/(?<!\\)```(.*)\s*\n((\n|.)*)\h*(?<!\\)```\s*/g, pre)
.replace(/(?<!\\)`(?=[^\s])(([^`]|\\`)*[^\s])(?<!\\)`/gm, quote)
// XXX support "\==" in mark...
.replace(/(?<!\\)==(?=[^\s])(.*[^\s])(?<!\\)==/gm, '<mark>$1</mark>')
// links...
.replace(/(?<!\\)\[([^\]]*)\]\(([^)]*)\)/g, '<a href="$2">$1</a>')
.replace(/((?:https?:|ftps?:)[^\s]*)(\s*)/g, '<a href="$1">$1</a>$2')
// characters...
// XXX use ligatures for these???
.replace(/(?<!\\)\(i\)/gm, '🛈')
.replace(/(?<!\\)\(c\)/gm, '©')
.replace(/(?<!\\)\/!\\/gm, '⚠')
.replace(/(?<!\\)---(?!-)/gm, '&mdash;')
.replace(/(?<!\\)--(?!-)/gm, '&ndash;')
// quoting...
// NOTE: this must be last...
.replace(/(?<!\\)\\(.)/gm, '$1')
return elem }, return elem },
// XXX essentially here we need to remove service stuff like some // XXX essentially here we need to remove service stuff like some
// attributes (collapsed, id, ...)... // attributes (collapsed, id, ...)...
@ -482,7 +741,7 @@ var Outline = {
text = text text = text
.replace(/^\s*\n/, '') .replace(/^\s*\n/, '')
text = ('\n' + text) text = ('\n' + text)
.split(/\n(\s*)- /g) .split(/\n(\s*)(?:- |-\s*$)/gm)
.slice(1) .slice(1)
var tab = ' '.repeat(this.tab_size || 8) var tab = ' '.repeat(this.tab_size || 8)
var level = function(lst, prev_sep=undefined, parent=[]){ var level = function(lst, prev_sep=undefined, parent=[]){
@ -744,9 +1003,19 @@ var Outline = {
function(evt){ function(evt){
var elem = evt.target var elem = evt.target
// prevent focusing parent by clicking between blocks...
if(elem.classList.contains('children')){ if(elem.classList.contains('children')){
return } return }
// empty outline -> create new eleemnt...
if(elem.classList.contains('outline')
&& elem.children.length == 0){
// create new eleemnt and edit it...
var block = that.Block()
that.outline.append(block)
that.edit(block)
return }
// expand/collapse // expand/collapse
if(elem.classList.contains('view') if(elem.classList.contains('view')
&& elem.parentElement.getAttribute('tabindex')){ && elem.parentElement.getAttribute('tabindex')){
@ -769,24 +1038,15 @@ var Outline = {
if(elem.getAttribute('tabindex')){ if(elem.getAttribute('tabindex')){
elem.querySelector('.code').focus() } elem.querySelector('.code').focus() }
// toggle checkbox... that.runPlugins('__click__', evt, that, elem) })
if(elem.type == 'checkbox'){ // keyboard handling...
var node = that.get(elem)
var text = node.querySelector('.code')
// get the checkbox order...
var i = [...node.querySelectorAll('input[type=checkbox]')].indexOf(elem)
var to = elem.checked ?
'[X]'
: '[_]'
var toggle = function(m){
return i-- == 0 ?
to
: m }
text.value = text.value.replace(/\[[Xx_]\]/g, toggle) } })
// heboard handling...
outline.addEventListener('keydown', outline.addEventListener('keydown',
function(evt){ function(evt){
var elem = evt.target var elem = evt.target
// code editing...
if(elem.nodeName == 'CODE'
&& elem.getAttribute('contenteditable') == 'true'){
return }
// update element state... // update element state...
if(elem.nodeName == 'TEXTAREA'){ if(elem.nodeName == 'TEXTAREA'){
setTimeout(function(){ setTimeout(function(){
@ -795,6 +1055,28 @@ var Outline = {
// handle keyboard... // handle keyboard...
evt.key in that.keyboard evt.key in that.keyboard
&& that.keyboard[evt.key].call(that, evt) }) && that.keyboard[evt.key].call(that, evt) })
// update code block...
outline.addEventListener('keyup',
function(evt){
var elem = evt.target
// editable code...
if(elem.nodeName == 'CODE'
&& elem.getAttribute('contenteditable') == 'true'){
// XXX should we clear the syntax???
// XXX do this only if things changed...
delete elem.dataset.highlighted
var block = that.get(elem)
var code = block.querySelector('.code')
var update = elem.innerText
var i = [...block
.querySelectorAll('.view code[contenteditable=true]')]
.indexOf(elem)
// update element content...
code.value = quoted.replace(code.value, i, update)
return } })
// toggle view/code of nodes... // toggle view/code of nodes...
outline.addEventListener('focusin', outline.addEventListener('focusin',
function(evt){ function(evt){
@ -825,11 +1107,8 @@ var Outline = {
if(node.nodeName == 'TEXTAREA' if(node.nodeName == 'TEXTAREA'
&& node?.nextElementSibling?.nodeName == 'SPAN'){ && node?.nextElementSibling?.nodeName == 'SPAN'){
var block = node.parentElement var block = node.parentElement
that.update(block, { text: node.value }) } that.update(block, { text: node.value })
that.runPlugins('__changed__', evt, that, node) } })
// XXX do a plugin...
window.hljs
&& hljs.highlightAll() })
// update .code... // update .code...
var update_code_timeout var update_code_timeout
outline.addEventListener('change', outline.addEventListener('change',
@ -870,6 +1149,8 @@ var Outline = {
.replace(/&gt;/g, '>')) .replace(/&gt;/g, '>'))
console.log(`Parse: ${Date.now() - t}ms`)} console.log(`Parse: ${Date.now() - t}ms`)}
this.runPlugins('__setup__', this)
return this }, return this },
} }

View File

@ -26,11 +26,7 @@ var setup = function(){
window.editor = { window.editor = {
__proto__: Outline, __proto__: Outline,
}.setup( }.setup(
document.querySelector('.editor')) document.querySelector('.editor')) }
// XXX make this a plugin...
window.hljs
&& hljs.highlightAll() }
</script> </script>
</head> </head>
@ -56,19 +52,46 @@ var setup = function(){
an we'll not get here... an we'll not get here...
- -
- ## ToDo: - ## ToDo:
- ASAP: editor: bksapce/del at start/end of a block should join it with prev/next - ASAP: editor: backsapce/del at start/end of a block should join it with prev/next
- ASAP: editor: pressing enter in text edit mode should split text into two blocks - ASAP: editor: pressing enter in text edit mode should split text into two blocks
- ASAP: editor: shifting nodes up/down - ASAP: editor: shifting nodes up/down
- ASAP: use \\t for indent...
- ASAP: scroll into view is bad... - ASAP: scroll into view is bad...
- on item click, place the cursor where it was clicked before the code expanded... - ASAP: need to reach checkboxes via keyboard
- ~editor: semi-live update styles~ - FEATURE: read-only mode
- need to reach checkboxes via keyboard - FEATURE: `collapse-children:: true` block option -- when loading collapse all immediate children
- persistent empty first/last node (a button to create a new node) - FF: figure out a way to draw expand/collapse bullets without the use of CSS' `:has(..)`
- add completion percentage to blocks with todo's nested - Code blocks and bullets:
- _...use `[%]`, `%%`, or something similar..._ - ```
- read-only mode code
- should bulets be on the same level as nodes or offset?? ```
- _bullet should be either in the middle of the block or at the first line of code (preferred)..._
- editor as a custom element...
- Nerd fonts (option???)
- multiple node selection
- copy/paste nodes/trees
- undo
collapsed:: true
- delete node
- indent/deindent
- edit node
- FEATURE? block templates...
collapsed:: true
- something like: `TPL: [_] <editable/> -- <editable/>`
- `TPL:` -- template marker
- `<editable/>` -- field marker
- each child node will copy the template and allow editing of only fields
- not clear how to handle template changes...
- Q: can we get the caret line in a textarea???
- _...this will fix a lot of issues with moving between blocks in edit mode..._
- Q: do we use \\t for indent? (option???)
- Q: can we place the cursor on item click where it was clicked before before the code expanded?
collapsed:: true
- for example
- #### Click in this line and see where the cursor goes
- _not sure how..._
- Q: persistent empty first/last node (a button to create a new node)?
- Q: should bullets be on the same level as nodes or offset??
collapsed:: true
- A) justified to bullet: - A) justified to bullet:
* list item * list item
* list item * list item
@ -78,8 +101,11 @@ var setup = function(){
* list item * list item
block text block text
- NOTE: this is only a problem if making list-items manually -- disable??? - NOTE: this is only a problem if making list-items manually -- disable???
- FF: figure out a way to draw expand/collapse bullets without the use of CSS' `:has(..)` - empty item height is a bit off...
- Nerd fonts (options?) - ~Q: can we edit code in a code block directly? (a-la Logseq)~
- ~"percentage complete" in parent blocks with todo's nested~
- ~`.editor .outline:empty` view and behavior...~
- ~editor: semi-live update styles~
- ~do a better expand/collapse icons~ - ~do a better expand/collapse icons~
- ~loading from DOM -- fill textarea~ - ~loading from DOM -- fill textarea~
- ~focus management~ - ~focus management~
@ -89,17 +115,29 @@ var setup = function(){
- ~shift subtree up/down~ - ~shift subtree up/down~
- ~create node~ - ~create node~
- ~edit node~ - ~edit node~
- multiple node selection
- copy/paste nodes/trees
- undo
collapsed:: true
- delete node
- indent/deindent
- edit node
- empty item height is a bit off...
- ~serialize/deserialize~ - ~serialize/deserialize~
- ~add optional text styling to nodes~ - ~add optional text styling to nodes~
- -
- ## Refactoring:
- Plugin architecture
- Item parser (`.__code2html__(..)`)
- ~split out~
- ~define~/doc api
- ~define a way to extend/stack parsers~
- Format parser/generator
- split out
- define api
- experiment with clean markdown as format
- CSS
- separate out theming
- separate out settings
- Actions -- move user actions (code in `.keyboard`) into methods
- Move to `keyboard.js`
- Plugin architecture
- Q: do we need `features.js` and/or `actions.js`
- Q: do we need a concatenative API??
- `<block>.get() -> <block>`
-
- ## TEST - ## TEST
- ### Formatting: - ### Formatting:
- Styles - Styles
@ -161,12 +199,13 @@ var setup = function(){
- --- - ---
- Markers: ASAP, BUG, FIX, HACK, STUB, WARNING, and CAUTION - Markers: ASAP, BUG, FIX, HACK, STUB, WARNING, and CAUTION
- Basic task management - Basic task management
- Inline [X] checkboxes [_] - [%] Completion status
- To do items/blocks - Inline [X] checkboxes [_]
- [_] undone item - To do items/blocks
_(clicking the checkbox updates the item)_ - [_] undone item
- [X] done item _(clicking the checkbox updates the item)_
- [_] we can also add inline [x] checkboxes - [X] done item
- [_] we can also add inline [x] checkboxes and states: [%]
- links - links
- [example](about:blank) - [example](about:blank)
- https://example.com - https://example.com