| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423 |
- 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 <div><br></div> on Enter. Let's help.
- if (isFirefox && event.key === 'Enter') {
- preventDefault(event);
- event.stopPropagation();
- if (afterCursor() == '') {
- insert('\n ');
- const pos = save();
- pos.start = --pos.end;
- restore(pos);
- }
- else {
- insert('\n');
- }
- }
- }
- function handleSelfClosingCharacters(event) {
- const open = `([{'"`;
- const close = `)]}'"`;
- const codeAfter = afterCursor();
- const codeBefore = beforeCursor();
- const escapeCharacter = codeBefore.substr(codeBefore.length - 1) === '\\';
- const charAfter = codeAfter.substr(0, 1);
- if (close.includes(event.key) && !escapeCharacter && charAfter === event.key) {
- // We already have closing char next to cursor.
- // Move one char to right.
- const pos = save();
- preventDefault(event);
- pos.start = ++pos.end;
- restore(pos);
- }
- else if (open.includes(event.key)
- && !escapeCharacter
- && (`"'`.includes(event.key) || ['', ' ', '\n'].includes(charAfter))) {
- preventDefault(event);
- const pos = save();
- const wrapText = pos.start == pos.end ? '' : getSelection().toString();
- const text = event.key + wrapText + close[open.indexOf(event.key)];
- insert(text);
- pos.start++;
- pos.end++;
- restore(pos);
- }
- }
- function handleTabCharacters(event) {
- 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) {
- 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) {
- preventDefault(event);
- const text = (event.originalEvent || event)
- .clipboardData
- .getData('text/plain')
- .replace(/\r/g, '');
- const pos = save();
- insert(text);
- highlight(editor);
- restore({ start: pos.start + text.length, end: pos.start + text.length });
- }
- function visit(editor, visitor) {
- const queue = [];
- 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) {
- return event.metaKey || event.ctrlKey;
- }
- function isUndo(event) {
- return isCtrl(event) && !event.shiftKey && event.key === 'z';
- }
- function isRedo(event) {
- return isCtrl(event) && event.shiftKey && event.key === 'z';
- }
- function insert(text) {
- text = text
- .replace(/&/g, '&')
- .replace(/</g, '<')
- .replace(/>/g, '>')
- .replace(/"/g, '"')
- .replace(/'/g, ''');
- document.execCommand('insertHTML', false, text);
- }
- function debounce(cb, wait) {
- let timeout = 0;
- return (...args) => {
- clearTimeout(timeout);
- timeout = window.setTimeout(() => cb(...args), wait);
- };
- }
- function findPadding(text) {
- // 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.preventDefault();
- }
- function getSelection() {
- var _a;
- if (((_a = editor.parentNode) === null || _a === void 0 ? void 0 : _a.nodeType) == Node.DOCUMENT_FRAGMENT_NODE) {
- return editor.parentNode.getSelection();
- }
- return window.getSelection();
- }
- return {
- updateOptions(options) {
- options = Object.assign(Object.assign({}, options), options);
- },
- updateCode(code) {
- editor.textContent = code;
- highlight(editor);
- },
- onUpdate(cb) {
- callback = cb;
- },
- toString,
- save,
- restore,
- recordHistory,
- destroy() {
- for (let [type, fn] of listeners) {
- editor.removeEventListener(type, fn);
- }
- },
- };
- }
|