refactoring + lots of tweaks and fixes...

Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
Alex A. Naanou 2023-10-14 22:49:02 +03:00
parent ec04205bfa
commit 7b755b413c
3 changed files with 191 additions and 89 deletions

View File

@ -23,6 +23,20 @@ var atLine = function(elem, index){
return false } return false }
/*
function clickPoint(x,y){
document
.elementFromPoint(x, y)
.dispatchEvent(
new MouseEvent( 'click', {
view: window,
bubbles: true,
cancelable: true,
screenX: x,
screenY: y,
} )) }
//*/
//--------------------------------------------------------------------- //---------------------------------------------------------------------
@ -147,6 +161,39 @@ var quoted = {
return text return text
.replace(this.pre_pattern, this.pre.bind(this)) .replace(this.pre_pattern, this.pre.bind(this))
.replace(this.quote_pattern, this.quote.bind(this)) }, .replace(this.quote_pattern, this.quote.bind(this)) },
// XXX is this a good strategy???
__state: undefined,
__keydown__: function(evt, editor, elem){
// code editing...
if(elem.nodeName == 'CODE'
&& elem.getAttribute('contenteditable') == 'true'){
// XXX can keydown and keyup be triggered from different elements???
this.__state = elem.innerText
return false } },
// defined <plugin>.__editedview__(..) handler
__keyup__: function(evt, editor, elem){
var elem = evt.target
if(elem.nodeName == 'CODE'
&& elem.getAttribute('contenteditable') == 'true'){
// trigger if state actually changed..
this.__state != elem.innerText
&& editor.runPlugins('__editedview__', evt, editor, elem) } },
__focusout__: function(){
this.__state = undefined },
__editedview__: function(evt, editor, elem){
// editable code...
var block = editor.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 this },
} }
@ -164,8 +211,22 @@ var syntax = {
__setup__: function(editor){ __setup__: function(editor){
return this.update() }, return this.update() },
// XXX make a local update... // XXX make a local update...
__changed__: function(evt, editor, node){ __editedcode__: function(evt, editor, elem){
return this.update() }, return this.update(elem) },
__editedview__: function(evt, editor, elem){
// XXX should we also clear the syntax???
delete elem.dataset.highlighted
return this },
// XXX this removes highlighting, can we make it update live???
__focusin__: function(evt, editor, elem){
if(elem.nodeName == 'CODE'
&& elem.getAttribute('contenteditable') == 'true'){
elem.classList.remove('hljs') } },
__focusout__: function(evt, editor, elem){
if(elem.nodeName == 'CODE'
&& elem.getAttribute('contenteditable') == 'true'){
this.update(elem) }
return this },
} }
@ -301,7 +362,7 @@ var tasks = {
// completion... // completion...
// XXX add support for being like a todo checkbox... // XXX add support for being like a todo checkbox...
.replace(/(?<!\\)\[[%]\]/gm, '<span class="completion"></span>') }, .replace(/(?<!\\)\[[%]\]/gm, '<span class="completion"></span>') },
__changed__: function(evt, editor, node){ __editedcode__: function(evt, editor, node){
return this.updateBranch(editor, node) }, return this.updateBranch(editor, node) },
__click__: function(evt, editor, elem){ __click__: function(evt, editor, elem){
// toggle checkbox... // toggle checkbox...
@ -341,6 +402,9 @@ var Outline = {
carot_jump_edge_then_block: false, carot_jump_edge_then_block: false,
// The order of plugins can be significant in the following cases:
// - parsing
// - event dropping
plugins: [ plugins: [
attributes, attributes,
blocks, blocks,
@ -355,11 +419,14 @@ var Outline = {
// XXX revise -- should this be external??? // XXX revise -- should this be external???
escaping, escaping,
], ],
// NOTE: if a handler returns false it will break plugin execution...
// XXX is this the right way to go???
runPlugins: function(method, ...args){ runPlugins: function(method, ...args){
for(var plugin of this.plugins){ for(var plugin of this.plugins){
method in plugin if(method in plugin){
&& plugin[method](...args) } if(plugin[method](...args) === false){
return this }, return false } } }
return true },
threadPlugins: function(method, value, ...args){ threadPlugins: function(method, value, ...args){
for(var plugin of this.plugins){ for(var plugin of this.plugins){
method in plugin method in plugin
@ -629,7 +696,6 @@ var Outline = {
return this }, return this },
// block serialization... // block serialization...
// XXX split this up into a generic handler + plugins...
// XXX need a way to filter input text... // XXX need a way to filter input text...
// use-case: hidden attributes... // use-case: hidden attributes...
// NOTE: this is auto-populated by .__code2html__(..) // NOTE: this is auto-populated by .__code2html__(..)
@ -693,11 +759,11 @@ var Outline = {
// - text in the above block ('-' needs to be quoted) // - text in the above block ('-' needs to be quoted)
// - next block // - next block
__code2text__: function(code){ __code2text__: function(code){
// XXX return code
}, .replace(/(\n\s*)-/g, '$1\\-') },
__text2code__: function(text){ __text2code__: function(text){
// XXX return text
}, .replace(/(\n\s*)\\-/g, '$1-') },
// serialization... // serialization...
data: function(elem, deep=true){ data: function(elem, deep=true){
@ -711,8 +777,10 @@ var Outline = {
} }, } },
json: function(node){ json: function(node){
var that = this var that = this
node ??= this.outline var children = [...(node ?
return [...node.lastChild.children] node.lastChild.children
: this.outline.children)]
return children
.map(function(elem){ .map(function(elem){
return that.data(elem) }) }, return that.data(elem) }) },
// XXX add option to customize indent size... // XXX add option to customize indent size...
@ -727,7 +795,7 @@ var Outline = {
for(var elem of node){ for(var elem of node){
text.push( text.push(
level +'- ' level +'- '
+ elem.text + this.__code2text__(elem.text)
.replace(/\n/g, '\n'+ level +' ') .replace(/\n/g, '\n'+ level +' ')
+ (elem.collapsed ? + (elem.collapsed ?
'\n'+level+' ' + 'collapsed:: true' '\n'+level+' ' + 'collapsed:: true'
@ -741,6 +809,7 @@ var Outline = {
.join('\n') }, .join('\n') },
parse: function(text){ parse: function(text){
var that = this
text = text text = text
.replace(/^\s*\n/, '') .replace(/^\s*\n/, '')
text = ('\n' + text) text = ('\n' + text)
@ -765,10 +834,10 @@ var Outline = {
collapsed = value == 'true' collapsed = value == 'true'
return '' }) return '' })
parent.push({ parent.push({
text: block text: that.__text2code__(block
// normalize indent... // normalize indent...
.split(new RegExp('\n'+sep+' ', 'g')) .split(new RegExp('\n'+sep+' ', 'g'))
.join('\n'), .join('\n')),
collapsed, collapsed,
children: [], children: [],
}) })
@ -850,6 +919,18 @@ var Outline = {
&& (code.innerHTML = this.text()) && (code.innerHTML = this.text())
return this }, return this },
// Actions...
prev: function(){},
next: function(){},
above: function(){},
below: function(){},
up: function(){},
down: function(){},
left: function(){},
right: function(){},
// XXX move the code here into methods/actions... // XXX move the code here into methods/actions...
// XXX add scrollIntoView(..) to nav... // XXX add scrollIntoView(..) to nav...
// XXX use keyboard.js... // XXX use keyboard.js...
@ -862,16 +943,10 @@ var Outline = {
var that = this var that = this
var edited = this.get('edited') var edited = this.get('edited')
if(edited){ if(edited){
var c = edited.selectionStart var line = edited.getTextGeometry().line
var jump = function(){ if(line == 0){
if(edited.selectionStart == 0){ evt.preventDefault()
// needed to remember the position... that.focus('edited', 'prev') }
edited.selectionStart = c
edited.selectionEnd = c
that.focus('edited', -1) } }
this.carot_jump_edge_then_block ?
jump()
: setTimeout(jump, 0)
} else { } else {
evt.preventDefault() evt.preventDefault()
this.focus('focused', -1) } }, this.focus('focused', -1) } },
@ -879,16 +954,10 @@ var Outline = {
var that = this var that = this
var edited = this.get('edited') var edited = this.get('edited')
if(edited){ if(edited){
var c = edited.selectionStart var {line, lines} = edited.getTextGeometry()
var jump = function(){ if(line == lines -1){
if(edited.selectionStart == edited.value.length){ evt.preventDefault()
// needed to remember the position... that.focus('edited', 'next') }
edited.selectionStart = c
edited.selectionEnd = c
that.focus('edited', 1) } }
this.carot_jump_edge_then_block ?
jump()
: setTimeout(jump, 0)
} else { } else {
evt.preventDefault() evt.preventDefault()
this.focus('focused', 1) } }, this.focus('focused', 1) } },
@ -989,9 +1058,6 @@ var Outline = {
: focused.setAttribute('selected', '') }, : focused.setAttribute('selected', '') },
}, },
// XXX might be a good idea to defer specific actions to event-like
// handlers...
// e.g. clicking left if block -> .blockleft(..) ... etc.
setup: function(dom){ setup: function(dom){
var that = this var that = this
this.dom = dom this.dom = dom
@ -1020,8 +1086,7 @@ var Outline = {
return } return }
// expand/collapse // expand/collapse
if(elem.classList.contains('view') if(elem.classList.contains('view')){
&& elem.parentElement.getAttribute('tabindex')){
// click: left of elem (outside) // click: left of elem (outside)
if(evt.offsetX < 0){ if(evt.offsetX < 0){
// XXX item menu? // XXX item menu?
@ -1038,7 +1103,7 @@ var Outline = {
// edit of focus... // edit of focus...
// NOTE: this is usefull if element text is hidden but the // NOTE: this is usefull if element text is hidden but the
// frame is still visible... // frame is still visible...
if(elem.getAttribute('tabindex')){ if(elem.classList.contains('block')){
elem.querySelector('.code').focus() } elem.querySelector('.code').focus() }
that.runPlugins('__click__', evt, that, elem) }) that.runPlugins('__click__', evt, that, elem) })
@ -1046,12 +1111,10 @@ var Outline = {
outline.addEventListener('keydown', outline.addEventListener('keydown',
function(evt){ function(evt){
var elem = evt.target var elem = evt.target
// code editing... if(that.runPlugins('__keydown__', evt, that, evt.target) !== true){
if(elem.nodeName == 'CODE'
&& elem.getAttribute('contenteditable') == 'true'){
return } return }
// update element state... // update element state...
if(elem.nodeName == 'TEXTAREA'){ if(elem.classList.contains('code')){
setTimeout(function(){ setTimeout(function(){
that.update(elem.parentElement) that.update(elem.parentElement)
elem.updateSize() }, 0) } elem.updateSize() }, 0) }
@ -1061,30 +1124,14 @@ var Outline = {
// update code block... // update code block...
outline.addEventListener('keyup', outline.addEventListener('keyup',
function(evt){ function(evt){
var elem = evt.target that.runPlugins('__keyup__', evt, that, 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){
var elem = evt.target var elem = evt.target
// ignore children container...
if(elem.classList.contains('children')){ if(elem.classList.contains('children')){
return } return }
@ -1103,15 +1150,18 @@ var Outline = {
behavior: 'smooth', behavior: 'smooth',
}) })
//*/ //*/
})
// XXX do we need this???
that.runPlugins('__focusin__', evt, that, elem) })
outline.addEventListener('focusout', outline.addEventListener('focusout',
function(evt){ function(evt){
var node = evt.target var elem = evt.target
if(node.nodeName == 'TEXTAREA' if(elem.classList.contains('code')){
&& node?.nextElementSibling?.nodeName == 'SPAN'){ var block = elem.parentElement
var block = node.parentElement that.update(block, { text: elem.value })
that.update(block, { text: node.value }) that.runPlugins('__editedcode__', evt, that, elem) }
that.runPlugins('__changed__', evt, that, node) } })
that.runPlugins('__focusout__', evt, that, elem) })
// update .code... // update .code...
var update_code_timeout var update_code_timeout
outline.addEventListener('change', outline.addEventListener('change',
@ -1121,7 +1171,8 @@ var Outline = {
update_code_timeout = setTimeout( update_code_timeout = setTimeout(
function(){ function(){
update_code_timeout = undefined update_code_timeout = undefined
that.sync() }, that.sync()
that.runPlugins('__change__', evt, that) },
that.code_update_interval || 5000) }) that.code_update_interval || 5000) })
// toolbar... // toolbar...

View File

@ -14,6 +14,59 @@ HTMLTextAreaElement.prototype.autoUpdateSize = function(){
function(evt){ function(evt){
that.updateSize() }) that.updateSize() })
return this } return this }
HTMLTextAreaElement.prototype.getTextGeometry = function(){
var offset = this.selectionStart
var text = this.value
// get the relevant styles...
var style = getComputedStyle(this)
var s = {}
for(var i=0; i < style.length; i++){
var k = style[i]
if(k.startsWith('font')
|| k.startsWith('line')
|| k.startsWith('white-space')){
s[k] = style[k] } }
var carret = document.createElement('span')
carret.innerText = '|'
carret.style.margin = '0px'
carret.style.padding = '0px'
var span = document.createElement('span')
span.innerText = text.slice(0, offset)
Object.assign(span.style, {
...s,
position: 'fixed',
display: 'block',
top: '-100%',
left: '-100%',
width: this.offsetWidth + 'px',
height: this.scrollHeight + 'px',
padding: style.padding,
outline: 'solid 1px red',
pointerEvents: 'none',
})
span.append(carret)
document.body.append(span)
var res = {
length: text.length,
lines: Math.floor(this.offsetHeight / carret.offsetHeight),
line: Math.floor(carret.offsetTop / carret.offsetHeight),
offset: offset,
offsetLeft: carret.offsetLeft,
offsetTop: carret.offsetTop,
}
span.remove()
return res }
// calculate number of lines in text area (both wrapped and actual lines) // calculate number of lines in text area (both wrapped and actual lines)
Object.defineProperty(HTMLTextAreaElement.prototype, 'heightLines', { Object.defineProperty(HTMLTextAreaElement.prototype, 'heightLines', {

View File

@ -44,14 +44,7 @@ var setup = function(){
- // Seems that I unintentionally implemented quite a chunk of the markdown spec ;) - // Seems that I unintentionally implemented quite a chunk of the markdown spec ;)
- -
- ## Bugs: - ## Bugs:
- BUG: ASAP: editor: `-` at start of line is interpreted as block marker... - BUG: last node seems to get trash tags added to it's end...
- need to either:
- quote the `-` in .text() -- _preferreed_
- split the lines starting with `-` into nested nodes (a-la .load())
- BUG? pressing down from a longer line will jump over a shorter line
- to reproduce\:
- here is the line to jump from, for example from here
an we'll not get here...
- -
- ## ToDo: - ## ToDo:
- ASAP: editor: backsapce/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
@ -92,7 +85,7 @@ var setup = function(){
- #### Click in this line and see where the cursor goes - #### Click in this line and see where the cursor goes
- _not sure how..._ - _not sure how..._
- Q: persistent empty first/last node (a button to create a new node)? - 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?? - Q: should list bullets be on the same level as nodes or offset??
collapsed:: true collapsed:: true
- A) justified to bullet: - A) justified to bullet:
* list item * list item
@ -122,14 +115,16 @@ var setup = function(){
- -
- ## Refactoring: - ## Refactoring:
- Plugin architecture - Plugin architecture
- Item parser (`.__code2html__(..)`) - DONE basic structure
- ~split out~ - plugin handler sequencing (see: `.setup(..)`)
- ~define~/doc api - plugin handler canceling
- ~define a way to extend/stack parsers~ - DONE Item parser (`.__code2html__(..)`)
- DONE split out
- DONE define a way to extend/stack parsers
- Format parser/generator - Format parser/generator
- split out - split out
- define api - define api
- experiment with clean markdown as format - experiment with clean _markdown_ as format
- CSS - CSS
- separate out theming - separate out theming
- separate out settings - separate out settings
@ -139,6 +134,7 @@ var setup = function(){
- Q: do we need `features.js` and/or `actions.js` - Q: do we need `features.js` and/or `actions.js`
- Q: do we need a concatenative API?? - Q: do we need a concatenative API??
- `<block>.get() -> <block>` - `<block>.get() -> <block>`
- Docs
- -
- ## TEST - ## TEST
- ### Formatting: - ### Formatting:
@ -235,7 +231,8 @@ var setup = function(){
- This is a line of text - This is a line of text
- This is a set - This is a set
text lines text lines
- Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text </pre> - Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text Lots of text
- </pre>
<!-- outline --> <!-- outline -->
<div class="outline"></div> <div class="outline"></div>
<!-- toolbar (optional) --> <!-- toolbar (optional) -->
@ -248,6 +245,7 @@ var setup = function(){
<button onclick="editor.toggleCollapse()?.focus()">&#709;&#708;</button> <button onclick="editor.toggleCollapse()?.focus()">&#709;&#708;</button>
<button onclick="editor.remove()">&times;</button> <button onclick="editor.remove()">&times;</button>
</div--> </div-->
<span class="__textarea"></span>
</div> </div>
<hr> <hr>