forked from FINAKON/HelpProject
1. Initial Commit - a boiler plate code and POC to realize the concept of context sensitive help 2. Frontend code written in ReactJS 3. Backend code written in Java, Spring Boot Framework 4. Frontend Start: pre-requisites : node, npm npm run dev ==> to start the frontend vite server 5. Backend Start: pre-requisites : java, mvn mvn spring-boot:run ==> to start the backend server 6. Visit http://localhost:5173/ for basic demo of help, press F1 in textboxes 7. Visit http://localhost:5173/editor and enter "admin123" to add/modify texts. Happy Coding !!! Thank you, Bhargava.
359 lines
12 KiB
JavaScript
359 lines
12 KiB
JavaScript
import extend from 'extend';
|
|
import Delta from 'quill-delta';
|
|
import Parchment from 'parchment';
|
|
import Quill from '../core/quill';
|
|
import logger from '../core/logger';
|
|
import Module from '../core/module';
|
|
|
|
import { AlignAttribute, AlignStyle } from '../formats/align';
|
|
import { BackgroundStyle } from '../formats/background';
|
|
import CodeBlock from '../formats/code';
|
|
import { ColorStyle } from '../formats/color';
|
|
import { DirectionAttribute, DirectionStyle } from '../formats/direction';
|
|
import { FontStyle } from '../formats/font';
|
|
import { SizeStyle } from '../formats/size';
|
|
|
|
let debug = logger('quill:clipboard');
|
|
|
|
|
|
const DOM_KEY = '__ql-matcher';
|
|
|
|
const CLIPBOARD_CONFIG = [
|
|
[Node.TEXT_NODE, matchText],
|
|
[Node.TEXT_NODE, matchNewline],
|
|
['br', matchBreak],
|
|
[Node.ELEMENT_NODE, matchNewline],
|
|
[Node.ELEMENT_NODE, matchBlot],
|
|
[Node.ELEMENT_NODE, matchSpacing],
|
|
[Node.ELEMENT_NODE, matchAttributor],
|
|
[Node.ELEMENT_NODE, matchStyles],
|
|
['li', matchIndent],
|
|
['b', matchAlias.bind(matchAlias, 'bold')],
|
|
['i', matchAlias.bind(matchAlias, 'italic')],
|
|
['style', matchIgnore]
|
|
];
|
|
|
|
const ATTRIBUTE_ATTRIBUTORS = [
|
|
AlignAttribute,
|
|
DirectionAttribute
|
|
].reduce(function(memo, attr) {
|
|
memo[attr.keyName] = attr;
|
|
return memo;
|
|
}, {});
|
|
|
|
const STYLE_ATTRIBUTORS = [
|
|
AlignStyle,
|
|
BackgroundStyle,
|
|
ColorStyle,
|
|
DirectionStyle,
|
|
FontStyle,
|
|
SizeStyle
|
|
].reduce(function(memo, attr) {
|
|
memo[attr.keyName] = attr;
|
|
return memo;
|
|
}, {});
|
|
|
|
|
|
class Clipboard extends Module {
|
|
constructor(quill, options) {
|
|
super(quill, options);
|
|
this.quill.root.addEventListener('paste', this.onPaste.bind(this));
|
|
this.container = this.quill.addContainer('ql-clipboard');
|
|
this.container.setAttribute('contenteditable', true);
|
|
this.container.setAttribute('tabindex', -1);
|
|
this.matchers = [];
|
|
CLIPBOARD_CONFIG.concat(this.options.matchers).forEach(([selector, matcher]) => {
|
|
if (!options.matchVisual && matcher === matchSpacing) return;
|
|
this.addMatcher(selector, matcher);
|
|
});
|
|
}
|
|
|
|
addMatcher(selector, matcher) {
|
|
this.matchers.push([selector, matcher]);
|
|
}
|
|
|
|
convert(html) {
|
|
if (typeof html === 'string') {
|
|
this.container.innerHTML = html.replace(/\>\r?\n +\</g, '><'); // Remove spaces between tags
|
|
return this.convert();
|
|
}
|
|
const formats = this.quill.getFormat(this.quill.selection.savedRange.index);
|
|
if (formats[CodeBlock.blotName]) {
|
|
const text = this.container.innerText;
|
|
this.container.innerHTML = '';
|
|
return new Delta().insert(text, { [CodeBlock.blotName]: formats[CodeBlock.blotName] });
|
|
}
|
|
let [elementMatchers, textMatchers] = this.prepareMatching();
|
|
let delta = traverse(this.container, elementMatchers, textMatchers);
|
|
// Remove trailing newline
|
|
if (deltaEndsWith(delta, '\n') && delta.ops[delta.ops.length - 1].attributes == null) {
|
|
delta = delta.compose(new Delta().retain(delta.length() - 1).delete(1));
|
|
}
|
|
debug.log('convert', this.container.innerHTML, delta);
|
|
this.container.innerHTML = '';
|
|
return delta;
|
|
}
|
|
|
|
dangerouslyPasteHTML(index, html, source = Quill.sources.API) {
|
|
if (typeof index === 'string') {
|
|
this.quill.setContents(this.convert(index), html);
|
|
this.quill.setSelection(0, Quill.sources.SILENT);
|
|
} else {
|
|
let paste = this.convert(html);
|
|
this.quill.updateContents(new Delta().retain(index).concat(paste), source);
|
|
this.quill.setSelection(index + paste.length(), Quill.sources.SILENT);
|
|
}
|
|
}
|
|
|
|
onPaste(e) {
|
|
if (e.defaultPrevented || !this.quill.isEnabled()) return;
|
|
let range = this.quill.getSelection();
|
|
let delta = new Delta().retain(range.index);
|
|
let scrollTop = this.quill.scrollingContainer.scrollTop;
|
|
this.container.focus();
|
|
this.quill.selection.update(Quill.sources.SILENT);
|
|
setTimeout(() => {
|
|
delta = delta.concat(this.convert()).delete(range.length);
|
|
this.quill.updateContents(delta, Quill.sources.USER);
|
|
// range.length contributes to delta.length()
|
|
this.quill.setSelection(delta.length() - range.length, Quill.sources.SILENT);
|
|
this.quill.scrollingContainer.scrollTop = scrollTop;
|
|
this.quill.focus();
|
|
}, 1);
|
|
}
|
|
|
|
prepareMatching() {
|
|
let elementMatchers = [], textMatchers = [];
|
|
this.matchers.forEach((pair) => {
|
|
let [selector, matcher] = pair;
|
|
switch (selector) {
|
|
case Node.TEXT_NODE:
|
|
textMatchers.push(matcher);
|
|
break;
|
|
case Node.ELEMENT_NODE:
|
|
elementMatchers.push(matcher);
|
|
break;
|
|
default:
|
|
[].forEach.call(this.container.querySelectorAll(selector), (node) => {
|
|
// TODO use weakmap
|
|
node[DOM_KEY] = node[DOM_KEY] || [];
|
|
node[DOM_KEY].push(matcher);
|
|
});
|
|
break;
|
|
}
|
|
});
|
|
return [elementMatchers, textMatchers];
|
|
}
|
|
}
|
|
Clipboard.DEFAULTS = {
|
|
matchers: [],
|
|
matchVisual: true
|
|
};
|
|
|
|
|
|
function applyFormat(delta, format, value) {
|
|
if (typeof format === 'object') {
|
|
return Object.keys(format).reduce(function(delta, key) {
|
|
return applyFormat(delta, key, format[key]);
|
|
}, delta);
|
|
} else {
|
|
return delta.reduce(function(delta, op) {
|
|
if (op.attributes && op.attributes[format]) {
|
|
return delta.push(op);
|
|
} else {
|
|
return delta.insert(op.insert, extend({}, {[format]: value}, op.attributes));
|
|
}
|
|
}, new Delta());
|
|
}
|
|
}
|
|
|
|
function computeStyle(node) {
|
|
if (node.nodeType !== Node.ELEMENT_NODE) return {};
|
|
const DOM_KEY = '__ql-computed-style';
|
|
return node[DOM_KEY] || (node[DOM_KEY] = window.getComputedStyle(node));
|
|
}
|
|
|
|
function deltaEndsWith(delta, text) {
|
|
let endText = "";
|
|
for (let i = delta.ops.length - 1; i >= 0 && endText.length < text.length; --i) {
|
|
let op = delta.ops[i];
|
|
if (typeof op.insert !== 'string') break;
|
|
endText = op.insert + endText;
|
|
}
|
|
return endText.slice(-1*text.length) === text;
|
|
}
|
|
|
|
function isLine(node) {
|
|
if (node.childNodes.length === 0) return false; // Exclude embed blocks
|
|
let style = computeStyle(node);
|
|
return ['block', 'list-item'].indexOf(style.display) > -1;
|
|
}
|
|
|
|
function traverse(node, elementMatchers, textMatchers) { // Post-order
|
|
if (node.nodeType === node.TEXT_NODE) {
|
|
return textMatchers.reduce(function(delta, matcher) {
|
|
return matcher(node, delta);
|
|
}, new Delta());
|
|
} else if (node.nodeType === node.ELEMENT_NODE) {
|
|
return [].reduce.call(node.childNodes || [], (delta, childNode) => {
|
|
let childrenDelta = traverse(childNode, elementMatchers, textMatchers);
|
|
if (childNode.nodeType === node.ELEMENT_NODE) {
|
|
childrenDelta = elementMatchers.reduce(function(childrenDelta, matcher) {
|
|
return matcher(childNode, childrenDelta);
|
|
}, childrenDelta);
|
|
childrenDelta = (childNode[DOM_KEY] || []).reduce(function(childrenDelta, matcher) {
|
|
return matcher(childNode, childrenDelta);
|
|
}, childrenDelta);
|
|
}
|
|
return delta.concat(childrenDelta);
|
|
}, new Delta());
|
|
} else {
|
|
return new Delta();
|
|
}
|
|
}
|
|
|
|
|
|
function matchAlias(format, node, delta) {
|
|
return applyFormat(delta, format, true);
|
|
}
|
|
|
|
function matchAttributor(node, delta) {
|
|
let attributes = Parchment.Attributor.Attribute.keys(node);
|
|
let classes = Parchment.Attributor.Class.keys(node);
|
|
let styles = Parchment.Attributor.Style.keys(node);
|
|
let formats = {};
|
|
attributes.concat(classes).concat(styles).forEach((name) => {
|
|
let attr = Parchment.query(name, Parchment.Scope.ATTRIBUTE);
|
|
if (attr != null) {
|
|
formats[attr.attrName] = attr.value(node);
|
|
if (formats[attr.attrName]) return;
|
|
}
|
|
attr = ATTRIBUTE_ATTRIBUTORS[name];
|
|
if (attr != null && (attr.attrName === name || attr.keyName === name)) {
|
|
formats[attr.attrName] = attr.value(node) || undefined;
|
|
}
|
|
attr = STYLE_ATTRIBUTORS[name]
|
|
if (attr != null && (attr.attrName === name || attr.keyName === name)) {
|
|
attr = STYLE_ATTRIBUTORS[name];
|
|
formats[attr.attrName] = attr.value(node) || undefined;
|
|
}
|
|
});
|
|
if (Object.keys(formats).length > 0) {
|
|
delta = applyFormat(delta, formats);
|
|
}
|
|
return delta;
|
|
}
|
|
|
|
function matchBlot(node, delta) {
|
|
let match = Parchment.query(node);
|
|
if (match == null) return delta;
|
|
if (match.prototype instanceof Parchment.Embed) {
|
|
let embed = {};
|
|
let value = match.value(node);
|
|
if (value != null) {
|
|
embed[match.blotName] = value;
|
|
delta = new Delta().insert(embed, match.formats(node));
|
|
}
|
|
} else if (typeof match.formats === 'function') {
|
|
delta = applyFormat(delta, match.blotName, match.formats(node));
|
|
}
|
|
return delta;
|
|
}
|
|
|
|
function matchBreak(node, delta) {
|
|
if (!deltaEndsWith(delta, '\n')) {
|
|
delta.insert('\n');
|
|
}
|
|
return delta;
|
|
}
|
|
|
|
function matchIgnore() {
|
|
return new Delta();
|
|
}
|
|
|
|
function matchIndent(node, delta) {
|
|
let match = Parchment.query(node);
|
|
if (match == null || match.blotName !== 'list-item' || !deltaEndsWith(delta, '\n')) {
|
|
return delta;
|
|
}
|
|
let indent = -1, parent = node.parentNode;
|
|
while (!parent.classList.contains('ql-clipboard')) {
|
|
if ((Parchment.query(parent) || {}).blotName === 'list') {
|
|
indent += 1;
|
|
}
|
|
parent = parent.parentNode;
|
|
}
|
|
if (indent <= 0) return delta;
|
|
return delta.compose(new Delta().retain(delta.length() - 1).retain(1, { indent: indent}));
|
|
}
|
|
|
|
function matchNewline(node, delta) {
|
|
if (!deltaEndsWith(delta, '\n')) {
|
|
if (isLine(node) || (delta.length() > 0 && node.nextSibling && isLine(node.nextSibling))) {
|
|
delta.insert('\n');
|
|
}
|
|
}
|
|
return delta;
|
|
}
|
|
|
|
function matchSpacing(node, delta) {
|
|
if (isLine(node) && node.nextElementSibling != null && !deltaEndsWith(delta, '\n\n')) {
|
|
let nodeHeight = node.offsetHeight + parseFloat(computeStyle(node).marginTop) + parseFloat(computeStyle(node).marginBottom);
|
|
if (node.nextElementSibling.offsetTop > node.offsetTop + nodeHeight*1.5) {
|
|
delta.insert('\n');
|
|
}
|
|
}
|
|
return delta;
|
|
}
|
|
|
|
function matchStyles(node, delta) {
|
|
let formats = {};
|
|
let style = node.style || {};
|
|
if (style.fontStyle && computeStyle(node).fontStyle === 'italic') {
|
|
formats.italic = true;
|
|
}
|
|
if (style.fontWeight && (computeStyle(node).fontWeight.startsWith('bold') ||
|
|
parseInt(computeStyle(node).fontWeight) >= 700)) {
|
|
formats.bold = true;
|
|
}
|
|
if (Object.keys(formats).length > 0) {
|
|
delta = applyFormat(delta, formats);
|
|
}
|
|
if (parseFloat(style.textIndent || 0) > 0) { // Could be 0.5in
|
|
delta = new Delta().insert('\t').concat(delta);
|
|
}
|
|
return delta;
|
|
}
|
|
|
|
function matchText(node, delta) {
|
|
let text = node.data;
|
|
// Word represents empty line with <o:p> </o:p>
|
|
if (node.parentNode.tagName === 'O:P') {
|
|
return delta.insert(text.trim());
|
|
}
|
|
if (text.trim().length === 0 && node.parentNode.classList.contains('ql-clipboard')) {
|
|
return delta;
|
|
}
|
|
if (!computeStyle(node.parentNode).whiteSpace.startsWith('pre')) {
|
|
// eslint-disable-next-line func-style
|
|
let replacer = function(collapse, match) {
|
|
match = match.replace(/[^\u00a0]/g, ''); // \u00a0 is nbsp;
|
|
return match.length < 1 && collapse ? ' ' : match;
|
|
};
|
|
text = text.replace(/\r\n/g, ' ').replace(/\n/g, ' ');
|
|
text = text.replace(/\s\s+/g, replacer.bind(replacer, true)); // collapse whitespace
|
|
if ((node.previousSibling == null && isLine(node.parentNode)) ||
|
|
(node.previousSibling != null && isLine(node.previousSibling))) {
|
|
text = text.replace(/^\s+/, replacer.bind(replacer, false));
|
|
}
|
|
if ((node.nextSibling == null && isLine(node.parentNode)) ||
|
|
(node.nextSibling != null && isLine(node.nextSibling))) {
|
|
text = text.replace(/\s+$/, replacer.bind(replacer, false));
|
|
}
|
|
}
|
|
return delta.insert(text);
|
|
}
|
|
|
|
|
|
export { Clipboard as default, matchAttributor, matchBlot, matchNewline, matchSpacing, matchText };
|