0
Fork 0
mirror of https://github.com/fastmail/Squire.git synced 2024-12-22 07:13:08 -05:00

Release v2.2.0

This commit is contained in:
Neil Jenkins 2023-10-02 13:46:50 +11:00
parent 950e122c5c
commit 9f3e2610a6
13 changed files with 217 additions and 239 deletions

View file

@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file, starting fr
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.2.0] - 2023-10-02
### Added
- The Squire config now has support for a toPlainText function, that takes an
HTML string and should return the plain text version of that content to be
added to the clipboard when cutting/copying.
### Changed
- The default conversion of the HTML to plain text when cutting/copying now
uses the same algorithm as the getSelectedText method.
## [2.1.1] - 2023-09-27 ## [2.1.1] - 2023-09-27
### Fixed ### Fixed

186
dist/squire-raw.js vendored
View file

@ -1316,37 +1316,99 @@
moveRangeBoundariesDownTree(range); moveRangeBoundariesDownTree(range);
}; };
// source/range/Contents.ts
var getTextContentsOfRange = (range) => {
if (range.collapsed) {
return "";
}
const startContainer = range.startContainer;
const endContainer = range.endContainer;
const walker = new TreeIterator(
range.commonAncestorContainer,
SHOW_ELEMENT_OR_TEXT,
(node2) => {
return isNodeContainedInRange(range, node2, true);
}
);
walker.currentNode = startContainer;
let node = startContainer;
let textContent = "";
let addedTextInBlock = false;
let value;
if (!(node instanceof Element) && !(node instanceof Text) || !walker.filter(node)) {
node = walker.nextNode();
}
while (node) {
if (node instanceof Text) {
value = node.data;
if (value && /\S/.test(value)) {
if (node === endContainer) {
value = value.slice(0, range.endOffset);
}
if (node === startContainer) {
value = value.slice(range.startOffset);
}
textContent += value;
addedTextInBlock = true;
}
} else if (node.nodeName === "BR" || addedTextInBlock && !isInline(node)) {
textContent += "\n";
addedTextInBlock = false;
}
node = walker.nextNode();
}
textContent = textContent.replace(/ /g, " ");
return textContent;
};
// source/Clipboard.ts // source/Clipboard.ts
var indexOf = Array.prototype.indexOf; var indexOf = Array.prototype.indexOf;
var setClipboardData = (event, contents, root, toCleanHTML, toPlainText, plainTextOnly) => { var extractRangeToClipboard = (event, range, root, removeRangeFromDocument, toCleanHTML, toPlainText, plainTextOnly) => {
const clipboardData = event.clipboardData; const clipboardData = event.clipboardData;
const body = document.body; if (isLegacyEdge || !clipboardData) {
const node = createElement("DIV"); return false;
}
let text = toPlainText ? "" : getTextContentsOfRange(range);
const startBlock = getStartBlockOfRange(range, root);
const endBlock = getEndBlockOfRange(range, root);
let copyRoot = root;
if (startBlock === endBlock && (startBlock == null ? void 0 : startBlock.contains(range.commonAncestorContainer))) {
copyRoot = startBlock;
}
let contents;
if (removeRangeFromDocument) {
contents = deleteContentsOfRange(range, root);
} else {
range = range.cloneRange();
moveRangeBoundariesDownTree(range);
moveRangeBoundariesUpTree(range, copyRoot, copyRoot, root);
contents = range.cloneContents();
}
let parent = range.commonAncestorContainer;
if (parent instanceof Text) {
parent = parent.parentNode;
}
while (parent && parent !== copyRoot) {
const newContents = parent.cloneNode(false);
newContents.appendChild(contents);
contents = newContents;
parent = parent.parentNode;
}
let html; let html;
let text;
if (contents.childNodes.length === 1 && contents.childNodes[0] instanceof Text) { if (contents.childNodes.length === 1 && contents.childNodes[0] instanceof Text) {
text = contents.childNodes[0].data.replace(/ /g, " "); text = contents.childNodes[0].data.replace(/ /g, " ");
plainTextOnly = true; plainTextOnly = true;
} else { } else {
const node = createElement("DIV");
node.appendChild(contents); node.appendChild(contents);
html = node.innerHTML; html = node.innerHTML;
if (toCleanHTML) { if (toCleanHTML) {
html = toCleanHTML(html); html = toCleanHTML(html);
} }
} }
if (text !== void 0) { if (plainTextOnly) {
} else if (toPlainText && html !== void 0) { } else if (toPlainText && html !== void 0) {
text = toPlainText(html); text = toPlainText(html);
} else {
cleanupBRs(node, root, true);
node.setAttribute(
"style",
"position:fixed;overflow:hidden;bottom:100%;right:100%;"
);
body.appendChild(node);
text = node.innerText || node.textContent;
text = text.replace(/ /g, " ");
body.removeChild(node);
} }
if (isWin) { if (isWin) {
text = text.replace(/\r?\n/g, "\r\n"); text = text.replace(/\r?\n/g, "\r\n");
@ -1356,45 +1418,7 @@
} }
clipboardData.setData("text/plain", text); clipboardData.setData("text/plain", text);
event.preventDefault(); event.preventDefault();
}; return true;
var extractRangeToClipboard = (event, range, root, removeRangeFromDocument, toCleanHTML, toPlainText, plainTextOnly) => {
if (!isLegacyEdge && event.clipboardData) {
const startBlock = getStartBlockOfRange(range, root);
const endBlock = getEndBlockOfRange(range, root);
let copyRoot = root;
if (startBlock === endBlock && (startBlock == null ? void 0 : startBlock.contains(range.commonAncestorContainer))) {
copyRoot = startBlock;
}
let contents;
if (removeRangeFromDocument) {
contents = deleteContentsOfRange(range, root);
} else {
range = range.cloneRange();
moveRangeBoundariesDownTree(range);
moveRangeBoundariesUpTree(range, copyRoot, copyRoot, root);
contents = range.cloneContents();
}
let parent = range.commonAncestorContainer;
if (parent instanceof Text) {
parent = parent.parentNode;
}
while (parent && parent !== copyRoot) {
const newContents = parent.cloneNode(false);
newContents.appendChild(contents);
contents = newContents;
parent = parent.parentNode;
}
setClipboardData(
event,
contents,
root,
toCleanHTML,
toPlainText,
plainTextOnly
);
return true;
}
return false;
}; };
var _onCut = function(event) { var _onCut = function(event) {
const range = this.getSelection(); const range = this.getSelection();
@ -1410,7 +1434,7 @@
root, root,
true, true,
this._config.willCutCopy, this._config.willCutCopy,
null, this._config.toPlainText,
false false
); );
if (!handled) { if (!handled) {
@ -1431,7 +1455,7 @@
this._root, this._root,
false, false,
this._config.willCutCopy, this._config.willCutCopy,
null, this._config.toPlainText,
false false
); );
}; };
@ -2169,11 +2193,6 @@
}); });
this._mutation = mutation; this._mutation = mutation;
root.setAttribute("contenteditable", "true"); root.setAttribute("contenteditable", "true");
try {
document.execCommand("enableObjectResizing", false, "false");
document.execCommand("enableInlineTableEditing", false, "false");
} catch (_) {
}
this.addEventListener( this.addEventListener(
"beforeinput", "beforeinput",
this._beforeInput this._beforeInput
@ -2208,6 +2227,7 @@
}, },
addLinks: true, addLinks: true,
willCutCopy: null, willCutCopy: null,
toPlainText: null,
sanitizeToDOMFragment: (html) => { sanitizeToDOMFragment: (html) => {
const frag = DOMPurify.sanitize(html, { const frag = DOMPurify.sanitize(html, {
ALLOW_UNKNOWN_PROTOCOLS: true, ALLOW_UNKNOWN_PROTOCOLS: true,
@ -3002,48 +3022,8 @@
} }
return this.insertHTML(lines.join(""), isPaste); return this.insertHTML(lines.join(""), isPaste);
} }
getSelectedText() { getSelectedText(range) {
const range = this.getSelection(); return getTextContentsOfRange(range || this.getSelection());
if (range.collapsed) {
return "";
}
const startContainer = range.startContainer;
const endContainer = range.endContainer;
const walker = new TreeIterator(
range.commonAncestorContainer,
SHOW_ELEMENT_OR_TEXT,
(node2) => {
return isNodeContainedInRange(range, node2, true);
}
);
walker.currentNode = startContainer;
let node = startContainer;
let textContent = "";
let addedTextInBlock = false;
let value;
if (!(node instanceof Element) && !(node instanceof Text) || !walker.filter(node)) {
node = walker.nextNode();
}
while (node) {
if (node instanceof Text) {
value = node.data;
if (value && /\S/.test(value)) {
if (node === endContainer) {
value = value.slice(0, range.endOffset);
}
if (node === startContainer) {
value = value.slice(range.startOffset);
}
textContent += value;
addedTextInBlock = true;
}
} else if (node.nodeName === "BR" || addedTextInBlock && !isInline(node)) {
textContent += "\n";
addedTextInBlock = false;
}
node = walker.nextNode();
}
return textContent;
} }
// --- Inline formatting // --- Inline formatting
/** /**

186
dist/squire-raw.mjs vendored
View file

@ -1314,37 +1314,99 @@ var insertTreeFragmentIntoRange = (range, frag, root) => {
moveRangeBoundariesDownTree(range); moveRangeBoundariesDownTree(range);
}; };
// source/range/Contents.ts
var getTextContentsOfRange = (range) => {
if (range.collapsed) {
return "";
}
const startContainer = range.startContainer;
const endContainer = range.endContainer;
const walker = new TreeIterator(
range.commonAncestorContainer,
SHOW_ELEMENT_OR_TEXT,
(node2) => {
return isNodeContainedInRange(range, node2, true);
}
);
walker.currentNode = startContainer;
let node = startContainer;
let textContent = "";
let addedTextInBlock = false;
let value;
if (!(node instanceof Element) && !(node instanceof Text) || !walker.filter(node)) {
node = walker.nextNode();
}
while (node) {
if (node instanceof Text) {
value = node.data;
if (value && /\S/.test(value)) {
if (node === endContainer) {
value = value.slice(0, range.endOffset);
}
if (node === startContainer) {
value = value.slice(range.startOffset);
}
textContent += value;
addedTextInBlock = true;
}
} else if (node.nodeName === "BR" || addedTextInBlock && !isInline(node)) {
textContent += "\n";
addedTextInBlock = false;
}
node = walker.nextNode();
}
textContent = textContent.replace(/ /g, " ");
return textContent;
};
// source/Clipboard.ts // source/Clipboard.ts
var indexOf = Array.prototype.indexOf; var indexOf = Array.prototype.indexOf;
var setClipboardData = (event, contents, root, toCleanHTML, toPlainText, plainTextOnly) => { var extractRangeToClipboard = (event, range, root, removeRangeFromDocument, toCleanHTML, toPlainText, plainTextOnly) => {
const clipboardData = event.clipboardData; const clipboardData = event.clipboardData;
const body = document.body; if (isLegacyEdge || !clipboardData) {
const node = createElement("DIV"); return false;
}
let text = toPlainText ? "" : getTextContentsOfRange(range);
const startBlock = getStartBlockOfRange(range, root);
const endBlock = getEndBlockOfRange(range, root);
let copyRoot = root;
if (startBlock === endBlock && startBlock?.contains(range.commonAncestorContainer)) {
copyRoot = startBlock;
}
let contents;
if (removeRangeFromDocument) {
contents = deleteContentsOfRange(range, root);
} else {
range = range.cloneRange();
moveRangeBoundariesDownTree(range);
moveRangeBoundariesUpTree(range, copyRoot, copyRoot, root);
contents = range.cloneContents();
}
let parent = range.commonAncestorContainer;
if (parent instanceof Text) {
parent = parent.parentNode;
}
while (parent && parent !== copyRoot) {
const newContents = parent.cloneNode(false);
newContents.appendChild(contents);
contents = newContents;
parent = parent.parentNode;
}
let html; let html;
let text;
if (contents.childNodes.length === 1 && contents.childNodes[0] instanceof Text) { if (contents.childNodes.length === 1 && contents.childNodes[0] instanceof Text) {
text = contents.childNodes[0].data.replace(/ /g, " "); text = contents.childNodes[0].data.replace(/ /g, " ");
plainTextOnly = true; plainTextOnly = true;
} else { } else {
const node = createElement("DIV");
node.appendChild(contents); node.appendChild(contents);
html = node.innerHTML; html = node.innerHTML;
if (toCleanHTML) { if (toCleanHTML) {
html = toCleanHTML(html); html = toCleanHTML(html);
} }
} }
if (text !== void 0) { if (plainTextOnly) {
} else if (toPlainText && html !== void 0) { } else if (toPlainText && html !== void 0) {
text = toPlainText(html); text = toPlainText(html);
} else {
cleanupBRs(node, root, true);
node.setAttribute(
"style",
"position:fixed;overflow:hidden;bottom:100%;right:100%;"
);
body.appendChild(node);
text = node.innerText || node.textContent;
text = text.replace(/ /g, " ");
body.removeChild(node);
} }
if (isWin) { if (isWin) {
text = text.replace(/\r?\n/g, "\r\n"); text = text.replace(/\r?\n/g, "\r\n");
@ -1354,45 +1416,7 @@ var setClipboardData = (event, contents, root, toCleanHTML, toPlainText, plainTe
} }
clipboardData.setData("text/plain", text); clipboardData.setData("text/plain", text);
event.preventDefault(); event.preventDefault();
}; return true;
var extractRangeToClipboard = (event, range, root, removeRangeFromDocument, toCleanHTML, toPlainText, plainTextOnly) => {
if (!isLegacyEdge && event.clipboardData) {
const startBlock = getStartBlockOfRange(range, root);
const endBlock = getEndBlockOfRange(range, root);
let copyRoot = root;
if (startBlock === endBlock && startBlock?.contains(range.commonAncestorContainer)) {
copyRoot = startBlock;
}
let contents;
if (removeRangeFromDocument) {
contents = deleteContentsOfRange(range, root);
} else {
range = range.cloneRange();
moveRangeBoundariesDownTree(range);
moveRangeBoundariesUpTree(range, copyRoot, copyRoot, root);
contents = range.cloneContents();
}
let parent = range.commonAncestorContainer;
if (parent instanceof Text) {
parent = parent.parentNode;
}
while (parent && parent !== copyRoot) {
const newContents = parent.cloneNode(false);
newContents.appendChild(contents);
contents = newContents;
parent = parent.parentNode;
}
setClipboardData(
event,
contents,
root,
toCleanHTML,
toPlainText,
plainTextOnly
);
return true;
}
return false;
}; };
var _onCut = function(event) { var _onCut = function(event) {
const range = this.getSelection(); const range = this.getSelection();
@ -1408,7 +1432,7 @@ var _onCut = function(event) {
root, root,
true, true,
this._config.willCutCopy, this._config.willCutCopy,
null, this._config.toPlainText,
false false
); );
if (!handled) { if (!handled) {
@ -1429,7 +1453,7 @@ var _onCopy = function(event) {
this._root, this._root,
false, false,
this._config.willCutCopy, this._config.willCutCopy,
null, this._config.toPlainText,
false false
); );
}; };
@ -2166,11 +2190,6 @@ var Squire = class {
}); });
this._mutation = mutation; this._mutation = mutation;
root.setAttribute("contenteditable", "true"); root.setAttribute("contenteditable", "true");
try {
document.execCommand("enableObjectResizing", false, "false");
document.execCommand("enableInlineTableEditing", false, "false");
} catch (_) {
}
this.addEventListener( this.addEventListener(
"beforeinput", "beforeinput",
this._beforeInput this._beforeInput
@ -2205,6 +2224,7 @@ var Squire = class {
}, },
addLinks: true, addLinks: true,
willCutCopy: null, willCutCopy: null,
toPlainText: null,
sanitizeToDOMFragment: (html) => { sanitizeToDOMFragment: (html) => {
const frag = DOMPurify.sanitize(html, { const frag = DOMPurify.sanitize(html, {
ALLOW_UNKNOWN_PROTOCOLS: true, ALLOW_UNKNOWN_PROTOCOLS: true,
@ -2999,48 +3019,8 @@ var Squire = class {
} }
return this.insertHTML(lines.join(""), isPaste); return this.insertHTML(lines.join(""), isPaste);
} }
getSelectedText() { getSelectedText(range) {
const range = this.getSelection(); return getTextContentsOfRange(range || this.getSelection());
if (range.collapsed) {
return "";
}
const startContainer = range.startContainer;
const endContainer = range.endContainer;
const walker = new TreeIterator(
range.commonAncestorContainer,
SHOW_ELEMENT_OR_TEXT,
(node2) => {
return isNodeContainedInRange(range, node2, true);
}
);
walker.currentNode = startContainer;
let node = startContainer;
let textContent = "";
let addedTextInBlock = false;
let value;
if (!(node instanceof Element) && !(node instanceof Text) || !walker.filter(node)) {
node = walker.nextNode();
}
while (node) {
if (node instanceof Text) {
value = node.data;
if (value && /\S/.test(value)) {
if (node === endContainer) {
value = value.slice(0, range.endOffset);
}
if (node === startContainer) {
value = value.slice(range.startOffset);
}
textContent += value;
addedTextInBlock = true;
}
} else if (node.nodeName === "BR" || addedTextInBlock && !isInline(node)) {
textContent += "\n";
addedTextInBlock = false;
}
node = walker.nextNode();
}
return textContent;
} }
// --- Inline formatting // --- Inline formatting
/** /**

20
dist/squire.js vendored

File diff suppressed because one or more lines are too long

8
dist/squire.js.map vendored

File diff suppressed because one or more lines are too long

22
dist/squire.mjs vendored

File diff suppressed because one or more lines are too long

8
dist/squire.mjs.map vendored

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
{"version":3,"file":"Clipboard.d.ts","sourceRoot":"","sources":["../../source/Clipboard.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAyEvC,QAAA,MAAM,uBAAuB,UAClB,cAAc,SACd,KAAK,QACN,WAAW,2BACQ,OAAO,uBACJ,MAAM,KAAK,MAAM,+BACjB,MAAM,KAAK,MAAM,yBAC9B,OAAO,KACvB,OAmDF,CAAC;AAIF,QAAA,MAAM,MAAM,SAAmB,MAAM,SAAS,cAAc,KAAG,IAkC9D,CAAC;AAEF,QAAA,MAAM,OAAO,SAAmB,MAAM,SAAS,cAAc,KAAG,IAU/D,CAAC;AAIF,QAAA,MAAM,gBAAgB,SAAmB,MAAM,SAAS,aAAa,KAAG,IAEvE,CAAC;AAEF,QAAA,MAAM,QAAQ,SAAmB,MAAM,SAAS,cAAc,KAAG,IAqLhE,CAAC;AAKF,QAAA,MAAM,OAAO,SAAmB,MAAM,SAAS,SAAS,KAAG,IAwB1D,CAAC;AAIF,OAAO,EACH,uBAAuB,EACvB,MAAM,EACN,OAAO,EACP,gBAAgB,EAChB,QAAQ,EACR,OAAO,GACV,CAAC"} {"version":3,"file":"Clipboard.d.ts","sourceRoot":"","sources":["../../source/Clipboard.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAOvC,QAAA,MAAM,uBAAuB,UAClB,cAAc,SACd,KAAK,QACN,WAAW,2BACQ,OAAO,uBACJ,MAAM,KAAK,MAAM,+BACjB,MAAM,KAAK,MAAM,yBAC9B,OAAO,KACvB,OA2FF,CAAC;AAIF,QAAA,MAAM,MAAM,SAAmB,MAAM,SAAS,cAAc,KAAG,IAkC9D,CAAC;AAEF,QAAA,MAAM,OAAO,SAAmB,MAAM,SAAS,cAAc,KAAG,IAU/D,CAAC;AAIF,QAAA,MAAM,gBAAgB,SAAmB,MAAM,SAAS,aAAa,KAAG,IAEvE,CAAC;AAEF,QAAA,MAAM,QAAQ,SAAmB,MAAM,SAAS,cAAc,KAAG,IAqLhE,CAAC;AAKF,QAAA,MAAM,OAAO,SAAmB,MAAM,SAAS,SAAS,KAAG,IAwB1D,CAAC;AAIF,OAAO,EACH,uBAAuB,EACvB,MAAM,EACN,OAAO,EACP,gBAAgB,EAChB,QAAQ,EACR,OAAO,GACV,CAAC"}

View file

@ -23,6 +23,7 @@ interface SquireConfig {
}; };
addLinks: boolean; addLinks: boolean;
willCutCopy: null | ((html: string) => string); willCutCopy: null | ((html: string) => string);
toPlainText: null | ((html: string) => string);
sanitizeToDOMFragment: (html: string, editor: Squire) => DocumentFragment; sanitizeToDOMFragment: (html: string, editor: Squire) => DocumentFragment;
didError: (x: any) => void; didError: (x: any) => void;
} }
@ -104,7 +105,7 @@ declare class Squire {
insertElement(el: Element, range?: Range): Squire; insertElement(el: Element, range?: Range): Squire;
insertImage(src: string, attributes: Record<string, string>): HTMLImageElement; insertImage(src: string, attributes: Record<string, string>): HTMLImageElement;
insertPlainText(plainText: string, isPaste: boolean): Squire; insertPlainText(plainText: string, isPaste: boolean): Squire;
getSelectedText(): string; getSelectedText(range?: Range): string;
/** /**
* Extracts the font-family and font-size (if any) of the element * Extracts the font-family and font-size (if any) of the element
* holding the cursor. If there's a selection, returns an empty object. * holding the cursor. If there's a selection, returns an empty object.

File diff suppressed because one or more lines are too long

3
dist/types/range/Contents.d.ts vendored Normal file
View file

@ -0,0 +1,3 @@
declare const getTextContentsOfRange: (range: Range) => string;
export { getTextContentsOfRange };
//# sourceMappingURL=Contents.d.ts.map

1
dist/types/range/Contents.d.ts.map vendored Normal file
View file

@ -0,0 +1 @@
{"version":3,"file":"Contents.d.ts","sourceRoot":"","sources":["../../../source/range/Contents.ts"],"names":[],"mappings":"AAMA,QAAA,MAAM,sBAAsB,UAAW,KAAK,WAsD3C,CAAC;AAIF,OAAO,EAAE,sBAAsB,EAAE,CAAC"}

View file

@ -1,6 +1,6 @@
{ {
"name": "squire-rte", "name": "squire-rte",
"version": "2.1.1", "version": "2.2.0",
"description": "Squire is an HTML5 rich text editor, which provides powerful cross-browser normalisation, whilst being supremely lightweight and flexible.", "description": "Squire is an HTML5 rich text editor, which provides powerful cross-browser normalisation, whilst being supremely lightweight and flexible.",
"main": "dist/squire.mjs", "main": "dist/squire.mjs",
"types": "dist/types/Squire.d.ts", "types": "dist/types/Squire.d.ts",