🎉 Add an option to enable and disable HTML paste

This commit is contained in:
Aitor Moreno
2025-11-03 10:23:58 +01:00
parent 0a3fe9836a
commit d73be5832b
8 changed files with 109 additions and 19 deletions

View File

@@ -22,6 +22,13 @@ import { createEmptyTextSpan, createTextSpan } from "./content/dom/TextSpan.js";
import { isLineBreak } from "./content/dom/LineBreak.js";
import LayoutType from "./layout/LayoutType.js";
/**
* @typedef {Object} TextEditorOptions
* @property {CSSStyleDeclaration|Object.<string,*>} [styleDefaults]
* @property {SelectionControllerDebug} [debug]
* @property {boolean} [allowHTMLPaste=false]
*/
/**
* Text Editor.
*/
@@ -73,6 +80,8 @@ export class TextEditor extends EventTarget {
* `beforeinput` and `input` have different `data` when
* characters are deleted when the input type is
* `insertCompositionText`.
*
* @type {boolean}
*/
#fixInsertCompositionText = false;
@@ -88,6 +97,7 @@ export class TextEditor extends EventTarget {
*
* @param {HTMLElement} element
* @param {HTMLCanvasElement} canvas
* @param {TextEditorOptions} [options]
*/
constructor(element, canvas, options) {
super();
@@ -578,14 +588,26 @@ export class TextEditor extends EventTarget {
}
}
export function createRootFromHTML(html, style = undefined) {
const fragment = mapContentFragmentFromHTML(html, style || undefined);
/**
*
* @param {string} html
* @param {*} style
* @param {boolean} allowHTMLPaste
* @returns {Root}
*/
export function createRootFromHTML(html, style = undefined, allowHTMLPaste = undefined) {
const fragment = mapContentFragmentFromHTML(html, style || undefined, allowHTMLPaste || undefined);
const root = createRoot([], style);
root.replaceChildren(fragment);
resetInertElement();
return root;
}
/**
*
* @param {string} string
* @returns {Root}
*/
export function createRootFromString(string) {
const fragment = mapContentFragmentFromString(string);
const root = createRoot([]);

View File

@@ -11,6 +11,48 @@ import {
mapContentFragmentFromString,
} from "../content/dom/Content.js";
/**
* Returns a DocumentFragment from text/html.
*
* @param {DataTransfer} clipboardData
* @returns {DocumentFragment}
*/
function getFormattedFragmentFromClipboardData(clipboardData) {
return mapContentFragmentFromHTML(
clipboardData.getData("text/html"),
selectionController.currentStyle,
);
}
/**
* Returns a DocumentFragment from text/plain.
*
* @param {DataTransfer} clipboardData
* @returns {DocumentFragment}
*/
function getPlainFragmentFromClipboardData(clipboardData) {
return mapContentFragmentFromString(
clipboardData.getData("text/plain"),
selectionController.currentStyle,
);
}
/**
* Returns a DocumentFragment (or null) if it contains
* a compatible clipboardData type.
*
* @param {DataTransfer} clipboardData
* @returns {DocumentFragment|null}
*/
function getFragmentFromClipboardData(clipboardData) {
if (clipboardData.types.includes("text/html")) {
return getFormattedFragmentFromClipboardData(clipboardData)
} else if (clipboardData.types.includes("text/plain")) {
return getPlainFragmentFromClipboardData(clipboardData)
}
return null
}
/**
* When the user pastes some HTML, what we do is generate
* a new DOM based on what the user pasted and then we
@@ -28,18 +70,10 @@ export function paste(event, editor, selectionController) {
event.preventDefault();
let fragment = null;
if (event.clipboardData.types.includes("text/html")) {
const html = event.clipboardData.getData("text/html");
fragment = mapContentFragmentFromHTML(
html,
selectionController.currentStyle,
);
} else if (event.clipboardData.types.includes("text/plain")) {
const plain = event.clipboardData.getData("text/plain");
fragment = mapContentFragmentFromString(
plain,
selectionController.currentStyle,
);
if (editor?.options?.allowHTMLPaste) {
fragment = getFragmentFromClipboardData(event.clipboardData);
} else {
fragment = getPlainFragmentFromClipboardData(event.clipboardData);
}
if (!fragment) {

View File

@@ -167,9 +167,21 @@ export function htmlToText(html) {
*
* @param {string} html
* @param {CSSStyleDeclaration} [styleDefaults]
* @param {boolean} [allowHTMLPaste=false]
* @returns {DocumentFragment}
*/
export function mapContentFragmentFromHTML(html, styleDefaults) {
export function mapContentFragmentFromHTML(html, styleDefaults, allowHTMLPaste) {
if (allowHTMLPaste) {
try {
const parser = new DOMParser()
const document = parser.parseFromString(html, "text/html");
return mapContentFragmentFromDocument(document, styleDefaults);
} catch (error) {
console.error("Couldn't parse HTML", html, error);
const plainText = htmlToText(html);
return mapContentFragmentFromString(plainText, styleDefaults);
}
}
const plainText = htmlToText(html);
return mapContentFragmentFromString(plainText, styleDefaults);
}

View File

@@ -58,7 +58,7 @@ import SafeGuard from "./SafeGuard.js";
* Supported options for the SelectionController.
*
* @typedef {Object} SelectionControllerOptions
* @property {Object} [debug] An object with references to DOM elements that will keep all the debugging values.
* @property {SelectionControllerDebug} [debug] An object with references to DOM elements that will keep all the debugging values.
*/
/**
@@ -203,6 +203,11 @@ export class SelectionController extends EventTarget {
*/
#fixInsertCompositionText = false;
/**
* @type {TextEditorOptions}
*/
#options
/**
* Constructor
*
@@ -219,6 +224,7 @@ export class SelectionController extends EventTarget {
throw new TypeError("Invalid EventTarget");
}
*/
this.#options = options;
this.#debug = options?.debug;
this.#styleDefaults = options?.styleDefaults;
this.#selection = selection;
@@ -238,6 +244,15 @@ export class SelectionController extends EventTarget {
return this.#currentStyle;
}
/**
* Text editor options.
*
* @type {TextEditorOptions}
*/
get options() {
return this.#options;
}
/**
* Applies the default styles to the currentStyle
* CSSStyleDeclaration.