mirror of
https://github.com/penpot/penpot.git
synced 2025-05-11 04:06:39 +02:00
✨ Import text-editor code into the repository
This commit is contained in:
parent
68397edd4d
commit
04a0d867b0
65 changed files with 11112 additions and 7 deletions
272
frontend/text-editor/editor/content/dom/Inline.js
Normal file
272
frontend/text-editor/editor/content/dom/Inline.js
Normal file
|
@ -0,0 +1,272 @@
|
|||
/**
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Copyright (c) KALEIDOS INC
|
||||
*/
|
||||
|
||||
import {
|
||||
createElement,
|
||||
isElement,
|
||||
isOffsetAtStart,
|
||||
isOffsetAtEnd,
|
||||
} from "./Element";
|
||||
import { createLineBreak, isLineBreak } from "./LineBreak";
|
||||
import { setStyles, mergeStyles } from "./Style";
|
||||
import { createRandomId } from "./Element";
|
||||
|
||||
export const TAG = "SPAN";
|
||||
export const TYPE = "inline";
|
||||
export const QUERY = `[data-itype="${TYPE}"]`;
|
||||
export const STYLES = [
|
||||
["--typography-ref-id"],
|
||||
["--typography-ref-file"],
|
||||
["--font-id"],
|
||||
["--font-variant-id"],
|
||||
["--fills"],
|
||||
["font-variant"],
|
||||
["font-family"],
|
||||
["font-size", "px"],
|
||||
["font-weight"],
|
||||
["font-style"],
|
||||
["line-height"],
|
||||
["letter-spacing", "px"],
|
||||
["text-decoration"],
|
||||
["text-transform"],
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns true if passed node is an inline.
|
||||
*
|
||||
* @param {Node} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isInline(node) {
|
||||
if (!node) return false;
|
||||
if (!isElement(node, TAG)) return false;
|
||||
if (node.dataset.itype !== TYPE) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the passed node "behaves" like an
|
||||
* inline.
|
||||
*
|
||||
* @param {Node} element
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isLikeInline(element) {
|
||||
return element
|
||||
? [
|
||||
"A",
|
||||
"ABBR",
|
||||
"ACRONYM",
|
||||
"B",
|
||||
"BDO",
|
||||
"BIG",
|
||||
"BR",
|
||||
"BUTTON",
|
||||
"CITE",
|
||||
"CODE",
|
||||
"DFN",
|
||||
"EM",
|
||||
"I",
|
||||
"IMG",
|
||||
"INPUT",
|
||||
"KBD",
|
||||
"LABEL",
|
||||
"MAP",
|
||||
"OBJECT",
|
||||
"OUTPUT",
|
||||
"Q",
|
||||
"SAMP",
|
||||
"SCRIPT",
|
||||
"SELECT",
|
||||
"SMALL",
|
||||
"SPAN",
|
||||
"STRONG",
|
||||
"SUB",
|
||||
"SUP",
|
||||
"TEXTAREA",
|
||||
"TIME",
|
||||
"TT",
|
||||
"VAR",
|
||||
].includes(element.nodeName)
|
||||
: false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Inline
|
||||
*
|
||||
* @param {Text|HTMLBRElement} text
|
||||
* @param {Object.<string, *>|CSSStyleDeclaration} styles
|
||||
* @param {Object.<string, *>} [attrs]
|
||||
* @returns {HTMLSpanElement}
|
||||
*/
|
||||
export function createInline(textOrLineBreak, styles, attrs) {
|
||||
if (
|
||||
!(textOrLineBreak instanceof HTMLBRElement) &&
|
||||
!(textOrLineBreak instanceof Text)
|
||||
) {
|
||||
throw new TypeError("Invalid inline child");
|
||||
}
|
||||
if (textOrLineBreak instanceof Text
|
||||
&& textOrLineBreak.nodeValue.length === 0) {
|
||||
console.trace("nodeValue", textOrLineBreak.nodeValue)
|
||||
throw new TypeError("Invalid inline child, cannot be an empty text");
|
||||
}
|
||||
return createElement(TAG, {
|
||||
attributes: { id: createRandomId(), ...attrs },
|
||||
data: { itype: TYPE },
|
||||
styles: styles,
|
||||
allowedStyles: STYLES,
|
||||
children: textOrLineBreak,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new inline from an older inline. This only
|
||||
* merges styles from the older inline to the new inline.
|
||||
*
|
||||
* @param {HTMLSpanElement} inline
|
||||
* @param {Object.<string, *>} textOrLineBreak
|
||||
* @param {Object.<string, *>|CSSStyleDeclaration} styles
|
||||
* @param {Object.<string, *>} [attrs]
|
||||
* @returns {HTMLSpanElement}
|
||||
*/
|
||||
export function createInlineFrom(inline, textOrLineBreak, styles, attrs) {
|
||||
return createInline(
|
||||
textOrLineBreak,
|
||||
mergeStyles(STYLES, inline.style, styles),
|
||||
attrs
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new empty inline.
|
||||
*
|
||||
* @param {Object.<string,*>|CSSStyleDeclaration} styles
|
||||
* @returns {HTMLSpanElement}
|
||||
*/
|
||||
export function createEmptyInline(styles) {
|
||||
return createInline(createLineBreak(), styles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the inline styles.
|
||||
*
|
||||
* @param {HTMLSpanElement} element
|
||||
* @param {Object.<string,*>|CSSStyleDeclaration} styles
|
||||
* @returns {HTMLSpanElement}
|
||||
*/
|
||||
export function setInlineStyles(element, styles) {
|
||||
return setStyles(element, STYLES, styles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the closest inline from a node.
|
||||
*
|
||||
* @param {Node} node
|
||||
* @returns {HTMLElement|null}
|
||||
*/
|
||||
export function getInline(node) {
|
||||
if (!node) return null; // FIXME: Should throw?
|
||||
if (isInline(node)) return node;
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
const inline = node?.parentElement;
|
||||
if (!inline) return null;
|
||||
if (!isInline(inline)) return null;
|
||||
return inline;
|
||||
}
|
||||
return node.closest(QUERY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if we are at the start offset
|
||||
* of an inline.
|
||||
*
|
||||
* NOTE: Only the first inline returns this as true
|
||||
*
|
||||
* @param {TextNode|HTMLBRElement} node
|
||||
* @param {number} offset
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isInlineStart(node, offset) {
|
||||
const inline = getInline(node);
|
||||
if (!inline) return false;
|
||||
return isOffsetAtStart(inline, offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if we are at the end offset
|
||||
* of an inline.
|
||||
*
|
||||
* @param {TextNode|HTMLBRElement} node
|
||||
* @param {number} offset
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isInlineEnd(node, offset) {
|
||||
const inline = getInline(node);
|
||||
if (!inline) return false;
|
||||
return isOffsetAtEnd(inline.firstChild, offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits an inline.
|
||||
*
|
||||
* @param {HTMLSpanElement} inline
|
||||
* @param {number} offset
|
||||
*/
|
||||
export function splitInline(inline, offset) {
|
||||
const textNode = inline.firstChild;
|
||||
const style = inline.style;
|
||||
const newTextNode = textNode.splitText(offset);
|
||||
return createInline(newTextNode, style);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the inlines of a paragraph starting at
|
||||
* the specified inline.
|
||||
*
|
||||
* @param {HTMLSpanElement} startInline
|
||||
* @returns {Array<HTMLSpanElement>}
|
||||
*/
|
||||
export function getInlinesFrom(startInline) {
|
||||
const inlines = [];
|
||||
let currentInline = startInline;
|
||||
let index = 0;
|
||||
while (currentInline) {
|
||||
if (index > 0) inlines.push(currentInline);
|
||||
currentInline = currentInline.nextElementSibling;
|
||||
index++;
|
||||
}
|
||||
return inlines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the length of an inline.
|
||||
*
|
||||
* @param {HTMLElement} inline
|
||||
* @returns {number}
|
||||
*/
|
||||
export function getInlineLength(inline) {
|
||||
if (!isInline(inline)) throw new Error("Invalid inline");
|
||||
if (isLineBreak(inline.firstChild)) return 0;
|
||||
return inline.firstChild.nodeValue.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges two inlines.
|
||||
*
|
||||
* @param {HTMLSpanElement} a
|
||||
* @param {HTMLSpanElement} b
|
||||
* @returns {HTMLSpanElement}
|
||||
*/
|
||||
export function mergeInlines(a, b) {
|
||||
a.append(...b.childNodes);
|
||||
b.remove();
|
||||
// We need to normalize Text nodes.
|
||||
a.normalize();
|
||||
return a;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue