mirror of
https://github.com/penpot/penpot.git
synced 2025-12-11 22:14:05 +01:00
🎉 Add an option to enable and disable HTML paste
This commit is contained in:
@@ -3,5 +3,6 @@
|
||||
"paths": {
|
||||
"~/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"include": ["./src/**/*.js"]
|
||||
}
|
||||
|
||||
@@ -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([]);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user