mirror of
				https://github.com/flynx/pWiki.git
				synced 2025-10-31 11:00:08 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			397 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			HTML
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			397 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			HTML
		
	
	
		
			Executable File
		
	
	
	
	
| <html>
 | |
| <head>
 | |
| <style>
 | |
| 
 | |
| :root {
 | |
| 	font-family: sans-serif;
 | |
| 	font-size: 5mm;
 | |
| }
 | |
| 
 | |
| .editor [tabindex] {
 | |
| 	position: relative;
 | |
| }
 | |
| .editor div [tabindex] {
 | |
| 	margin-left: 2em;
 | |
| }
 | |
| .editor [tabindex]>span,
 | |
| .editor [tabindex]>textarea {
 | |
| 	--padding: 0.2em;
 | |
| 
 | |
| 	display: block;
 | |
| 	width: 100%;
 | |
| 	padding: var(--padding);
 | |
| 	margin: 0;
 | |
| 
 | |
| 	font-family: sans-serif;
 | |
| 	font-size: 5mm;
 | |
| 	white-space: pre;
 | |
| 
 | |
| 	outline: none;
 | |
| 	border: none;
 | |
| }
 | |
| .editor [tabindex]>textarea {
 | |
| 	height: calc(2 * var(--padding) + 1em);
 | |
| 	overflow: hidden;
 | |
| 	resize: none;
 | |
| }
 | |
| 
 | |
| /* show/hide node's view/code... */
 | |
| .editor [tabindex]>span+textarea:not(:focus),
 | |
| /* XXX not sure how to do this without :has(..)... */
 | |
| .editor [tabindex]:has(>span+textarea:focus)>span:has(+textarea),
 | |
| .editor [tabindex]:focus>span+textarea {
 | |
| 	position: absolute;
 | |
| 	opacity: 0;
 | |
| 	top: 0;
 | |
| }
 | |
| 
 | |
| .editor div[collapsed] {
 | |
| 	border-bottom: solid 1px silver;
 | |
| }
 | |
| .editor div[collapsed] div {
 | |
| 	display: none;
 | |
| }
 | |
| 
 | |
| .editor div:focus {
 | |
| 	/*outline: solid 0.2em silver;*/
 | |
| 	outline: none;
 | |
| }
 | |
| .editor div:focus>span,
 | |
| .editor div:focus>textarea {
 | |
| 	background: silver;
 | |
| }
 | |
| 
 | |
| </style>
 | |
| <script>
 | |
| var updateTextareaSize = function(elem){
 | |
| 	elem.style.height = ''
 | |
| 	elem.style.height = elem.scrollHeight + 'px'
 | |
| 	return elem }
 | |
| 
 | |
| 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')]){
 | |
| 			updateTextareaSize(elem) }
 | |
| 	}
 | |
| 	return node }
 | |
| 
 | |
| // XXX add reference node...
 | |
| var createBlock = function(place=none){
 | |
| 	var block = document.createElement('div')
 | |
| 	block.setAttribute('tabindex', '0')
 | |
| 	block.innerHTML = `<span></span><textarea></textarea>`
 | |
| 	var cur = getFocused()
 | |
| 		|| getEditable()?.parentElement
 | |
| 	place && cur
 | |
| 		&& cur[place](block)
 | |
| 	return block }
 | |
| 
 | |
| 
 | |
| // 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
 | |
| }
 | |
| 
 | |
| var LEFT_COLLAPSE = false
 | |
| var RIGHT_EXPAND = true
 | |
| 
 | |
| // XXX add scrollIntoView(..) to nav...
 | |
