@point-api/dropdown-react
Version:
HOC to add a Point API autocomplete dropdown
264 lines • 10.9 kB
JavaScript
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(/<|>/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(/ $/, " ");
// 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