added project'
This commit is contained in:
@@ -0,0 +1,403 @@
|
||||
class SelectionPreserver {
|
||||
constructor(rootNode) {
|
||||
if (rootNode === undefined || rootNode === null) {
|
||||
throw new Error("Please provide a valid rootNode.");
|
||||
}
|
||||
|
||||
this.rootNode = rootNode;
|
||||
this.rangeStartContainerAddress = null;
|
||||
this.rangeStartOffset = null;
|
||||
}
|
||||
|
||||
preserve() {
|
||||
const selection = window.getSelection();
|
||||
this.rangeStartOffset = selection.getRangeAt(0).startOffset;
|
||||
this.rangeStartContainerAddress = this.findRangeStartContainerAddress(
|
||||
selection
|
||||
);
|
||||
}
|
||||
|
||||
restore(restoreIndex) {
|
||||
if (
|
||||
this.rangeStartOffset === null ||
|
||||
this.rangeStartContainerAddress === null
|
||||
) {
|
||||
throw new Error("Please call preserve() first.");
|
||||
}
|
||||
|
||||
let rangeStartContainer = this.findRangeStartContainer();
|
||||
|
||||
const range = document.createRange();
|
||||
const offSet = restoreIndex || this.rangeStartOffset;
|
||||
range.setStart(rangeStartContainer, offSet);
|
||||
range.collapse();
|
||||
|
||||
const selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
|
||||
findRangeStartContainer() {
|
||||
let rangeStartContainer = this.rootNode;
|
||||
|
||||
this.rangeStartContainerAddress.forEach(address => {
|
||||
rangeStartContainer = rangeStartContainer.childNodes[address];
|
||||
});
|
||||
|
||||
return rangeStartContainer;
|
||||
}
|
||||
|
||||
findRangeStartContainerAddress(selection) {
|
||||
let rangeStartContainerAddress = [];
|
||||
|
||||
for (
|
||||
let currentContainer = selection.getRangeAt(0).startContainer;
|
||||
currentContainer !== this.rootNode;
|
||||
currentContainer = currentContainer.parentNode
|
||||
) {
|
||||
const parent = currentContainer.parentElement;
|
||||
const children = parent.childNodes;
|
||||
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
if (children[i] === currentContainer) {
|
||||
rangeStartContainerAddress = [i, ...rangeStartContainerAddress];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rangeStartContainerAddress;
|
||||
}
|
||||
}
|
||||
|
||||
const WORD_REGEX = /^[^\s]+$/;
|
||||
|
||||
const UP_KEY_CODE = 38;
|
||||
const DOWN_KEY_CODE = 40;
|
||||
const ENTER_KEY_CODE = 13;
|
||||
|
||||
(function(factory) {
|
||||
if (typeof define === "function" && define.amd) {
|
||||
define(["jquery"], factory);
|
||||
} else if (typeof module === "object" && module.exports) {
|
||||
module.exports = factory(require("jquery"));
|
||||
} else {
|
||||
factory(window.jQuery);
|
||||
}
|
||||
})(function($) {
|
||||
$.extend($.summernote.plugins, {
|
||||
summernoteAtMention: function(context) {
|
||||
/************************
|
||||
* Setup instance vars. *
|
||||
************************/
|
||||
this.editableEl = context.layoutInfo.editable[0];
|
||||
this.editorEl = context.layoutInfo.editor[0];
|
||||
|
||||
this.autocompleteAnchor = { left: null, top: null };
|
||||
this.autocompleteContainer = null;
|
||||
this.showingAutocomplete = false;
|
||||
this.selectedIndex = null;
|
||||
this.suggestions = null;
|
||||
|
||||
this.getSuggestions = _ => {
|
||||
return [];
|
||||
};
|
||||
|
||||
/********************
|
||||
* Read-in options. *
|
||||
********************/
|
||||
if (
|
||||
context.options &&
|
||||
context.options.callbacks &&
|
||||
context.options.callbacks.summernoteAtMention
|
||||
) {
|
||||
const summernoteCallbacks =
|
||||
context.options.callbacks.summernoteAtMention;
|
||||
|
||||
if (summernoteCallbacks.getSuggestions) {
|
||||
this.getSuggestions = summernoteCallbacks.getSuggestions;
|
||||
}
|
||||
|
||||
if (summernoteCallbacks.onSelect) {
|
||||
this.onSelect = summernoteCallbacks.onSelect;
|
||||
}
|
||||
}
|
||||
|
||||
/**********
|
||||
* Events *
|
||||
**********/
|
||||
this.events = {
|
||||
"summernote.blur": () => {
|
||||
if (this.showingAutocomplete) this.hideAutocomplete();
|
||||
},
|
||||
"summernote.keydown": (_, event) => {
|
||||
if (this.showingAutocomplete) {
|
||||
switch (event.keyCode) {
|
||||
case ENTER_KEY_CODE: {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.handleEnter();
|
||||
break;
|
||||
}
|
||||
case UP_KEY_CODE: {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const newIndex =
|
||||
this.selectedIndex === 0 ? 0 : this.selectedIndex - 1;
|
||||
this.updateAutocomplete(this.suggestions, newIndex);
|
||||
break;
|
||||
}
|
||||
case DOWN_KEY_CODE: {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const newIndex =
|
||||
this.selectedIndex === this.suggestions.length - 1
|
||||
? this.selectedIndex
|
||||
: this.selectedIndex + 1;
|
||||
|
||||
this.updateAutocomplete(this.suggestions, newIndex);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"summernote.keyup": async (_, event) => {
|
||||
const selection = document.getSelection();
|
||||
const currentText = selection.anchorNode.nodeValue;
|
||||
const { word, absoluteIndex } = this.findWordAndIndices(
|
||||
currentText || "",
|
||||
selection.anchorOffset
|
||||
);
|
||||
const trimmedWord = word.slice(1);
|
||||
|
||||
if (
|
||||
this.showingAutocomplete &&
|
||||
![DOWN_KEY_CODE, UP_KEY_CODE, ENTER_KEY_CODE].includes(
|
||||
event.keyCode
|
||||
)
|
||||
) {
|
||||
if (word[0] === "@") {
|
||||
const suggestions = await this.getSuggestions(trimmedWord);
|
||||
this.updateAutocomplete(suggestions, this.selectedIndex);
|
||||
} else {
|
||||
this.hideAutocomplete();
|
||||
}
|
||||
} else if (!this.showingAutocomplete && word[0] === "@") {
|
||||
this.suggestions = await this.getSuggestions(trimmedWord);
|
||||
this.selectedIndex = 0;
|
||||
this.showAutocomplete(absoluteIndex, selection.anchorNode);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/***********
|
||||
* Helpers *
|
||||
***********/
|
||||
|
||||
this.handleEnter = () => {
|
||||
this.handleSelection();
|
||||
};
|
||||
|
||||
this.handleClick = suggestion => {
|
||||
const selectedIndex = this.suggestions.findIndex(s => s === suggestion);
|
||||
|
||||
if (selectedIndex === -1) {
|
||||
throw new Error("Unable to find suggestion in suggestions.");
|
||||
}
|
||||
|
||||
this.selectedIndex = selectedIndex;
|
||||
this.handleSelection();
|
||||
};
|
||||
|
||||
this.handleSelection = () => {
|
||||
if (this.suggestions === null || this.suggestions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newWord = this.suggestions[this.selectedIndex].trimStart();
|
||||
|
||||
if (this.onSelect !== undefined) {
|
||||
this.onSelect(newWord);
|
||||
}
|
||||
|
||||
const selection = document.getSelection();
|
||||
const currentText = selection.anchorNode.nodeValue;
|
||||
const { word, absoluteIndex } = this.findWordAndIndices(
|
||||
currentText || "",
|
||||
selection.anchorOffset
|
||||
);
|
||||
|
||||
const selectionPreserver = new SelectionPreserver(this.editableEl);
|
||||
selectionPreserver.preserve();
|
||||
|
||||
selection.anchorNode.textContent =
|
||||
currentText.slice(0, absoluteIndex + 1) +
|
||||
newWord +
|
||||
" " +
|
||||
currentText.slice(absoluteIndex + word.length);
|
||||
|
||||
selectionPreserver.restore(absoluteIndex + newWord.length + 1);
|
||||
|
||||
if (context.options.callbacks.onChange !== undefined) {
|
||||
context.options.callbacks.onChange(this.editableEl.innerHTML);
|
||||
}
|
||||
};
|
||||
|
||||
this.updateAutocomplete = (suggestions, selectedIndex) => {
|
||||
this.selectedIndex = selectedIndex;
|
||||
this.suggestions = suggestions;
|
||||
this.renderAutocomplete();
|
||||
};
|
||||
|
||||
this.showAutocomplete = (atTextIndex, indexAnchor) => {
|
||||
if (this.showingAutocomplete) {
|
||||
throw new Error(
|
||||
"Cannot call showAutocomplete if autocomplete is already showing."
|
||||
);
|
||||
}
|
||||
this.setAutocompleteAnchor(atTextIndex, indexAnchor);
|
||||
this.renderAutocompleteContainer();
|
||||
this.renderAutocomplete();
|
||||
this.showingAutocomplete = true;
|
||||
};
|
||||
|
||||
this.renderAutocompleteContainer = () => {
|
||||
this.autocompleteContainer = document.createElement("div");
|
||||
this.autocompleteContainer.style.top =
|
||||
String(this.autocompleteAnchor.top) + "px";
|
||||
this.autocompleteContainer.style.left =
|
||||
String(this.autocompleteAnchor.left) + "px";
|
||||
this.autocompleteContainer.style.position = "absolute";
|
||||
this.autocompleteContainer.style.backgroundColor = "#e4e4e4";
|
||||
this.autocompleteContainer.style.zIndex = Number.MAX_SAFE_INTEGER;
|
||||
|
||||
document.body.appendChild(this.autocompleteContainer);
|
||||
};
|
||||
|
||||
this.renderAutocomplete = () => {
|
||||
if (this.autocompleteContainer === null) {
|
||||
throw new Error(
|
||||
"Cannot call renderAutocomplete without an autocompleteContainer. "
|
||||
);
|
||||
}
|
||||
const autocompleteContent = document.createElement("div");
|
||||
|
||||
this.suggestions.forEach((suggestion, idx) => {
|
||||
const suggestionDiv = document.createElement("div");
|
||||
suggestionDiv.textContent = suggestion;
|
||||
|
||||
suggestionDiv.style.padding = "5px 10px";
|
||||
|
||||
if (this.selectedIndex === idx) {
|
||||
suggestionDiv.style.backgroundColor = "#2e6da4";
|
||||
suggestionDiv.style.color = "white";
|
||||
}
|
||||
|
||||
suggestionDiv.addEventListener("mousedown", () => {
|
||||
this.handleClick(suggestion);
|
||||
});
|
||||
|
||||
autocompleteContent.appendChild(suggestionDiv);
|
||||
});
|
||||
|
||||
this.autocompleteContainer.innerHTML = "";
|
||||
this.autocompleteContainer.appendChild(autocompleteContent);
|
||||
};
|
||||
|
||||
this.hideAutocomplete = () => {
|
||||
if (!this.showingAutocomplete)
|
||||
throw new Error(
|
||||
"Cannot call hideAutocomplete if autocomplete is not showing."
|
||||
);
|
||||
|
||||
document.body.removeChild(this.autocompleteContainer);
|
||||
this.autocompleteAnchor = { left: null, top: null };
|
||||
this.selectedIndex = null;
|
||||
this.suggestions = null;
|
||||
this.showingAutocomplete = false;
|
||||
};
|
||||
|
||||
this.findWordAndIndices = (text, offset) => {
|
||||
if (offset > text.length) {
|
||||
return { word: "", relativeIndex: 0 };
|
||||
} else {
|
||||
let leftWord = "";
|
||||
let rightWord = "";
|
||||
let relativeIndex = 0;
|
||||
let absoluteIndex = offset;
|
||||
|
||||
for (let currentOffset = offset; currentOffset > 0; currentOffset--) {
|
||||
if (text[currentOffset - 1].match(WORD_REGEX)) {
|
||||
leftWord = text[currentOffset - 1] + leftWord;
|
||||
relativeIndex++;
|
||||
absoluteIndex--;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (
|
||||
let currentOffset = offset - 1;
|
||||
currentOffset > 0 && currentOffset < text.length - 1;
|
||||
currentOffset++
|
||||
) {
|
||||
if (text[currentOffset + 1].match(WORD_REGEX)) {
|
||||
rightWord = rightWord + text[currentOffset + 1];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
word: leftWord + rightWord,
|
||||
relativeIndex,
|
||||
absoluteIndex
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
this.setAutocompleteAnchor = (atTextIndex, indexAnchor) => {
|
||||
let html = indexAnchor.parentNode.innerHTML;
|
||||
const text = indexAnchor.nodeValue;
|
||||
|
||||
let atIndex = -1;
|
||||
for (let i = 0; i <= atTextIndex; i++) {
|
||||
if (text[i] === "@") {
|
||||
atIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
let htmlIndex;
|
||||
for (let i = 0, htmlAtIndex = 0; i < html.length; i++) {
|
||||
if (html[i] === "@") {
|
||||
if (htmlAtIndex === atIndex) {
|
||||
htmlIndex = i;
|
||||
break;
|
||||
} else {
|
||||
htmlAtIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const atNodeId = "at-node-" + String(Math.floor(Math.random() * 10000));
|
||||
const spanString = `<span id="${atNodeId}">@</span>`;
|
||||
|
||||
const selectionPreserver = new SelectionPreserver(this.editableEl);
|
||||
selectionPreserver.preserve();
|
||||
|
||||
indexAnchor.parentNode.innerHTML =
|
||||
html.slice(0, htmlIndex) + spanString + html.slice(htmlIndex + 1);
|
||||
const anchorElement = document.querySelector("#" + atNodeId);
|
||||
const anchorBoundingRect = anchorElement.getBoundingClientRect();
|
||||
|
||||
this.autocompleteAnchor = {
|
||||
top: anchorBoundingRect.top + anchorBoundingRect.height + 2,
|
||||
left: anchorBoundingRect.left
|
||||
};
|
||||
|
||||
selectionPreserver.findRangeStartContainer().parentNode.innerHTML = html;
|
||||
selectionPreserver.restore();
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user