UNPKG

@point-api/dropdown-react

Version:

HOC to add a Point API autocomplete dropdown

264 lines 10.9 kB
import { getLineHeight } from "./Utils"; /** * Get the text from beginning of line to the caret (cursor) * @param editable - A ContentEditable div element */ function getTextFromHeadToCaret(editable) { const sel = window.getSelection && window.getSelection(); if (sel && sel.rangeCount > 0) { const range = sel.getRangeAt(0); const selection = range.cloneRange(); selection.selectNodeContents(range.startContainer); selection.setEnd(range.endContainer, range.endOffset); return selection.toString(); } return ""; } /** Adapter class for a ContentEditable div */ class ContentEditableAdapter { /** * Expose EditableAdapter API functions for a ContentEditable div * @param el - ContentEditable div element to wrap */ constructor(el, usesReact, supportsRichText, supportsAutoInsert) { if (!(el instanceof HTMLDivElement)) { throw new Error("Attempted to create ContentEditableAdapter with non-div element"); } this.el = el; this.usesReact = usesReact !== undefined ? usesReact : false; this.supportsRichText = supportsRichText !== undefined ? supportsRichText : true; this.supportsAutoInsert = supportsAutoInsert !== undefined ? supportsAutoInsert : true; } /** @note Gets text of the current line a user's caret is in, not the whole ContentEditable */ get text() { const sel = window.getSelection(); if (!sel) { return ""; } const initialRange = sel.getRangeAt(0); const range = initialRange.cloneRange(); range.selectNodeContents(range.startContainer); return range.toString(); } variableReplacer(match, p1, p2, p3, offset, str) { const wrappedVariable = document.createElement('span'); wrappedVariable.setAttribute('class', 'point-variable-' + match.substring(1, match.length - 1)); wrappedVariable.innerHTML = match; return wrappedVariable.outerHTML; } setCursorToEndOfSelectionStart() { const selection = window.getSelection(); if (selection && selection.rangeCount > 0) { const range = selection.getRangeAt(0); const rangeClone = range.cloneRange(); rangeClone.selectNodeContents(range.startContainer); range.setStartAfter(rangeClone.startContainer); } } insertText(text, cursorIndex, node) { if (!this.supportsAutoInsert) { return; } if (!this.supportsRichText) { text = this.stripRichText(text); } else { text = text.replace(/(\{[^\{]*\})+/gmu, this.variableReplacer); } const wrapper = document.createElement("span"); wrapper.innerHTML = text; const sel = window.getSelection(); if (!sel || sel.rangeCount === 0) { return; } const range = sel.getRangeAt(0); if (this.usesReact) { if (cursorIndex === undefined || node === undefined) return; // if span, cursor is at start of empty line const selection = range.cloneRange(); selection.selectNodeContents(node); if (node.nodeName === 'SPAN') { // V annoying case where double insertion w/out user text input in between // returns span again instead of text node if (node.firstChild && node.firstChild.nodeName === 'SPAN' && node.firstChild.textContent.length !== 0) { node = node.firstChild; const currNodeValue = node.innerText || ''; if (cursorIndex !== 0) { const pre = currNodeValue.slice(0, cursorIndex); const newNodeValue = pre + wrapper.innerText + currNodeValue.slice(cursorIndex); node.innerText = newNodeValue; } const inputEvent = new Event('input', { bubbles: true }); this.el.dispatchEvent(inputEvent); } else { node.appendChild(wrapper); range.setStartAfter(node); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); const event = new Event('input', { bubbles: true }); this.el.dispatchEvent(event); node.removeChild(wrapper); this.setCursorToEndOfSelectionStart(); this.el.dispatchEvent(event); } // else is text node } else { const currNodeValue = node.nodeValue; let newStartIndex; if (cursorIndex !== 0) { const pre = currNodeValue.slice(0, cursorIndex); newStartIndex = (pre + wrapper.textContent).length; const newNodeValue = pre + wrapper.textContent + currNodeValue.slice(cursorIndex); node.nodeValue = newNodeValue; } else if (wrapper.textContent) { newStartIndex = wrapper.textContent.length; node.nodeValue += wrapper.textContent; } const event = new Event('input', { bubbles: true }); range.setStart(node, newStartIndex); this.el.dispatchEvent(event); } } else { range.deleteContents(); range.insertNode(wrapper); const event = new Event('input', { bubbles: true }); this.el.dispatchEvent(event); range.collapse(); } const keyPressEvent = new Event('keypress', { bubbles: true }); this.el.dispatchEvent(keyPressEvent); } stripRichText(text) { const tempDiv = document.createElement('div'); tempDiv.innerHTML = text.replace(/<br>/g, '\n').replace(/&lt;|&gt;/g, ''); return tempDiv.innerText; } replaceText(match, text) { if (!this.supportsRichText) { text = this.stripRichText(text); } else { text = text.replace(/(\{[^\{]*\})+/gmu, this.variableReplacer); } let pre = getTextFromHeadToCaret(this.el); // use ownerDocument instead of window to support iframes const sel = window.getSelection(); if (!sel) { return; } const range = sel.getRangeAt(0); const selection = range.cloneRange(); selection.selectNodeContents(range.startContainer); const content = selection.toString(); const post = content.substring(match.index + match[0].length); pre = [ pre.slice(0, match.index), text, pre.slice(match.index + match[0].length) ].join(""); pre = pre.replace(/ $/, "&nbsp"); // create temporary elements const preWrapper = document.createElement("div"); preWrapper.innerHTML = pre; const postWrapper = document.createElement("div"); postWrapper.innerHTML = post; // create the fragment thats inserted const fragment = document.createDocumentFragment(); let childNode; let lastOfPre; // tslint:disable-next-line:no-conditional-assignment while ((childNode = preWrapper.firstChild)) { lastOfPre = fragment.appendChild(childNode); if (lastOfPre && lastOfPre.nodeType !== Node.TEXT_NODE) { // append empty node to prevent accidental writing inside html node lastOfPre = fragment.appendChild(document.createTextNode('')); } } // tslint:disable-next-line:no-conditional-assignment while ((childNode = postWrapper.firstChild)) { fragment.appendChild(childNode); } if (this.usesReact) { const node = selection.startContainer; node.nodeValue = fragment.textContent; // hack to trigger fb state updates // unfortunately strips our custom span class // so no dynamic vars :^() const event = new Event('input', { bubbles: true }); this.el.dispatchEvent(event); range.setStartAfter(node); } else { // insert the fragment & jump at the of the last node in "pre" range.selectNodeContents(range.startContainer); range.deleteContents(); range.insertNode(fragment); if (lastOfPre) { range.setStartAfter(lastOfPre); } range.collapse(true); sel.removeAllRanges(); sel.addRange(range); } } /** @note Gets cursor index of the current line a user's caret is in, not the whole ContentEditable */ getCursorIndex() { this.el.focus(); const sel = window.getSelection(); if (!sel) { return null; } const initialRange = sel.getRangeAt(0); const range = initialRange.cloneRange(); range.selectNodeContents(range.startContainer); range.setEnd(initialRange.endContainer, initialRange.endOffset); return range.toString().length; } getCaretPos() { const sel = window.getSelection(); if (!sel) { return null; } if (sel.rangeCount) { const range = sel.getRangeAt(0).cloneRange(); if (range.getClientRects()) { range.collapse(true); let rect = range.getClientRects()[0]; // edge case where rects is empty on collapsed range // known chrome bug - insert a new zws node and grab it's rect // https://github.com/ckeditor/ckeditor-dev/issues/1930 // god bless the souls who work on rich text editors if (!rect) { const empty = document.createTextNode('\u200B'); range.insertNode(empty); rect = range.getClientRects()[0]; if (empty.parentNode) { empty.parentNode.removeChild(empty); } } if (!sel.anchorNode) { return null; } if (rect && sel.anchorNode.parentElement) { return { left: rect.left, top: rect.top + getLineHeight(sel.anchorNode.parentElement) - this.el.scrollTop }; } } } return null; } } export default ContentEditableAdapter; //# sourceMappingURL=ContentEditableAdapter.js.map