| var keyboard = {
 | |
| 	// vertical navigation...
 | |
| 	ArrowUp: function(evt){
 | |
| 		var action = getFocused
 | |
| 		var edited = document.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 = document.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(document.querySelector('.editor textarea:focus')){
 | |
| 			// XXX if at end of element move to next...
 | |
| 			return }
 | |
| 		if(LEFT_COLLAPSE){
 | |
| 				toggleCollapse(true)
 | |
| 				getFocused('parent')?.focus()
 | |
| 		} else { 
 | |
| 			evt.shiftKey ?
 | |
| 				toggleCollapse(true)
 | |
| 				: getFocused('parent')?.focus() } },
 | |
| 	ArrowRight: function(evt){
 | |
| 		if(document.querySelector('.editor textarea:focus')){
 | |
| 			// XXX if at end of element move to next...
 | |
| 			return }
 | |
| 		if(RIGHT_EXPAND){
 | |
| 			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()
 | |
| 			createBlock('before')?.querySelector('textarea')?.focus() } },
 | |
| 	o: function(evt){
 | |
| 		if(evt.target.nodeName != 'TEXTAREA'){
 | |
| 			evt.preventDefault()
 | |
| 			createBlock('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' ?
 | |
| 			createBlock('after')?.querySelector('textarea')?.focus()
 | |
| 			: getFocused()?.querySelector('textarea')?.focus() },
 | |
| 	Escape: function(evt){
 | |
| 		document.querySelector('textarea:focus')?.parentElement?.focus() },
 | |
| 	Delete: function(evt){
 | |
| 		if(evt.target.isContentEditable){
 | |
| 			return }
 | |
| 		var next = getFocused(1)
 | |
| 		getFocused()?.remove() 
 | |
| 		next?.focus() },
 | |
| }
 | |
| document.addEventListener('keydown', 
 | |
| 	function(evt){
 | |
| 		evt.key in keyboard 
 | |
| 			&& keyboard[evt.key](evt) })
 | |
| 
 | |
| document.addEventListener('input', 
 | |
| 	function(evt){
 | |
| 		updateTextareaSize(evt.target) })
 | |
| 
 | |
| // XXX add support for markup handlers...
 | |
| document.addEventListener('focusin', 
 | |
| 	function(evt){
 | |
| 		var node = evt.target
 | |
| 		if(node.nodeName == 'TEXTAREA' 
 | |
| 				&& node?.previousElementSibling?.nodeName == 'SPAN'){
 | |
| 			node.value = node.previousElementSibling.innerHTML 
 | |
| 			updateTextareaSize(node) } })
 | |
| document.addEventListener('focusout', 
 | |
| 	function(evt){
 | |
| 		var node = evt.target
 | |
| 		if(node.nodeName == 'TEXTAREA' 
 | |
| 				&& node?.previousElementSibling?.nodeName == 'SPAN'){
 | |
| 			node.previousElementSibling.innerHTML = node.value } })
 | |
| 
 | |
| var setup = function(){
 | |
| 	for(var elem of [...document.querySelectorAll('.editor textarea')]){
 | |
| 		updateTextareaSize(elem) } }
 | |
| 
 | |
| </script>
 | |
| </head>
 | |
| <body onload="setup()">
 | |
| <pre>
 | |
| TODO:
 | |
| - <s>navigation</s>
 | |
| - <s>expand/collapse subtree</s>
 | |
| - <s>shift subtree up/down</s>
 | |
| - <s>create node</s>
 | |
| - <s>edit node</s>
 | |
| - undo delete node
 | |
| - copy/paste nodes/trees
 | |
| - shifting nodes up/down
 | |
| - multiple node selection
 | |
| - mouse controls
 | |
| - touch controls
 | |
| - serialize/deserialize
 | |
| - add optional styling to nodes
 | |
| 
 | |
| Controls:
 | |
| 	up         - focus node above
 | |
| 	down       - focus node below
 | |
| 	left       - focus parent node
 | |
| 	right      - focus first child node
 | |
| 	tab        - indent node
 | |
| 	s-tab      - deindent node
 | |
| 	s-left     - collapse node
 | |
| 	s-right    - expand node
 | |
| 	enter      - normal mode: edit node
 | |
| 	           - edit mode: create node below
 | |
| 	esc        - exit edit mode 
 | |
| </pre>
 | |
| 
 | |
| <hr>
 | |
| 
 | |
| <div class="editor">
 | |
| 	<div tabindex=0>
 | |
| 		<span><i>root</i></span><textarea></textarea>
 | |
| 		<div tabindex=0 collapsed>
 | |
| 			<span>A</span><textarea></textarea>
 | |
| 			<div tabindex=0><span>a</span><textarea></textarea>
 | |
| 			</div>
 | |
| 			<div tabindex=0><span>b</span><textarea></textarea>
 | |
| 			</div>
 | |
| 			<div tabindex=0><span>c</span><textarea></textarea>
 | |
| 			</div>
 | |
| 		</div>
 | |
| 		<div tabindex=0><span>B</span><textarea></textarea>
 | |
| 			<div tabindex=0><span>d</span><textarea></textarea>
 | |
| 			</div>
 | |
| 			<div tabindex=0><span>e</span><textarea></textarea>
 | |
| 			</div>
 | |
| 		</div>
 | |
| 		<div tabindex=0><span>C</span><textarea></textarea>
 | |
| 			<div tabindex=0><span>This is a line of text</span><textarea></textarea>
 | |
| 			</div>
 | |
| 			<div tabindex=0><span>This is a set
 | |
| text lines</span><textarea></textarea>
 | |
| 			</div>
 | |
| 		</div>
 | |
| 	</div>
 | |
| </div>
 | |
| 
 | |
| </body>
 | |
| </html>
 | |
| <!-- vim:set ts=4 sw=4 : -->
 |