summaryrefslogtreecommitdiffstats
path: root/debian/missing-sources/codejar.js
diff options
context:
space:
mode:
Diffstat (limited to 'debian/missing-sources/codejar.js')
-rw-r--r--debian/missing-sources/codejar.js428
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, "&amp;")
+ .replace(/</g, "&lt;")
+ .replace(/>/g, "&gt;")
+ .replace(/"/g, "&quot;")
+ .replace(/'/g, "&#039;")
+ 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)
+ }
+ },
+ }
+}
+