const globalWindow = window; function CodeJar(editor, highlight, opt = {}) { const options = Object.assign({ tab: '\t', indentOn: /{$/, spellcheck: false, catchTab: true, preserveIdent: true, addClosing: true, history: true, window: globalWindow }, opt); const window = options.window; const document = window.document; let listeners = []; let history = []; let at = -1; let focus = false; let callback; let prev; // code content prior keydown event let isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; editor.setAttribute('contentEditable', isFirefox ? 'true' : 'plaintext-only'); editor.setAttribute('spellcheck', options.spellcheck ? 'true' : '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, pos); restore(pos); }, 30); let recording = false; const shouldRecord = (event) => { return !isUndo(event) && !isRedo(event) && event.key !== 'Meta' && event.key !== 'Control' && event.key !== 'Alt' && !event.key.startsWith('Arrow'); }; const debounceRecordHistory = debounce((event) => { if (shouldRecord(event)) { recordHistory(); recording = false; } }, 300); const on = (type, fn) => { listeners.push([type, fn]); editor.addEventListener(type, fn); }; on('keydown', event => { if (event.defaultPrevented) return; prev = toString(); if (options.preserveIdent) handleNewLine(event); else firefoxNewLineFix(event); if (options.catchTab) handleTabCharacters(event); if (options.addClosing) handleSelfClosingCharacters(event); if (options.history) { 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() { const s = getSelection(); const pos = { 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) { const s = getSelection(); let startNode, startOffset = 0; let endNode, 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 = 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 = 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) { 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; } // Preserve padding if (newLinePadding.length > 0) { preventDefault(event); event.stopPropagation(); insert('\n' + newLinePadding); } else { firefoxNewLineFix(event); } // Place adjacent "}" on next line if (newLinePadding !== padding && after[0] === '}') { const pos = save(); insert('\n' + padding); restore(pos); } } } function firefoxNewLineFix(event) { // Firefox does not support plaintext-only mode // and puts