diff options
Diffstat (limited to 'debian/missing-sources/codejar.js')
-rw-r--r-- | debian/missing-sources/codejar.js | 428 |
1 files changed, 428 insertions, 0 deletions
diff --git a/debian/missing-sources/codejar.js b/debian/missing-sources/codejar.js new file mode 100644 index 0000000..7ea7d44 --- /dev/null +++ b/debian/missing-sources/codejar.js @@ -0,0 +1,428 @@ +type Options = { + tab: string + indentOn: RegExp +} + +type HistoryRecord = { + html: string + pos: Position +} + +type Position = { + start: number + end: number + dir?: "->" | "<-" +} + +export type CodeJar = ReturnType<typeof CodeJar> + +export function CodeJar(editor: HTMLElement, highlight: (e: HTMLElement) => void, opt: Partial<Options> = {}) { + const options: Options = { + tab: "\t", + indentOn: /{$/, + ...opt + } + let listeners: [string, any][] = [] + let history: HistoryRecord[] = [] + let at = -1 + let focus = false + let callback: (code: string) => void | undefined + let prev: string // code content prior keydown event + let isFirefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1 + + editor.setAttribute("contentEditable", isFirefox ? "true" : "plaintext-only") + editor.setAttribute("spellcheck", "false") + editor.style.outline = "none" + editor.style.overflowWrap = "break-word" + editor.style.overflowY = "auto" + editor.style.resize = "vertical" + editor.style.whiteSpace = "pre-wrap" + + highlight(editor) + + const debounceHighlight = debounce(() => { + const pos = save() + highlight(editor) + restore(pos) + }, 30) + + let recording = false + const shouldRecord = (event: KeyboardEvent): boolean => { + return !isUndo(event) && !isRedo(event) + && event.key !== "Meta" + && event.key !== "Control" + && event.key !== "Alt" + && !event.key.startsWith("Arrow") + } + const debounceRecordHistory = debounce((event: KeyboardEvent) => { + if (shouldRecord(event)) { + recordHistory() + recording = false + } + }, 300) + + const on = <K extends keyof HTMLElementEventMap>(type: K, fn: (event: HTMLElementEventMap[K]) => void) => { + listeners.push([type, fn]) + editor.addEventListener(type, fn) + } + + on("keydown", event => { + if (event.defaultPrevented) return + + prev = toString() + handleNewLine(event) + handleTabCharacters(event) + handleSelfClosingCharacters(event) + handleUndoRedo(event) + if (shouldRecord(event) && !recording) { + recordHistory() + recording = true + } + }) + + on("keyup", event => { + if (event.defaultPrevented) return + if (event.isComposing) return + + if (prev !== toString()) debounceHighlight() + debounceRecordHistory(event) + if (callback) callback(toString()) + }) + + on("focus", _event => { + focus = true + }) + + on("blur", _event => { + focus = false + }) + + on("paste", event => { + recordHistory() + handlePaste(event) + recordHistory() + if (callback) callback(toString()) + }) + + function save(): Position { + const s = window.getSelection()! + const pos: Position = {start: 0, end: 0, dir: undefined} + + visit(editor, el => { + if (el === s.anchorNode && el === s.focusNode) { + pos.start += s.anchorOffset + pos.end += s.focusOffset + pos.dir = s.anchorOffset <= s.focusOffset ? "->" : "<-" + return "stop" + } + + if (el === s.anchorNode) { + pos.start += s.anchorOffset + if (!pos.dir) { + pos.dir = "->" + } else { + return "stop" + } + } else if (el === s.focusNode) { + pos.end += s.focusOffset + if (!pos.dir) { + pos.dir = "<-" + } else { + return "stop" + } + } + + if (el.nodeType === Node.TEXT_NODE) { + if (pos.dir != "->") pos.start += el.nodeValue!.length + if (pos.dir != "<-") pos.end += el.nodeValue!.length + } + }) + + return pos + } + + function restore(pos: Position) { + const s = window.getSelection()! + let startNode: Node | undefined, startOffset = 0 + let endNode: Node | undefined, endOffset = 0 + + if (!pos.dir) pos.dir = "->" + if (pos.start < 0) pos.start = 0 + if (pos.end < 0) pos.end = 0 + + // Flip start and end if the direction reversed + if (pos.dir == "<-") { + const {start, end} = pos + pos.start = end + pos.end = start + } + + let current = 0 + + visit(editor, el => { + if (el.nodeType !== Node.TEXT_NODE) return + + const len = (el.nodeValue || "").length + if (current + len >= pos.start) { + if (!startNode) { + startNode = el + startOffset = pos.start - current + } + if (current + len >= pos.end) { + endNode = el + endOffset = pos.end - current + return "stop" + } + } + current += len + }) + + // If everything deleted place cursor at editor + if (!startNode) startNode = editor + if (!endNode) endNode = editor + + // Flip back the selection + if (pos.dir == "<-") { + [startNode, startOffset, endNode, endOffset] = [endNode, endOffset, startNode, startOffset] + } + + s.setBaseAndExtent(startNode, startOffset, endNode, endOffset) + } + + function beforeCursor() { + const s = window.getSelection()! + const r0 = s.getRangeAt(0) + const r = document.createRange() + r.selectNodeContents(editor) + r.setEnd(r0.startContainer, r0.startOffset) + return r.toString() + } + + function afterCursor() { + const s = window.getSelection()! + const r0 = s.getRangeAt(0) + const r = document.createRange() + r.selectNodeContents(editor) + r.setStart(r0.endContainer, r0.endOffset) + return r.toString() + } + + function handleNewLine(event: KeyboardEvent) { + if (event.key === "Enter") { + const before = beforeCursor() + const after = afterCursor() + + let [padding] = findPadding(before) + let newLinePadding = padding + + // If last symbol is "{" ident new line + // Allow user defines indent rule + if (options.indentOn.test(before)) { + newLinePadding += options.tab + } + + if (isFirefox) { + preventDefault(event) + insert("\n" + newLinePadding) + } else { + // Normal browsers + if (newLinePadding.length > 0) { + preventDefault(event) + insert("\n" + newLinePadding) + } + } + + // Place adjacent "}" on next line + if (newLinePadding !== padding && after[0] === "}") { + const pos = save() + insert("\n" + padding) + restore(pos) + } + } + } + + function handleSelfClosingCharacters(event: KeyboardEvent) { + const open = `([{'"` + const close = `)]}'"` + const codeAfter = afterCursor() + if (close.includes(event.key) && codeAfter.substr(0, 1) === event.key) { + const pos = save() + preventDefault(event) + pos.start = ++pos.end + restore(pos) + } else if (open.includes(event.key)) { + const pos = save() + preventDefault(event) + const text = event.key + close[open.indexOf(event.key)] + insert(text) + pos.start = ++pos.end + restore(pos) + } + } + + function handleTabCharacters(event: KeyboardEvent) { + if (event.key === "Tab") { + preventDefault(event) + if (event.shiftKey) { + const before = beforeCursor() + let [padding, start,] = findPadding(before) + if (padding.length > 0) { + const pos = save() + // Remove full length tab or just remaining padding + const len = Math.min(options.tab.length, padding.length) + restore({start, end: start + len}) + document.execCommand("delete") + pos.start -= len + pos.end -= len + restore(pos) + } + } else { + insert(options.tab) + } + } + } + + function handleUndoRedo(event: KeyboardEvent) { + if (isUndo(event)) { + preventDefault(event) + at-- + const record = history[at] + if (record) { + editor.innerHTML = record.html + restore(record.pos) + } + if (at < 0) at = 0 + } + if (isRedo(event)) { + preventDefault(event) + at++ + const record = history[at] + if (record) { + editor.innerHTML = record.html + restore(record.pos) + } + if (at >= history.length) at-- + } + } + + function recordHistory() { + if (!focus) return + + const html = editor.innerHTML + const pos = save() + + const lastRecord = history[at] + if (lastRecord) { + if (lastRecord.html === html + && lastRecord.pos.start === pos.start + && lastRecord.pos.end === pos.end) return + } + + at++ + history[at] = {html, pos} + history.splice(at + 1) + + const maxHistory = 300 + if (at > maxHistory) { + at = maxHistory + history.splice(0, 1) + } + } + + function handlePaste(event: ClipboardEvent) { + preventDefault(event) + const text = ((event as any).originalEvent || event).clipboardData.getData("text/plain") + const pos = save() + insert(text) + highlight(editor) + restore({start: pos.end + text.length, end: pos.end + text.length}) + } + + + function visit(editor: HTMLElement, visitor: (el: Node) => "stop" | undefined) { + const queue: Node[] = [] + + if (editor.firstChild) queue.push(editor.firstChild) + + let el = queue.pop() + + while (el) { + if (visitor(el) === "stop") + break + + if (el.nextSibling) queue.push(el.nextSibling) + if (el.firstChild) queue.push(el.firstChild) + + el = queue.pop() + } + } + + function isCtrl(event: KeyboardEvent) { + return event.metaKey || event.ctrlKey + } + + function isUndo(event: KeyboardEvent) { + return isCtrl(event) && !event.shiftKey && event.code === "KeyZ" + } + + function isRedo(event: KeyboardEvent) { + return isCtrl(event) && event.shiftKey && event.code === "KeyZ" + } + + function insert(text: string) { + text = text + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, """) + .replace(/'/g, "'") + document.execCommand("insertHTML", false, text) + } + + function debounce(cb: any, wait: number) { + let timeout = 0 + return (...args: any) => { + clearTimeout(timeout) + timeout = window.setTimeout(() => cb(...args), wait) + } + } + + function findPadding(text: string): [string, number, number] { + // Find beginning of previous line. + let i = text.length - 1 + while (i >= 0 && text[i] !== "\n") i-- + i++ + // Find padding of the line. + let j = i + while (j < text.length && /[ \t]/.test(text[j])) j++ + return [text.substring(i, j) || "", i, j] + } + + function toString() { + return editor.textContent || "" + } + + function preventDefault(event: Event) { + event.preventDefault() + } + + return { + updateOptions(options: Partial<Options>) { + options = {...options, ...options} + }, + updateCode(code: string) { + editor.textContent = code + highlight(editor) + }, + onUpdate(cb: (code: string) => void) { + callback = cb + }, + toString, + destroy() { + for (let [type, fn] of listeners) { + editor.removeEventListener(type, fn) + } + }, + } +} + |