diff --git a/txt.js/txt.js b/txt.js/txt.js index 2177fb3..42f977c 100644 --- a/txt.js/txt.js +++ b/txt.js/txt.js @@ -1,60 +1,470 @@ -this.txtjs = {}; +let txtjs = {} txtjs.open = function(url) { - Ox.load(function() { - Ox.get(url, function(text) { - var $body = Ox.$('body') - .css({ - backgroundColor: 'rgb(255, 255, 255)', - overflowX: 'hidden' - }), - $text = Ox.$('
') - .css({ - padding: '10% 20% 10% 10%', - fontFamily: 'Georgia, Palatino, DejaVu Serif, Book Antiqua, Palatino Linotype, Times New Roman, serif', - fontSize: '20px', - lineHeight: '30px' - }) - .appendTo($body), - $scroll = Ox.$('
') - .css({ - position: 'fixed', - right: '24px', - top: '16px', - width: '7%', - bottom: '16px', - overflow: 'hidden' - }) - .appendTo($body), - $scrollText = Ox.$('
') - .css({ - fontSize: '2px', - lineHeight: '3px', - cursor: 'pointer', - WebkitUserSelect: 'none' - }) - .on({ - mousedown: function(e) { - var offset = 'offsetY' in e ? e.offsetY : e.layerY; - document.documentElement.scrollTop = offset / factor + margin - 16; - } - }) - .appendTo($scroll), - factor, margin; - text = Ox.encodeHTMLEntities(text) - .replace(/\r\n/g, '\n') - .replace(/[\r\n]/g, '
'); - $text.html(text); - $scrollText.html(text); - window.onresize = function() { - margin = $text.width() * 0.1; - factor = $scrollText[0].clientHeight / $text[0].clientHeight; - }; - window.onscroll = function() { - $scroll[0].scrollTop = (window.pageYOffset - margin + 16) * factor; - }; - window.onresize(); - }); - }); -}; + fetch(url).then(function(response) { + return response.text() + }).then(txtjs.renderText) +} +txtjs.mark = function(notes) { + notes.forEach(function(note) { + if (!txtjs.notes.includes(note)) { + txtjs.notes.push(note) + } + txtjs.renderNote(note) + }) +} + +txtjs.notes = [] + +;(function() { + let listener = addEventListener || attachEvent + let event = addEventListener ? 'message' : 'onmessage' + listener(event, function(e) { + let message = JSON.parse(e.message || e.data) + txtjs.onMessage(message.action, message.data) + }) +}()) + +txtjs.addNoteFromSelection = function() { + let note = txtjs.getNoteFromSelection() + if (!note || txtjs.noteExists(note)) { + return + } + txtjs.renderNote(note) + txtjs.notes.push(note) + txtjs.selectNote(note.id) + getSelection().removeAllRanges() + txtjs.postMessage('addNote', note) +} + +txtjs.cancelEdit = function() { + let editing = document.querySelector('g.editing') + editing && editing.classList.remove('editing') +} + +txtjs.createSVGElement = function(name) { + return document.createElementNS('http://www.w3.org/2000/svg', name) +} + +txtjs.editNote = function() { + let editing = document.querySelector('g.editing') + let note = txtjs.getNoteFromSelection() + if (!editing || !note) { + return + } + let id = txtjs.getNoteId(editing) + note = Object.assign(Ox.getObjectById(txtjs.notes, id), { + position: note.position, + text: note.text + }) + document.querySelector('svg').removeChild(editing) + txtjs.renderNote(note) + txtjs.selectNote(note.id) + txtjs.postMessage('editNote', { + id: id, + position: note.position, + text: note.text + }) +} + +txtjs.getNewId = function() { + let ids = txtjs.notes.map(function(note) { + return note.id + }) + let i = 1 + while (ids.includes(Ox.encodeBase26(i))) { + i++ + } + return Ox.encodeBase26(i) +} + +txtjs.getNoteId = function(element) { + let classNames = Array.from(element.classList).filter(function(className) { + return className.startsWith('note-') + }) + if (classNames.length == 0) { + return + } + return classNames[0].substr(5) +} + +txtjs.getNoteFromSelection = function() { + let selection = getSelection() + try { + var range = selection.getRangeAt(0) + } catch(e) { + return + } + if (range.collapsed) { + return + } + let container = range.commonAncestorContainer + if (container.id != 'txt') { + while (container != document.body) { + container = container.parentElement + if (container.id == 'txt') { + break + } + } + } + if (container.id != 'txt') { + return + } + let position = txtjs.getPosition(range) + let pos = position.split(':').map(function(v) { + return parseInt(v) + }) + let note = { + id: txtjs.getNewId(), + position: position, + text: txtjs.text.substr(pos[0], pos[1] - pos[0]), + editable: true + } + if (txtjs.noteExists(note)) { + return + } + return note +} + +txtjs.getPosition = function(range) { + let container = document.querySelector('#txt') + let nodes = Array.from(container.childNodes) + let startNodeIndex = nodes.indexOf(range.startContainer) + let endNodeIndex = nodes.indexOf(range.endContainer) + let index = 0 + let start = 0 + let end = 0 + for (let i = 0; i <= endNodeIndex; i++) { + if (i == startNodeIndex) { + start = index + range.startOffset + } + if (i == endNodeIndex) { + end = index + range.endOffset + } + if (nodes[i].nodeType == 1) { //
+ index++ + } else { + index += nodes[i].textContent.length + } + } + return start + ':' + end +} + +txtjs.getRange = function(id, start, end) { + let startContainer, startOffset, endContainer, endOffset + let container = document.querySelector('#' + id) + let nodes = Array.from(container.childNodes) + let index = 0 + for (let i = 0; i < nodes.length; i++) { + if (start < index + nodes[i].textContent.length && startOffset === void 0) { + startContainer = nodes[i] + startOffset = start - index + } + if (end <= index + nodes[i].textContent.length) { + endContainer = nodes[i] + endOffset = end - index + break + } + if (nodes[i].nodeType == 1) { //
+ index++ + } else { + index += nodes[i].textContent.length + } + } + let range = document.createRange() + range.setStart(startContainer, startOffset) + range.setEnd(endContainer, endOffset) + return range +} + +txtjs.getSelectedNote = function() { + let elements = Array.from(document.querySelectorAll('g.selected')) + if (elements.length == 0) { + return + } + let id = txtjs.getNoteId(elements[0]) + return Object.assign(Ox.getObjectById(txtjs.notes, id), { + elements: elements + }) +} + +txtjs.moveNote = function() { + let selected = txtjs.getSelectedNote() + if (!selected || !selected.elements[0].classList.contains('editable')) { + return + } + selected.elements.forEach(function(element) { + element.classList.add('editing') + }) +} + +txtjs.noteExists = function(note) { + return txtjs.notes.some(function(note_) { + return note_.position == note.position + }) +} + +txtjs.onMessage = function(action, data) { + +} + +txtjs.postMessage = function(action, data) { + console.log('postMessage', action, data) + parent.postMessage(JSON.stringify({action: action, data: data}), '*') +} + +txtjs.removeNote = function() { + let selected = txtjs.getSelectedNote() + if (!selected) { + return + } + let id = txtjs.getNoteId(selected.elements[0]) + selected.elements.forEach(function(element) { + element.parentElement.removeChild(element) + }) + let index = txtjs.notes.map(function(note) { + return note.id + }).indexOf(id) + txtjs.notes.splice(index, 1) + txtjs.postMessage('removeNote', { + id: id + }) +} + +txtjs.renderNote = function(note) { + let pos = note.position.split(':').map(function(v) { + return parseInt(v) + }) + let ids = ['txt', 'txt-scroll'] + ids.forEach(function(id) { + let range = txtjs.getRange(id, pos[0], pos[1]) + let rects = Array.from(range.getClientRects()) + let size = rects.reduce(function(width, rect) { + return width + rect.width + }, 0) + let maxHeight = 8192 + let firstIndex = Math.floor((rects[0].top + window.pageYOffset) / maxHeight) + let lastIndex = Math.floor((rects[rects.length - 1].top + window.pageYOffset + rects[rects.length - 1].height) / maxHeight) + for (let index = firstIndex; index <= lastIndex; index++) { + let g = txtjs.createSVGElement('g') + g.classList.add('note-' + note.id) + g.classList.add('selectable') + if (note.editable) { + g.classList.add('editable') + } + g.setAttribute('data-size', size) + g.setAttribute('pointer-events', id == 'txt' ? 'all' : 'none') + rects.forEach(function(rect) { + let element = txtjs.createSVGElement('rect') + let x = id == 'txt' ? rect.left + : rect.left - document.querySelector('#scroll').getBoundingClientRect().left + 8 + let y = id == 'txt' ? rect.top + window.pageYOffset - index * maxHeight + : rect.top + document.querySelector('#scroll').scrollTop - 16 - index * maxHeight + element.setAttribute('x', x) + element.setAttribute('y', y) + element.setAttribute('width', rect.width) + element.setAttribute('height', rect.height) + g.appendChild(element) + }) + let svg = document.querySelector('svg#svg-' + id + '-' + index) + for (let i = 0; i < svg.children.length; i++) { + let childSize = parseInt(svg.children[i].getAttribute('data-size')) + if (size > childSize) { + svg.insertBefore(g, svg.children[i]) + break + } + } + if (!g.parentElement) { + svg.appendChild(g) + } + } + }) +} + +txtjs.renderSVG = function(id, index, width, height) { + function mousedown(e) { + svg.addEventListener('mouseup', mouseup) + timeout = setTimeout(function() { + svg.removeEventListener('mouseup', mouseup) + e.target.classList.remove('selectable') + e.target.setAttribute('pointer-events', 'none') + document.addEventListener('mouseup', function() { + e.target.classList.add('selectable') + e.target.setAttribute('pointer-events', 'all') + }) + }, 250) + } + function mouseup(e) { + clearTimeout(timeout) + txtjs.selectNote(txtjs.getNoteId(e.target.parentElement)) + } + let timeout + let svg = txtjs.createSVGElement('svg') + svg.setAttribute('id', 'svg-' + id + '-' + index) + svg.setAttribute('pointer-events', 'none') + if (id == 'txt') { + svg.style.left = 0 + } else { + svg.style.right = '8px' + } + svg.style.top = index * 8192 + 'px' + svg.style.width = width + 'px' + svg.style.height = height + 'px' + if (id == 'txt') { + svg.addEventListener('mousedown', mousedown) + } + let parentElement = id == 'txt' ? document.body : document.querySelector('#scroll') + parentElement.appendChild(svg) +} + +txtjs.renderSVGs = function() { + let maxHeight = 8192 + let ids = ['txt', 'txt-scroll'] + ids.forEach(function(id) { + let rect = document.querySelector('#' + id).getBoundingClientRect() + let lastHeight = rect.height % maxHeight + let n = Math.ceil(rect.height / maxHeight) + for (let i = 0; i < n; i++) { + txtjs.renderSVG(id, i, rect.width, i < n - 1 ? maxHeight : lastHeight) + } + }) +} + +txtjs.renderText = function(text) { + txtjs.text = text + html = text.replace(//g, '>') + html = html.replace().replace(/\r\n/g, '\n').replace(/[\r\n]/g, '
') + txtjs.html = Ox.encodeHTMLEntities(text).replace(/\r\n/g, '\n').replace(/[\r\n]/g, '
') + window.addEventListener('resize', onResize) + window.addEventListener('resizeend', onResizeend) + window.addEventListener('scroll', onScroll) + document.addEventListener('keydown', function(e) { + console.log(e.keyCode) + if (e.keyCode == 13) { // ENTER + txtjs.editNote() + } else if (e.keyCode == 27) { // ESCAPE + txtjs.selectNote(null) + txtjs.cancelEdit() + } else if (e.keyCode == 37) { // LEFT + txtjs.selectNextNote(-1) + } else if (e.keyCode == 39) { // RIGHT + txtjs.selectNextNote(1) + } else if (e.keyCode == 46) { // DELETE + txtjs.removeNote() + } else if (e.keyCode == 77) { // M + txtjs.moveNote() + } else if (e.keyCode == 78) { // N + txtjs.addNoteFromSelection() + } + }) + let style = document.createElement('style') + style.innerText = [ + 'svg { mix-blend-mode: multiply; position: absolute }', + 'g { fill: rgb(255, 255, 192); fill-opacity: 0.5 }', + 'g.selectable { cursor: pointer }', + 'g.editable { fill: rgb(255, 255, 0) }', + 'g.selected { fill: rgb(224, 240, 255) }', + 'g.editable.selected { fill: rgb(128, 192, 255) }', + 'g.editable.editing { fill: rgb(128, 255, 128) }', + '::selection { background: rgb(192, 192, 192) }' + ].join('\n') + document.head.appendChild(style) + document.body.style.backgroundColor = 'rgb(255, 255, 255)' + document.body.style.margin = 0 + document.body.style.overflowX = 'hidden' + let textElement = document.createElement('div') + textElement.id = 'txt' + textElement.style.fontFamily = 'Georgia, Palatino, DejaVu Serif, Book Antiqua, Palatino Linotype, Times New Roman, serif', + textElement.style.fontSize = '20px' + textElement.style.lineHeight = '30px' + textElement.style.padding = '10% 20% 10% 10%' + textElement.innerHTML = txtjs.html + textElement.addEventListener('mousedown', function() { + txtjs.selectNote(null) + }) + document.body.appendChild(textElement) + let scrollElement = document.createElement('div') + scrollElement.id = 'scroll' + scrollElement.style.bottom = '16px' + scrollElement.style.overflow = 'hidden' + scrollElement.style.position = 'fixed' + scrollElement.style.right = '24px' + scrollElement.style.width = '7%' + scrollElement.style.top = '16px' + document.body.appendChild(scrollElement) + let scrollTextElement = document.createElement('div') + scrollTextElement.id = 'txt-scroll' + scrollTextElement.style.cursor = 'pointer' + scrollTextElement.style.fontFamily = 'Georgia, Palatino, DejaVu Serif, Book Antiqua, Palatino Linotype, Times New Roman, serif', + scrollTextElement.style.fontSize = '2px' + scrollTextElement.style.lineHeight = '3px' + scrollTextElement.style.MozUserSelect = 'none' + scrollTextElement.style.WebkitUserSelect = 'none' + scrollTextElement.innerHTML = txtjs.html + scrollTextElement.addEventListener('mousedown', function(e) { + let offset = 'offsetY' in e ? e.offsetY : e.layerY + document.documentElement.scrollTop = offset / factor + margin - 16 + }) + scrollElement.appendChild(scrollTextElement) + txtjs.renderSVGs() + let factor, margin + onResize() + function onResize() { + factor = scrollTextElement.clientHeight / textElement.clientHeight + margin = textElement.offsetWidth * 0.1 + setTimeout(function() { + Array.from(document.querySelectorAll('svg')).forEach(function(svg) { + svg.parentElement.removeChild(svg) + }) + txtjs.renderSVGs() + txtjs.mark(txtjs.notes) + }) + } + function onResizeend() { + + } + function onScroll() { + scrollElement.scrollTop = (window.pageYOffset - margin + 16) * factor + } + txtjs.mark([{id: 'A', position: '417:468', editable: false}]) +} + +txtjs.selectNextNote = function(direction) { + let selected = txtjs.getSelectedNote() + if (!selected) { + return + } + let id = txtjs.getNoteId(selected.elements[0]) + let ids = txtjs.notes.sort(function(a, b) { + return parseInt(a.position.split()[0]) - parseInt(b.position.split()[0]) + }).map(function(note) { + return note.id + }) + txtjs.selectNote(ids[Ox.mod(ids.indexOf(id) + direction, ids.length)]) +} + +txtjs.selectNote = function(id) { + let selected = txtjs.getSelectedNote() + if (selected) { + selected.elements.forEach(function(element) { + element.classList.remove('selected') + }) + } + if (id) { + let editing = Array.from(document.querySelectorAll('g.editing')) + if (editing.length && txtjs.getNoteId(editing) != id) { + editing.forEach(function(element) { + element.classList.remove('editing') + }) + } + let elements = Array.from(document.querySelectorAll('g.note-' + id)) + elements.forEach(function(element) { + element.classList.add('selected') + }) + // FIXME: SCROLL + // elements[0].scrollIntoView() + // window.scrollTo(0, elements[0].offsetTop) + } + txtjs.postMessage('selectNote', {id: id}) +}