mirror of
https://github.com/penpot/penpot.git
synced 2025-05-08 14:36:06 +02:00
259 lines
6.2 KiB
JavaScript
259 lines
6.2 KiB
JavaScript
/**
|
|
* 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.js";
|
|
import {
|
|
isInline,
|
|
isLikeInline,
|
|
getInline,
|
|
getInlinesFrom,
|
|
createInline,
|
|
createEmptyInline,
|
|
isInlineEnd,
|
|
splitInline,
|
|
} from "./Inline.js";
|
|
import { createLineBreak, isLineBreak } from "./LineBreak.js";
|
|
import { setStyles } from "./Style.js";
|
|
import { createRandomId } from "./Element.js";
|
|
import { isEmptyTextNode, isTextNode } from './TextNode.js';
|
|
|
|
export const TAG = "DIV";
|
|
export const TYPE = "paragraph";
|
|
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"],
|
|
["text-align"],
|
|
["direction"]
|
|
];
|
|
|
|
/**
|
|
* FIXME: This is a fix for Chrome that removes the
|
|
* current inline when the last character is deleted
|
|
* in `insertCompositionText`.
|
|
*
|
|
* @param {*} node
|
|
*/
|
|
export function fixParagraph(node) {
|
|
if (!isParagraph(node) || !isLineBreak(node.firstChild)) {
|
|
return;
|
|
}
|
|
const br = createLineBreak();
|
|
node.replaceChildren(
|
|
createInline(br)
|
|
);
|
|
return br;
|
|
}
|
|
|
|
/**
|
|
* Returns true if the passed node behaves like a paragraph.
|
|
*
|
|
* NOTE: This is mainly used in paste operations. Every element node
|
|
* it's going to be treated as paragraph it
|
|
*
|
|
* @param {Node} element
|
|
* @returns {boolean}
|
|
*/
|
|
export function isLikeParagraph(element) {
|
|
return !isLikeInline(element);
|
|
}
|
|
|
|
/**
|
|
* Returns true if we have an empty paragraph.
|
|
*
|
|
* @param {Node} element
|
|
* @returns {boolean}
|
|
*/
|
|
export function isEmptyParagraph(element) {
|
|
if (!isParagraph(element)) throw new TypeError("Invalid paragraph");
|
|
const inline = element.firstChild;
|
|
if (!isInline(inline)) throw new TypeError("Invalid inline");
|
|
return isLineBreak(inline.firstChild);
|
|
}
|
|
|
|
/**
|
|
* Returns true if passed node is a paragraph.
|
|
*
|
|
* @param {Node} node
|
|
* @returns {boolean}
|
|
*/
|
|
export function isParagraph(node) {
|
|
if (!node) return false;
|
|
if (!isElement(node, TAG)) return false;
|
|
if (node.dataset.itype !== TYPE) return false;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Creates a new paragraph.
|
|
*
|
|
* @param {Array<HTMLDivElement>} inlines
|
|
* @param {Object.<string, *>|CSSStyleDeclaration} styles
|
|
* @param {Object.<string, *>} [attrs]
|
|
* @returns {HTMLDivElement}
|
|
*/
|
|
export function createParagraph(inlines, styles, attrs) {
|
|
if (inlines && (!Array.isArray(inlines) || !inlines.every(isInline)))
|
|
throw new TypeError("Invalid paragraph children");
|
|
return createElement(TAG, {
|
|
attributes: { id: createRandomId(), ...attrs },
|
|
data: { itype: TYPE },
|
|
styles: styles,
|
|
allowedStyles: STYLES,
|
|
children: inlines,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Returns a new empty paragraph
|
|
*
|
|
* @param {Object.<string, *>} styles
|
|
* @returns {HTMLDivElement}
|
|
*/
|
|
export function createEmptyParagraph(styles) {
|
|
return createParagraph([
|
|
createEmptyInline(styles)
|
|
], styles);
|
|
}
|
|
|
|
/**
|
|
* Sets the paragraph styles.
|
|
*
|
|
* @param {HTMLDivElement} element
|
|
* @param {Object.<string,*>|CSSStyleDeclaration} styles
|
|
* @returns {HTMLDivElement}
|
|
*/
|
|
export function setParagraphStyles(element, styles) {
|
|
return setStyles(element, STYLES, styles);
|
|
}
|
|
|
|
/**
|
|
* Gets the closest paragraph from a node.
|
|
*
|
|
* @param {Text|HTMLBRElement} node
|
|
* @returns {HTMLElement|null}
|
|
*/
|
|
export function getParagraph(node) {
|
|
if (!node) return null;
|
|
if (isParagraph(node)) return node;
|
|
if (node.nodeType === Node.TEXT_NODE
|
|
|| isLineBreak(node)) {
|
|
const paragraph = node?.parentElement?.parentElement;
|
|
if (!paragraph) {
|
|
return null;
|
|
}
|
|
if (!isParagraph(paragraph)) {
|
|
return null;
|
|
}
|
|
return paragraph;
|
|
}
|
|
return node.closest(QUERY);
|
|
}
|
|
|
|
/**
|
|
* Returns if the specified node and offset represents
|
|
* the start of the paragraph.
|
|
*
|
|
* @param {Text|HTMLBRElement} node
|
|
* @param {number} offset
|
|
* @returns {boolean}
|
|
*/
|
|
export function isParagraphStart(node, offset) {
|
|
const paragraph = getParagraph(node);
|
|
if (!paragraph) throw new Error("Can't find the paragraph");
|
|
const inline = getInline(node);
|
|
if (!inline) throw new Error("Can't find the inline");
|
|
return (
|
|
paragraph.firstElementChild === inline &&
|
|
isOffsetAtStart(inline.firstChild, offset)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns if the specified node and offset represents
|
|
* the end of the paragraph.
|
|
*
|
|
* @param {Text|HTMLBRElement} node
|
|
* @param {number} offset
|
|
* @returns {boolean}
|
|
*/
|
|
export function isParagraphEnd(node, offset) {
|
|
const paragraph = getParagraph(node);
|
|
if (!paragraph) throw new Error("Cannot find the paragraph");
|
|
const inline = getInline(node);
|
|
if (!inline) throw new Error("Cannot find the inline");
|
|
return (
|
|
paragraph.lastElementChild === inline &&
|
|
isOffsetAtEnd(inline.firstChild, offset)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Splits a paragraph.
|
|
*
|
|
* @param {HTMLDivElement} paragraph
|
|
* @param {HTMLSpanElement} inline
|
|
* @param {number} offset
|
|
*/
|
|
export function splitParagraph(paragraph, inline, offset) {
|
|
const style = paragraph.style;
|
|
if (isInlineEnd(inline, offset)) {
|
|
const newParagraph = createParagraph(getInlinesFrom(inline), style);
|
|
return newParagraph;
|
|
}
|
|
const newInline = splitInline(inline, offset);
|
|
const newParagraph = createParagraph([newInline], style);
|
|
return newParagraph;
|
|
}
|
|
|
|
/**
|
|
* Splits a paragraph at a specified child node index
|
|
*
|
|
* @param {HTMLDivElement} paragraph
|
|
* @param {number} startIndex
|
|
*/
|
|
export function splitParagraphAtNode(paragraph, startIndex) {
|
|
const style = paragraph.style;
|
|
const newParagraph = createParagraph(null, style);
|
|
const newInlines = [];
|
|
for (let index = startIndex; index < paragraph.children.length; index++) {
|
|
newInlines.push(paragraph.children.item(index));
|
|
}
|
|
newParagraph.append(...newInlines);
|
|
return newParagraph;
|
|
}
|
|
|
|
/**
|
|
* Merges two paragraphs.
|
|
*
|
|
* @param {HTMLDivElement} a
|
|
* @param {HTMLDivElement} b
|
|
* @returns {HTMLDivElement}
|
|
*/
|
|
export function mergeParagraphs(a, b) {
|
|
a.append(...b.children);
|
|
b.remove();
|
|
return a;
|
|
}
|