inital commit

This commit is contained in:
2026-01-01 15:25:19 +05:30
commit f0ae49465a
36361 changed files with 4894111 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
export * from "./move-node-inside-start";
export * from "./move-the-node-along-the-edge-outward";
/**
* Check if the cursor is at the edge of the string
* @private
*/
export declare function cursorInTheEdgeOfString(container: Node, offset: number, start: boolean, end: boolean): boolean;
export declare function findCorrectCurrentNode(node: Node, range: Range, rightMode: boolean, isCollapsed: boolean, checkChild: boolean, child: (nd: Node) => Node | null): {
node: Node;
rightMode: boolean;
};

59
node_modules/jodit/esm/core/selection/helpers/index.js generated vendored Normal file
View File

@@ -0,0 +1,59 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
/**
* @module selection
*/
import { INVISIBLE_SPACE_REG_EXP_END as INV_END, INVISIBLE_SPACE_REG_EXP_START as INV_START } from "../../constants.js";
import { Dom } from "../../dom/dom.js";
export * from "./move-node-inside-start.js";
export * from "./move-the-node-along-the-edge-outward.js";
/**
* Check if the cursor is at the edge of the string
* @private
*/
export function cursorInTheEdgeOfString(container, offset, start, end) {
var _a;
const text = ((_a = container.nodeValue) === null || _a === void 0 ? void 0 : _a.length) ? container.nodeValue : '';
if (end && text.replace(INV_END(), '').length > offset) {
return true;
}
const inv = INV_START().exec(text);
return start && ((inv && inv[0].length < offset) || (!inv && offset > 0));
}
export function findCorrectCurrentNode(node, range, rightMode, isCollapsed, checkChild, child) {
node = range.startContainer.childNodes[range.startOffset];
if (!node) {
node = range.startContainer.childNodes[range.startOffset - 1];
rightMode = true;
}
if (node && isCollapsed && !Dom.isText(node)) {
// test Current method - Cursor in the left of some SPAN
if (!rightMode && Dom.isText(node.previousSibling)) {
node = node.previousSibling;
}
else if (checkChild) {
let current = child(node);
while (current) {
if (current && Dom.isText(current)) {
node = current;
break;
}
current = child(current);
}
}
}
if (node && !isCollapsed && !Dom.isText(node)) {
let leftChild = node, rightChild = node;
do {
leftChild = leftChild.firstChild;
rightChild = rightChild.lastChild;
} while (leftChild && rightChild && !Dom.isText(leftChild));
if (leftChild === rightChild && leftChild && Dom.isText(leftChild)) {
node = leftChild;
}
}
return { node, rightMode };
}

View File

@@ -0,0 +1,15 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
/**
* @module selection
*/
import type { IJodit } from "../../../types/index";
/**
* Moves the fake node inside the adjacent element if it lies next to it but not inside.
* When the cursor is positioned in its place, it must be inside the element and not outside its border.
* @private
*/
export declare function moveNodeInsideStart(j: IJodit, node: Node, start: boolean): void;

View File

@@ -0,0 +1,28 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import { INSEPARABLE_TAGS } from "../../constants.js";
import { Dom } from "../../dom/dom.js";
/**
* Moves the fake node inside the adjacent element if it lies next to it but not inside.
* When the cursor is positioned in its place, it must be inside the element and not outside its border.
* @private
*/
export function moveNodeInsideStart(j, node, start) {
let sibling = Dom.findSibling(node, start), anotherSibling = Dom.findSibling(node, !start);
while (Dom.isElement(sibling) &&
!Dom.isTag(sibling, INSEPARABLE_TAGS) &&
Dom.isContentEditable(sibling, j.editor) &&
(!anotherSibling || !Dom.closest(node, Dom.isElement, j.editor))) {
if (start || !sibling.firstChild) {
sibling.appendChild(node);
}
else {
Dom.before(sibling.firstChild, node);
}
sibling = Dom.sibling(node, start);
anotherSibling = Dom.sibling(node, !start);
}
}

View File

@@ -0,0 +1,10 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
/**
* Moves the fake node up until it encounters a non-empty sibling on the left(right)
* @private
*/
export declare function moveTheNodeAlongTheEdgeOutward(node: Node, start: boolean, root: HTMLElement): void;

View File

@@ -0,0 +1,27 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import { Dom } from "../../dom/dom.js";
/**
* Moves the fake node up until it encounters a non-empty sibling on the left(right)
* @private
*/
export function moveTheNodeAlongTheEdgeOutward(node, start, root) {
let item = node;
while (item && item !== root) {
const sibling = Dom.findSibling(item, start);
if (sibling) {
return;
}
if (Dom.isBlock(item.parentElement)) {
break;
}
item = item.parentElement;
if (item && item !== root) {
start ? Dom.before(item, node) : Dom.after(item, node);
}
}
return;
}

11
node_modules/jodit/esm/core/selection/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,11 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
/**
* @module selection
*/
export * from "./selection";
export * from "./style/commit-style";
export * from "./style/constants";

11
node_modules/jodit/esm/core/selection/index.js generated vendored Normal file
View File

@@ -0,0 +1,11 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
/**
* @module selection
*/
export * from "./selection.js";
export * from "./style/commit-style.js";
export * from "./style/constants.js";

24
node_modules/jodit/esm/core/selection/interface.d.ts generated vendored Normal file
View File

@@ -0,0 +1,24 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
/**
* @module selection
*/
import type { ICommitStyle } from "../../types/index";
declare module 'jodit/types/events' {
interface IEventEmitter {
/**
* The cursorInTheEdge method checks whether the cursor is at the beginning or at the end of the element,
* this event allows you to override the logic
* determining whether the element before/after the cursor is significant for its position
* true - element is not significant
*/
on(event: 'isInvisibleForCursor', callback: (elm: HTMLElement) => void | true): this;
/**
* Triggered after the style is applied to the element
*/
on(event: 'afterCommitStyle', style: ICommitStyle): this;
}
}

6
node_modules/jodit/esm/core/selection/interface.js generated vendored Normal file
View File

@@ -0,0 +1,6 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
export {};

248
node_modules/jodit/esm/core/selection/selection.d.ts generated vendored Normal file
View File

@@ -0,0 +1,248 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
/**
* [[include:core/selection/README.md]]
* @packageDocumentation
* @module selection
*/
import type { HTMLTagNames, IDictionary, IJodit, ISelect, IStyleOptions, MarkerInfo, Nullable } from "../../types/index";
import "./interface";
export declare class Selection implements ISelect {
readonly jodit: IJodit;
constructor(jodit: IJodit);
/**
* Short alias for this.jodit
*/
private get j();
/**
* Throw Error exception if parameter is not Node
*/
private errorNode;
/**
* Return current work place - for Jodit is Editor
*/
private get area();
/**
* Editor Window - it can be different for iframe mode
*/
private get win();
/**
* Current jodit editor doc
*/
private get doc();
/**
* Return current selection object
*/
get sel(): ISelect['sel'];
/**
* Return first selected range or create new
*/
get range(): Range;
/**
* Checks if the selected text is currently inside the editor
*/
get isInsideArea(): boolean;
/**
* Return current selection object
* @param select - Immediately add in selection
*/
createRange(select?: boolean): Range;
/**
* Remove all selected content
*/
remove(): void;
/**
* Clear all selection
*/
clear(): void;
/**
* Remove node element from editor
*/
removeNode(node: Node): void;
/**
* Insert the cursor to any point x, y
*
* @param x - Coordinate by horizontal
* @param y - Coordinate by vertical
* @returns false - Something went wrong
*/
insertCursorAtPoint(x: number, y: number): boolean;
/**
* Check if editor has selection markers
*/
get hasMarkers(): boolean;
/**
* Check if editor has selection markers
*/
get markers(): HTMLElement[];
/**
* Remove all markers
*/
removeMarkers(): void;
/**
* Create marker element
*/
marker(atStart?: boolean, range?: Range): HTMLSpanElement;
/**
* Restores user selections using marker invisible elements in the DOM.
*/
restore(): void;
fakes(): [
] | [
Node
] | [
Node,
Node
];
restoreFakes(fakes: [
] | [
Node
] | [
Node,
Node
]): void;
/**
* Saves selections using marker invisible elements in the DOM.
* @param silent - Do not change current range
*/
save(silent?: boolean): MarkerInfo[];
/**
* Set focus in editor
*/
focus(options?: FocusOptions): boolean;
/**
* Checks whether the current selection is something or just set the cursor is
* @returns true Selection does't have content
*/
isCollapsed(): boolean;
/**
* Checks whether the editor currently in focus
*/
isFocused(): boolean;
/**
* Returns the current element under the cursor inside editor
*/
current(checkChild?: boolean): Nullable<Node>;
/**
* Insert element in editor
*
* @param node - Node for insert
* @param insertCursorAfter - After insert, cursor will move after element
* @param fireChange - After insert, editor fire change event. You can prevent this behavior
*/
insertNode(node: Node, insertCursorAfter?: boolean, fireChange?: boolean): void;
/**
* Inserts in the current cursor position some HTML snippet
*
* @param html - HTML The text to be inserted into the document
* @param insertCursorAfter - After insert, cursor will move after element
* @example
* ```javascript
* parent.s.insertHTML('<img src="image.png"/>');
* ```
*/
insertHTML(html: number | string | Node, insertCursorAfter?: boolean): void;
/**
* Insert image in editor
*
* @param url - URL for image, or HTMLImageElement
* @param styles - If specified, it will be applied <code>$(image).css(styles)</code>
* @param defaultWidth - If specified, it will be applied <code>css('width', defaultWidth)</code>
*/
insertImage(url: string | HTMLImageElement, styles?: Nullable<IDictionary<string>>, defaultWidth?: Nullable<number | string>): void;
/**
* Call callback for all selection node
*/
eachSelection(callback: (current: Node) => void): void;
/**
* Checks if the cursor is at the end(start) block
*
* @param start - true - check whether the cursor is at the start block
* @param parentBlock - Find in this
* @param fake - Node for cursor position
*
* @returns true - the cursor is at the end(start) block, null - cursor somewhere outside
*/
cursorInTheEdge(start: boolean, parentBlock: HTMLElement, fake?: Node | null): Nullable<boolean>;
/**
* Wrapper for cursorInTheEdge
*/
cursorOnTheLeft(parentBlock: HTMLElement, fake?: Node | null): Nullable<boolean>;
/**
* Wrapper for cursorInTheEdge
*/
cursorOnTheRight(parentBlock: HTMLElement, fake?: Node | null): Nullable<boolean>;
/**
* Set cursor after the node
* @returns fake invisible textnode. After insert it can be removed
*/
setCursorAfter(node: Node): Nullable<Text>;
/**
* Set cursor before the node
* @returns fake invisible textnode. After insert it can be removed
*/
setCursorBefore(node: Node): Nullable<Text>;
/**
* Add fake node for new cursor position
*/
private setCursorNearWith;
/**
* Set cursor in the node
* @param node - Node element
* @param inStart - set cursor in start of element
*/
setCursorIn(node: Node, inStart?: boolean): Node;
/**
* Set range selection
*/
selectRange(range: Range, focus?: boolean): this;
/**
* Select node
* @param node - Node element
* @param inward - select all inside
*/
select(node: Node | HTMLElement | HTMLTableElement | HTMLTableCellElement, inward?: boolean): this;
/**
* Return current selected HTML
* @example
* ```javascript
* const editor = Jodit.make();
* console.log(editor.s.html); // html
* console.log(Jodit.modules.Helpers.stripTags(editor.s.html)); // plain text
* ```
*/
get html(): string;
/**
* Wrap all selected fragments inside Tag or apply some callback
*/
wrapInTagGen(fakes?: Node[]): Generator<HTMLElement, undefined>;
/**
* Wrap all selected fragments inside Tag or apply some callback
*/
wrapInTag(tagOrCallback: HTMLTagNames | ((font: HTMLElement) => any)): HTMLElement[];
/**
* Apply some css rules for all selections. It method wraps selections in nodeName tag.
* @example
* ```js
* const editor = Jodit.make('#editor');
* editor.value = 'test';
* editor.execCommand('selectall');
*
* editor.s.commitStyle({
* style: {color: 'red'}
* }) // will wrap `text` in `span` and add style `color:red`
* editor.s.commitStyle({
* style: {color: 'red'}
* }) // will remove `color:red` from `span`
* ```
*/
commitStyle(options: IStyleOptions): void;
/**
* Split selection on two parts: left and right
*/
splitSelection(currentBox: HTMLElement, edge?: Node): Nullable<Element>;
expandSelection(): this;
}

1151
node_modules/jodit/esm/core/selection/selection.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import type { IJodit } from "../../../../types/index";
/**
* If the selection area is inside an element that matches the commit (suitable relative),
* but does not completely fill it.
* Then the method cuts the parent and leaves itself in a copy of the parent (suitable relative) in the middle.
*
* @example
* Apply strong to
* ```html
* <strong><span>some<font>SELECTED</font>text</span></strong>
* ```
* Should extract selection from parent `strong`
* ```html
* `<strong><span>some</span></strong><strong><span><font>SELECTED</font></span></strong><strong><span>test</span></strong>
* ```
* @private
*/
export declare function extractSelectedPart(wrapper: HTMLElement, font: HTMLElement, jodit: IJodit): void;

View File

@@ -0,0 +1,54 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import { Dom } from "../../../dom/dom.js";
import { isMarker } from "../../../helpers/checker/is-marker.js";
import { trim } from "../../../helpers/string/trim.js";
import { call } from "../../../helpers/utils/utils.js";
/**
* If the selection area is inside an element that matches the commit (suitable relative),
* but does not completely fill it.
* Then the method cuts the parent and leaves itself in a copy of the parent (suitable relative) in the middle.
*
* @example
* Apply strong to
* ```html
* <strong><span>some<font>SELECTED</font>text</span></strong>
* ```
* Should extract selection from parent `strong`
* ```html
* `<strong><span>some</span></strong><strong><span><font>SELECTED</font></span></strong><strong><span>test</span></strong>
* ```
* @private
*/
export function extractSelectedPart(wrapper, font, jodit) {
const range = jodit.s.createRange();
// Left part
const leftEdge = isMarker(font.previousSibling)
? font.previousSibling
: font;
range.setStartBefore(wrapper);
range.setEndBefore(leftEdge);
extractAndMove(wrapper, range, true);
// Right part
const rightEdge = isMarker(font.nextSibling) ? font.nextSibling : font;
range.setStartAfter(rightEdge);
range.setEndAfter(wrapper);
extractAndMove(wrapper, range, false);
}
/**
* Retrieves content before after the selected area, clears it if it is empty, and inserts before after the framed selection
* @private
*/
function extractAndMove(wrapper, range, left) {
const fragment = range.extractContents();
if ((!fragment.textContent || !trim(fragment.textContent).length) &&
fragment.firstChild) {
Dom.unwrap(fragment.firstChild);
}
if (wrapper.parentNode) {
call(left ? Dom.before : Dom.after, wrapper, fragment);
}
}

View File

@@ -0,0 +1,22 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import type { IDictionary } from "../../../../types/index";
/**
* A state machine implementation for applying styles.
*/
export declare class FiniteStateMachine<K extends string, V extends object & {
next: K;
}, T extends IDictionary<IDictionary<(value: V) => V>, K> = IDictionary<IDictionary<(...attrs: any[]) => any>, K>, A extends keyof T[K] = keyof T[K]> {
private readonly transitions;
private __state;
private setState;
getState(): K;
private silent;
disableSilent(): void;
private __previewsStates;
constructor(state: K, transitions: T);
dispatch(actionName: A, value: V): V;
}

View File

@@ -0,0 +1,46 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import { IS_PROD } from "../../../constants.js";
import { isString } from "../../../helpers/checker/is-string.js";
import { assert } from "../../../helpers/utils/assert.js";
/**
* A state machine implementation for applying styles.
*/
export class FiniteStateMachine {
setState(state) {
assert(!this.__previewsStates.has(state), 'Circled states');
this.__previewsStates.add(state);
this.__state = state;
}
getState() {
return this.__state;
}
disableSilent() {
this.silent = false;
}
constructor(state, transitions) {
this.transitions = transitions;
this.silent = true;
this.__previewsStates = new Set();
this.setState(state);
}
dispatch(actionName, value) {
const action = this.transitions[this.getState()][actionName];
if (action) {
const res = action.call(this, value);
assert(res && res !== value, 'Action should return new value');
assert(isString(res.next), 'Value should contain the next state');
assert(res.next !== this.getState(), 'The new state should not be equal to the old one.');
this.setState(res.next);
if (!IS_PROD && !this.silent) {
// eslint-disable-next-line no-console
console.log(`State: ${this.getState()}`);
}
return res;
}
throw new Error(`invalid action: ${this.getState()}.${actionName.toString()}`);
}
}

View File

@@ -0,0 +1,15 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import type { ICommitStyle, Nullable } from "../../../../types/index";
/**
* Checks if child elements are suitable for applying styles.
* An element is suitable for us only if it is the only significant child.
* If the child matches then returns it.
* @example
* `<font><strong>selected</strong></font>`
* @private
*/
export declare function getSuitChild(style: ICommitStyle, font: HTMLElement): Nullable<HTMLElement>;

View File

@@ -0,0 +1,31 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import { Dom } from "../../../dom/dom.js";
import { isNormalNode } from "./is-normal-node.js";
import { isSuitElement } from "./is-suit-element.js";
/**
* Checks if child elements are suitable for applying styles.
* An element is suitable for us only if it is the only significant child.
* If the child matches then returns it.
* @example
* `<font><strong>selected</strong></font>`
* @private
*/
export function getSuitChild(style, font) {
let { firstChild: child } = font;
while (child && !isNormalNode(child)) {
child = child.nextSibling;
if (!child) {
return null;
}
}
if (child &&
!Dom.next(child, isNormalNode, font) &&
isSuitElement(style, child, false)) {
return child;
}
return null;
}

View File

@@ -0,0 +1,15 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import type { ICommitStyle, Nullable } from "../../../../types/index";
/**
* Checks if the parent of an element is suitable for applying styles, if applicable, then returns the parent *
*
* @param style - styles to be applied
* @param node - checked item
* @param root - editor root
* @private
*/
export declare function getSuitParent(style: ICommitStyle, node: Node, root: Node): Nullable<HTMLElement>;

View File

@@ -0,0 +1,40 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import { Dom } from "../../../dom/dom.js";
import { isNormalNode } from "./is-normal-node.js";
import { isSuitElement } from "./is-suit-element.js";
/**
* Checks if the parent of an element is suitable for applying styles, if applicable, then returns the parent *
*
* @param style - styles to be applied
* @param node - checked item
* @param root - editor root
* @private
*/
export function getSuitParent(style, node, root) {
const { parentNode } = node;
if (parentNode === root ||
!Dom.isHTMLElement(parentNode) ||
Dom.next(node, isNormalNode, parentNode) ||
Dom.prev(node, isNormalNode, parentNode)) {
return null;
}
// <h3><span style="color:red">|test|</span></h3> => apply <h2>
if (style.isElementCommit &&
style.elementIsBlock &&
!Dom.isBlock(parentNode)) {
return getSuitParent(style, parentNode, root);
}
if (isSuitElement(style, parentNode, false) &&
(!Dom.isBlock(parentNode) || style.elementIsBlock)) {
return parentNode;
}
// <strong style="color:red"><em>|test|</em></strong> => apply <strong>
if (style.isElementCommit && !Dom.isBlock(parentNode)) {
return getSuitParent(style, parentNode, root);
}
return null;
}

View File

@@ -0,0 +1,15 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import type { IStyle } from "../../../../types/index";
/**
* Element has the same styles as in the commit
* @private
*/
export declare function hasSameStyle(elm: Node, rules: IStyle): boolean;
/**
* Element has the similar styles keys
*/
export declare function hasSameStyleKeys(elm: Node, rules: IStyle): boolean;

View File

@@ -0,0 +1,57 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import { globalDocument } from "../../../constants.js";
import { Dom } from "../../../dom/dom.js";
import { isVoid } from "../../../helpers/checker/is-void.js";
import { normalizeCssValue } from "../../../helpers/normalize/normalize-css-value.js";
import { assert } from "../../../helpers/utils/assert.js";
import { css } from "../../../helpers/utils/css.js";
/**
* Element has the same styles as in the commit
* @private
*/
export function hasSameStyle(elm, rules) {
return Boolean(!Dom.isTag(elm, 'font') &&
Dom.isHTMLElement(elm) &&
Object.keys(rules).every(property => {
const value = css(elm, property, true);
if (value === '' &&
(rules[property] === '' || rules[property] == null)) {
return true;
}
return (!isVoid(value) &&
value !== '' &&
!isVoid(rules[property]) &&
normalizeCssValue(property, rules[property])
.toString()
.toLowerCase() === value.toString().toLowerCase());
}));
}
if (globalDocument) {
const elm = globalDocument.createElement('div');
elm.style.color = 'red';
assert(hasSameStyle(elm, { color: 'red' }), 'Style test');
assert(hasSameStyle(elm, { fontSize: null }), 'Style test');
assert(hasSameStyle(elm, { fontSize: '' }), 'Style test');
}
/**
* Element has the similar styles keys
*/
export function hasSameStyleKeys(elm, rules) {
return Boolean(!Dom.isTag(elm, 'font') &&
Dom.isHTMLElement(elm) &&
Object.keys(rules).every(property => {
const value = css(elm, property, true);
return value !== '';
}));
}
if (globalDocument) {
const elm2 = globalDocument.createElement('div');
elm2.style.color = 'red';
assert(hasSameStyleKeys(elm2, { color: 'red' }), 'Style test');
assert(!hasSameStyleKeys(elm2, { font: 'Arial', color: 'red' }), 'Style test');
assert(!hasSameStyleKeys(elm2, { border: '1px solid #ccc' }), 'Style test');
}

View File

@@ -0,0 +1,20 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
export * from "./extract";
export * from "./finite-state-machine";
export * from "./get-suit-child";
export * from "./get-suit-parent";
export * from "./has-same-style";
export * from "./is-inside-invisible-element";
export * from "./is-normal-node";
export * from "./is-same-attributes";
export * from "./is-suit-element";
export * from "./list/toggle-ordered-list";
export * from "./list/wrap-list";
export * from "./toggle-attributes";
export * from "./unwrap-children";
export * from "./wrap";
export * from "./wrap-unwrapped-text";

View File

@@ -0,0 +1,20 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
export * from "./extract.js";
export * from "./finite-state-machine.js";
export * from "./get-suit-child.js";
export * from "./get-suit-parent.js";
export * from "./has-same-style.js";
export * from "./is-inside-invisible-element.js";
export * from "./is-normal-node.js";
export * from "./is-same-attributes.js";
export * from "./is-suit-element.js";
export * from "./list/toggle-ordered-list.js";
export * from "./list/wrap-list.js";
export * from "./toggle-attributes.js";
export * from "./unwrap-children.js";
export * from "./wrap.js";
export * from "./wrap-unwrapped-text.js";

View File

@@ -0,0 +1,10 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
/**
* Check if FONT inside STYLE or SCRIPT element
* @private
*/
export declare function isInsideInvisibleElement(font: HTMLElement, root: HTMLElement): boolean;

View File

@@ -0,0 +1,13 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import { Dom } from "../../../dom/dom.js";
/**
* Check if FONT inside STYLE or SCRIPT element
* @private
*/
export function isInsideInvisibleElement(font, root) {
return Boolean(Dom.closest(font, ['style', 'script'], root));
}

View File

@@ -0,0 +1,11 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import type { Nullable } from "../../../../types/index";
/**
* Is normal usual element
* @private
*/
export declare function isNormalNode(elm: Nullable<Node>): boolean;

View File

@@ -0,0 +1,17 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import { Dom } from "../../../dom/dom.js";
import { isMarker } from "../../../helpers/checker/is-marker.js";
/**
* Is normal usual element
* @private
*/
export function isNormalNode(elm) {
return Boolean(elm &&
!Dom.isEmptyTextNode(elm) &&
!Dom.isTemporary(elm) &&
!isMarker(elm));
}

View File

@@ -0,0 +1,12 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import type { IDictionary } from "../../../../types/index";
/**
* Compares whether the given attributes match the element's own attributes
* @private
*/
export declare function isSameAttributes(elm: HTMLElement, attrs?: IDictionary): elm is HTMLElement;
export declare function elementsEqualAttributes(elm1: HTMLElement, elm2: HTMLElement): boolean;

View File

@@ -0,0 +1,36 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import { size } from "../../../helpers/size/object-size.js";
import { attr } from "../../../helpers/utils/index.js";
import { assert } from "../../../helpers/utils/assert.js";
import { hasSameStyle } from "./has-same-style.js";
/**
* Compares whether the given attributes match the element's own attributes
* @private
*/
export function isSameAttributes(elm, attrs) {
if (!elm.attributes.length && !size(attrs)) {
return true;
}
if (!size(attrs)) {
return true;
}
assert(attrs, 'Attrs must be a non-empty object');
return Object.keys(attrs).every(key => {
if (key === 'class' || key === 'className') {
return elm.classList.contains(attrs[key]);
}
if (key === 'style') {
return hasSameStyle(elm, attrs[key]);
}
return attr(elm, key) === attrs[key];
});
}
export function elementsEqualAttributes(elm1, elm2) {
return (elm1.attributes.length === elm2.attributes.length &&
Array.from(elm1.attributes).every(attr => elm2.hasAttribute(attr.name) &&
elm2.getAttribute(attr.name) === attr.value));
}

View File

@@ -0,0 +1,33 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import type { ICommitStyle, Nullable } from "../../../../types/index";
/**
* Checks if an item is suitable for applying a commit. The element suits us if it
* - has the same styles as in the commit (commitStyle.options.style)
* - has the same tag as in the commit (commitStyle.options.element)
*
* @param commitStyle - style commit
* @param elm - checked item
* @param strict - strict mode - false - the default tag is suitable for us if it is also in the commit
* @param strictStyle - strict style mode - true - the element has the same style keys as in the commit, but not their values
* @private
*/
export declare function isSuitElement(commitStyle: ICommitStyle, elm: Nullable<Node>, strict: boolean, strictStyle?: boolean): elm is HTMLElement;
/**
* @private
*/
export declare function suitableClosest(commitStyle: ICommitStyle, element: HTMLElement, root: HTMLElement): Nullable<HTMLElement>;
/**
* Inside the parent element there is a block with the same styles
* @example
* For selection:
* ```html
* <p>|test<strong>test</strong>|</p>
* ```
* Apply `{element:'strong'}`
* @private
*/
export declare function isSameStyleChild(commitStyle: ICommitStyle, elm: Nullable<Node>): elm is HTMLElement;

View File

@@ -0,0 +1,63 @@
import { Dom } from "../../../dom/dom.js";
import { hasSameStyle, hasSameStyleKeys } from "./has-same-style.js";
import { isNormalNode } from "./is-normal-node.js";
/**
* Checks if an item is suitable for applying a commit. The element suits us if it
* - has the same styles as in the commit (commitStyle.options.style)
* - has the same tag as in the commit (commitStyle.options.element)
*
* @param commitStyle - style commit
* @param elm - checked item
* @param strict - strict mode - false - the default tag is suitable for us if it is also in the commit
* @param strictStyle - strict style mode - true - the element has the same style keys as in the commit, but not their values
* @private
*/
export function isSuitElement(commitStyle, elm, strict, strictStyle = true) {
var _a;
if (!elm || !isNormalNode(elm)) {
return false;
}
const { element, elementIsDefault, options } = commitStyle;
if (Dom.isList(elm) && commitStyle.elementIsList) {
return true;
}
const elmIsSame = Dom.isTag(elm, element);
if (elmIsSame && !(elementIsDefault && strict)) {
return true;
}
const elmHasSameStyle = Boolean(((_a = options.attributes) === null || _a === void 0 ? void 0 : _a.style) &&
(strictStyle
? hasSameStyle(elm, options.attributes.style)
: hasSameStyleKeys(elm, options.attributes.style)));
if (elmHasSameStyle && !commitStyle.elementIsList) {
return true;
}
return !elmIsSame && !strict && elementIsDefault && Dom.isInlineBlock(elm);
}
/**
* @private
*/
export function suitableClosest(commitStyle, element, root) {
return Dom.closest(element, node => isSuitElement(commitStyle, node, true, false), root);
}
/**
* Inside the parent element there is a block with the same styles
* @example
* For selection:
* ```html
* <p>|test<strong>test</strong>|</p>
* ```
* Apply `{element:'strong'}`
* @private
*/
export function isSameStyleChild(commitStyle, elm) {
var _a, _b;
const { element, options } = commitStyle;
if (!elm || !isNormalNode(elm)) {
return false;
}
const elmIsSame = elm.nodeName.toLowerCase() === element;
const elmHasSameStyle = Boolean(((_a = options.attributes) === null || _a === void 0 ? void 0 : _a.style) &&
hasSameStyleKeys(elm, (_b = options.attributes) === null || _b === void 0 ? void 0 : _b.style));
return elmIsSame && elmHasSameStyle;
}

View File

@@ -0,0 +1,11 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import type { CommitMode, ICommitStyle, IJodit } from "../../../../../types/index";
/**
* Replaces `ul->ol` or `ol->ul`, apply styles to the list, or remove a list item from it
* @private
*/
export declare function toggleOrderedList(commitStyle: ICommitStyle, li: HTMLElement, jodit: IJodit, mode: CommitMode): CommitMode;

View File

@@ -0,0 +1,57 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import { Dom } from "../../../../dom/dom.js";
import { assert } from "../../../../helpers/utils/assert.js";
import { extractSelectedPart } from "../extract.js";
import { toggleAttributes } from "../toggle-attributes.js";
import { _PREFIX, CHANGE, INITIAL, REPLACE, UNWRAP } from "../../constants.js";
import { wrapList } from "./wrap-list.js";
/**
* Replaces `ul->ol` or `ol->ul`, apply styles to the list, or remove a list item from it
* @private
*/
export function toggleOrderedList(commitStyle, li, jodit, mode) {
if (!li) {
return mode;
}
const list = li.parentElement;
if (!list) {
return mode;
}
const result = jodit.e.fire(`${_PREFIX}BeforeToggleList`, mode, commitStyle, list);
if (result !== undefined) {
return result;
}
const hook = jodit.e.fire.bind(jodit.e, `${_PREFIX}AfterToggleList`);
if (mode !== UNWRAP) {
const isChangeMode = toggleAttributes(commitStyle, li.parentElement, jodit, INITIAL, true) === CHANGE;
// ul => ol, ol => ul or ul => ul.class1
if (mode === REPLACE ||
isChangeMode ||
list.tagName.toLowerCase() !== commitStyle.element) {
const wrapper = unwrapList(REPLACE, list, li, jodit, commitStyle);
const newList = wrapList(commitStyle, wrapper, jodit);
hook(REPLACE, newList, commitStyle);
return REPLACE;
}
}
const wrapper = unwrapList(UNWRAP, list, li, jodit, commitStyle);
hook(UNWRAP, wrapper, commitStyle);
return UNWRAP;
}
function unwrapList(mode, list, li, jodit, cs) {
const result = jodit.e.fire(`${_PREFIX}BeforeUnwrapList`, mode, list, cs);
if (result) {
assert(Dom.isHTMLElement(result), `${_PREFIX}BeforeUnwrapList hook must return HTMLElement`);
return result;
}
extractSelectedPart(list, li, jodit);
assert(Dom.isHTMLElement(li.parentElement), 'Element should be inside the list');
Dom.unwrap(li.parentElement);
return Dom.replace(li, jodit.o.enter.toLowerCase() !== 'br'
? jodit.o.enter
: jodit.createInside.fragment(), jodit.createInside);
}

View File

@@ -0,0 +1,12 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import type { ICommitStyle, IJodit } from "../../../../../types/index";
/**
* Replaces non-leaf items with leaf items and either creates a new list or
* adds a new item to the nearest old list
* @private
*/
export declare function wrapList(commitStyle: ICommitStyle, wrapper: HTMLElement | DocumentFragment, jodit: IJodit): HTMLElement;

View File

@@ -0,0 +1,40 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import { Dom } from "../../../../dom/index.js";
import { elementsEqualAttributes, isSameAttributes, toggleAttributes } from "../index.js";
import { _PREFIX, INITIAL, REPLACE, WRAP } from "../../constants.js";
/**
* Replaces non-leaf items with leaf items and either creates a new list or
* adds a new item to the nearest old list
* @private
*/
export function wrapList(commitStyle, wrapper, jodit) {
const result = jodit.e.fire(`${_PREFIX}BeforeWrapList`, REPLACE, wrapper, commitStyle);
const newWrapper = result !== null && result !== void 0 ? result : Dom.replace(wrapper, 'li', jodit.createInside);
const prev = newWrapper.previousElementSibling;
const next = newWrapper.nextElementSibling;
let list = Dom.isTag(prev, commitStyle.element) ? prev : null;
list !== null && list !== void 0 ? list : (list = Dom.isTag(next, commitStyle.element) ? next : null);
if (!Dom.isList(list) ||
!isSameAttributes(list, commitStyle.options.attributes)) {
list = jodit.createInside.element(commitStyle.element);
toggleAttributes(commitStyle, list, jodit, INITIAL);
Dom.before(newWrapper, list);
}
if (prev === list) {
Dom.append(list, newWrapper);
}
else {
Dom.prepend(list, newWrapper);
}
if (Dom.isTag(list.nextElementSibling, commitStyle.element) &&
elementsEqualAttributes(list, list.nextElementSibling)) {
Dom.append(list, Array.from(list.nextElementSibling.childNodes));
Dom.safeRemove(list.nextElementSibling);
}
jodit.e.fire(`${_PREFIX}AfterWrapList`, WRAP, list, commitStyle);
return list;
}

View File

@@ -0,0 +1,11 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import type { CommitMode, ICommitStyle, IJodit } from "../../../../types/index";
/**
* Toggles attributes
* @private
*/
export declare function toggleAttributes(commitStyle: ICommitStyle, elm: HTMLElement, jodit: IJodit, mode: CommitMode, dry?: boolean): CommitMode;

View File

@@ -0,0 +1,159 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import { globalDocument } from "../../../constants.js";
import { Dom } from "../../../dom/dom.js";
import { getContainer } from "../../../global.js";
import { isBoolean, isNumber, isPlainObject, isString } from "../../../helpers/checker/index.js";
import { normalizeCssValue } from "../../../helpers/normalize/normalize-css-value.js";
import { size } from "../../../helpers/size/object-size.js";
import { kebabCase } from "../../../helpers/string/kebab-case.js";
import { assert, attr } from "../../../helpers/utils/index.js";
import { css } from "../../../helpers/utils/css.js";
import { dataBind } from "../../../helpers/utils/data-bind.js";
import { _PREFIX, CHANGE, UNSET, UNWRAP } from "../constants.js";
const tak = 'toggleAttributes';
/**
* Toggles attributes
* @private
*/
export function toggleAttributes(commitStyle, elm, jodit, mode, dry = false) {
if (!dry && commitStyle.isApplied(elm, tak)) {
return mode;
}
!dry && commitStyle.setApplied(elm, tak);
const { attributes } = commitStyle.options;
if (attributes && size(attributes) > 0) {
Object.keys(attributes).forEach((key) => {
const value = attributes[key];
switch (key) {
case 'style': {
mode = toggleStyle(commitStyle, jodit, value, elm, dry, mode);
break;
}
case 'className':
case 'class':
mode = toggleClass(jodit, value, elm, mode, dry);
break;
default:
mode = toggleAttribute(jodit, value, elm, key, dry, mode);
}
});
}
return mode;
}
function toggleStyle(commitStyle, jodit, style, elm, dry, mode) {
assert(isPlainObject(style) && size(style), 'Style must be an object');
Object.keys(style).forEach((rule) => {
const inlineValue = elm.style.getPropertyValue(kebabCase(rule));
const newValue = style[rule];
if (inlineValue === '' && newValue == null) {
return;
}
if (getNativeCSSValue(jodit, elm, rule) ===
normalizeCssValue(rule, newValue)) {
if (!inlineValue) {
return;
}
!dry && css(elm, rule, null);
mode = UNSET;
mode = removeExtraStyleAttribute(commitStyle, elm, mode);
return;
}
mode = CHANGE;
if (!dry) {
css(elm, rule, newValue);
mode = removeExtraStyleAttribute(commitStyle, elm, mode);
}
});
return mode;
}
function toggleClass(jodit, value, elm, mode, dry) {
assert(isString(value), 'Class name must be a string');
const hook = jodit.e.fire.bind(jodit.e, `${_PREFIX}AfterToggleAttribute`);
if (elm.classList.contains(value.toString())) {
mode = UNSET;
if (!dry) {
elm.classList.remove(value);
if (elm.classList.length === 0) {
attr(elm, 'class', null);
hook(mode, elm, 'class', null);
}
}
}
else {
mode = CHANGE;
if (!dry) {
elm.classList.add(value);
hook(mode, elm, 'class', value);
}
}
return mode;
}
function toggleAttribute(jodit, value, elm, key, dry, mode) {
assert(isString(value) || isNumber(value) || isBoolean(value) || value == null, 'Attribute value must be a string or number or boolean or null');
const hook = jodit.e.fire.bind(jodit.e, `${_PREFIX}AfterToggleAttribute`);
if (attr(elm, key) === value) {
!dry && attr(elm, key, null);
mode = UNSET;
!dry && hook(mode, elm, key, value);
return mode;
}
mode = CHANGE;
if (!dry) {
attr(elm, key, value);
hook(mode, elm, key, value);
}
return mode;
}
/**
* If the element has an empty style attribute, it removes the attribute,
* and if it is default, it removes the element itself
*/
function removeExtraStyleAttribute(commitStyle, elm, mode) {
if (!attr(elm, 'style')) {
attr(elm, 'style', null);
if (elm.tagName.toLowerCase() === commitStyle.defaultTag) {
Dom.unwrap(elm);
mode = UNWRAP;
}
}
return mode;
}
/**
* Creates an iframe into which elements will be inserted to test their default styles in the browser
*/
function getShadowRoot(jodit) {
var _a;
if (dataBind(jodit, 'shadowRoot') !== undefined) {
return dataBind(jodit, 'shadowRoot');
}
const container = getContainer(jodit);
const iframe = globalDocument.createElement('iframe');
css(iframe, {
width: 0,
height: 0,
position: 'absolute',
border: 0
});
iframe.src = 'about:blank';
container.appendChild(iframe);
const doc = (_a = iframe.contentWindow) === null || _a === void 0 ? void 0 : _a.document;
const shadowRoot = !doc ? jodit.od.body : doc.body;
dataBind(jodit, 'shadowRoot', shadowRoot);
return shadowRoot;
}
/**
* `strong -> fontWeight 700`
*/
function getNativeCSSValue(jodit, elm, key) {
const newElm = jodit.create.element(elm.tagName.toLowerCase());
newElm.style.cssText = elm.style.cssText;
const root = getShadowRoot(jodit);
root.appendChild(newElm);
const result = css(newElm, key);
Dom.safeRemove(newElm);
return result;
}

View File

@@ -0,0 +1,11 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import type { ICommitStyle } from "../../../../types/index";
/**
* Unwrap all suit elements inside
* @private
*/
export declare function unwrapChildren(style: ICommitStyle, font: HTMLElement): boolean;

View File

@@ -0,0 +1,61 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import { Dom } from "../../../dom/dom.js";
import { attr, css } from "../../../helpers/utils/index.js";
import { hasSameStyleKeys } from "./has-same-style.js";
import { isSameStyleChild, isSuitElement } from "./is-suit-element.js";
/**
* Unwrap all suit elements inside
* @private
*/
export function unwrapChildren(style, font) {
var _a;
const needUnwrap = [];
const needChangeStyle = [];
let firstElementSuit;
const cssStyle = (_a = style.options.attributes) === null || _a === void 0 ? void 0 : _a.style;
if (font.firstChild) {
const gen = Dom.eachGen(font);
let item = gen.next();
while (!item.done) {
const elm = item.value;
if (isSuitElement(style, elm, true) &&
(!cssStyle || hasSameStyleKeys(elm, cssStyle))) {
if (firstElementSuit === undefined) {
firstElementSuit = true;
}
needUnwrap.push(elm);
}
else if (cssStyle && isSameStyleChild(style, elm)) {
if (firstElementSuit === undefined) {
firstElementSuit = false;
}
needChangeStyle.push(() => {
css(elm, Object.keys(cssStyle).reduce((acc, key) => {
acc[key] = null;
return acc;
}, {}));
if (!attr(elm, 'style')) {
attr(elm, 'style', null);
}
if (!attr(elm, 'style') &&
elm.nodeName.toLowerCase() === style.element) {
needUnwrap.push(elm);
}
});
}
else if (!Dom.isEmptyTextNode(elm)) {
if (firstElementSuit === undefined) {
firstElementSuit = false;
}
}
item = gen.next();
}
}
needChangeStyle.forEach(clb => clb());
needUnwrap.forEach(Dom.unwrap);
return Boolean(firstElementSuit);
}

View File

@@ -0,0 +1,11 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import type { ICommitStyle, IJodit } from "../../../../types/index";
/**
* Wrap text or inline elements inside Block element
* @private
*/
export declare function wrapUnwrappedText(style: ICommitStyle, elm: Node, jodit: IJodit): HTMLElement;

View File

@@ -0,0 +1,52 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import { Dom } from "../../../dom/dom.js";
import { isMarker } from "../../../helpers/checker/is-marker.js";
/**
* Wrap text or inline elements inside Block element
* @private
*/
export function wrapUnwrappedText(style, elm, jodit) {
const root = jodit.editor, ci = jodit.createInside, edge = (n, key = 'previousSibling') => {
let edgeNode = n, node = n;
while (node && !isMarker(node)) {
if (Dom.isTag(node, jodit.o.enter)) {
break;
}
edgeNode = node;
if (node[key]) {
node = node[key];
}
else {
node =
node.parentNode &&
!Dom.isBlock(node.parentNode) &&
node.parentNode !== root
? node.parentNode
: null;
}
if (Dom.isBlock(node)) {
break;
}
}
return edgeNode;
};
const start = edge(elm), end = edge(elm, 'nextSibling');
const range = jodit.s.createRange();
range.setStartBefore(start);
range.setEndAfter(end);
const fragment = range.extractContents();
const wrapper = ci.element(style.element);
wrapper.appendChild(fragment);
Dom.safeInsertNode(range, wrapper);
if (style.elementIsBlock) {
if (Dom.isEmpty(wrapper) &&
!Dom.isTag(wrapper.firstElementChild, 'br')) {
wrapper.appendChild(ci.element('br'));
}
}
return wrapper;
}

View File

@@ -0,0 +1,11 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import type { ICommitStyle, IJodit } from "../../../../types/index";
/**
* Replaces the parent tag with the applicable one, or wraps the text and also replaces the tag
* @private
*/
export declare function wrap(commitStyle: ICommitStyle, font: HTMLElement, jodit: IJodit): HTMLElement;

View File

@@ -0,0 +1,44 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import { Dom } from "../../../dom/index.js";
import { attr } from "../../../helpers/utils/attr.js";
import { wrapList } from "./list/wrap-list.js";
import { wrapUnwrappedText } from "./wrap-unwrapped-text.js";
/**
* Replaces the parent tag with the applicable one, or wraps the text and also replaces the tag
* @private
*/
export function wrap(commitStyle, font, jodit) {
const wrapper = findOrCreateWrapper(commitStyle, font, jodit);
return commitStyle.elementIsList
? wrapList(commitStyle, wrapper, jodit)
: Dom.replace(wrapper, commitStyle.element, jodit.createInside, true);
}
const WRAP_NODES = new Set([
'td',
'th',
'tr',
'tbody',
'table',
'li',
'ul',
'ol'
]);
/**
* If we apply a block element, then it finds the closest block parent (exclude table cell etc.),
* otherwise it wraps free text in an element.
*/
function findOrCreateWrapper(commitStyle, font, jodit) {
if (commitStyle.elementIsBlock) {
const box = Dom.up(font, node => Dom.isBlock(node) && !Dom.isTag(node, WRAP_NODES), jodit.editor);
if (box) {
return box;
}
return wrapUnwrappedText(commitStyle, font, jodit);
}
attr(font, 'size', null);
return font;
}

View File

@@ -0,0 +1,11 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
/**
* @module selection
*/
import type { ICommitStyle, IJodit } from "../../../types/index";
/** @internal */
export declare function ApplyStyle(jodit: IJodit, cs: ICommitStyle): void;

View File

@@ -0,0 +1,42 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import { FiniteStateMachine } from "./api/index.js";
import { INITIAL } from "./constants.js";
import { states, transactions } from "./transactions.js";
/** @internal */
export function ApplyStyle(jodit, cs) {
var _a;
const { s: sel, editor } = jodit;
// sel.save();
(_a = editor.firstChild) === null || _a === void 0 ? void 0 : _a.normalize(); // FF fix for test "commandsTest - Exec command "bold"
const fakes = sel.fakes();
const gen = jodit.s.wrapInTagGen(fakes);
let font = gen.next();
if (font.done) {
return;
}
let state = {
collapsed: sel.isCollapsed(),
mode: INITIAL,
element: font.value,
next: states.START,
jodit,
style: cs
};
while (font && !font.done) {
const machine = new FiniteStateMachine(states.START, transactions);
state.element = font.value;
// machine.disableSilent();
while (machine.getState() !== states.END) {
// console.log(machine.getState(), state);
state = machine.dispatch('exec', state);
}
// console.log('-------------------');
font = gen.next();
}
// sel.restore();
sel.restoreFakes(fakes);
}

View File

@@ -0,0 +1,29 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
/**
* @module selection
*/
import type { HTMLTagNames, ICommitStyle, IJodit, IStyleOptions } from "../../../types/index";
export declare class CommitStyle implements ICommitStyle {
readonly options: IStyleOptions;
private __applyMap;
isApplied(elm: HTMLElement, key: string): boolean;
setApplied(elm: HTMLElement, key: string): void;
get elementIsList(): boolean;
get element(): HTMLTagNames;
/**
* New element is blocked
*/
get elementIsBlock(): boolean;
/**
* The commit applies the tag change
*/
get isElementCommit(): boolean;
get defaultTag(): HTMLTagNames;
get elementIsDefault(): boolean;
constructor(options: IStyleOptions);
apply(jodit: IJodit): void;
}

View File

@@ -0,0 +1,74 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import { IS_BLOCK, LIST_TAGS } from "../../constants.js";
import { camelCase } from "../../helpers/string/camel-case.js";
import { _PREFIX } from "./constants.js";
import { ApplyStyle } from "./apply-style.js";
export class CommitStyle {
isApplied(elm, key) {
const data = this.__applyMap.get(elm);
if (!data) {
return false;
}
return data[key];
}
setApplied(elm, key) {
var _a;
const data = (_a = this.__applyMap.get(elm)) !== null && _a !== void 0 ? _a : {};
data[key] = true;
this.__applyMap.set(elm, data);
}
get elementIsList() {
return Boolean(this.options.element && LIST_TAGS.has(this.options.element));
}
get element() {
return this.options.element || this.defaultTag;
}
/**
* New element is blocked
*/
get elementIsBlock() {
return Boolean(this.options.element && IS_BLOCK.test(this.options.element));
}
/**
* The commit applies the tag change
*/
get isElementCommit() {
return Boolean(this.options.element &&
this.options.element !== this.options.defaultTag);
}
get defaultTag() {
if (this.options.defaultTag) {
return this.options.defaultTag;
}
return this.elementIsBlock ? 'p' : 'span';
}
get elementIsDefault() {
return this.element === this.defaultTag;
}
constructor(options) {
this.options = options;
this.__applyMap = new WeakMap();
}
apply(jodit) {
const { hooks } = this.options;
const keys = (hooks ? Object.keys(hooks) : []);
try {
keys.forEach(key => {
jodit.e.on(camelCase(_PREFIX + '_' + key), hooks[key]);
});
ApplyStyle(jodit, this);
}
finally {
keys.forEach(key => {
jodit.e.off(camelCase(_PREFIX + '_' + key), hooks[key]);
});
this.__applyMap = new WeakMap();
}
jodit.synchronizeValues();
jodit.e.fire('afterCommitStyle', this);
}
}

View File

@@ -0,0 +1,22 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
/**
* @module selection
*/
/** @internal */
export declare const WRAP = "wrap";
/** @internal */
export declare const UNWRAP = "unwrap";
/** @internal */
export declare const CHANGE = "change";
/** @internal */
export declare const UNSET = "unset";
/** @internal */
export declare const INITIAL = "initial";
/** @internal */
export declare const REPLACE = "replace";
/** @internal */
export declare const _PREFIX = "commitStyle";

View File

@@ -0,0 +1,22 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
/**
* @module selection
*/
/** @internal */
export const WRAP = 'wrap';
/** @internal */
export const UNWRAP = 'unwrap';
/** @internal */
export const CHANGE = 'change';
/** @internal */
export const UNSET = 'unset';
/** @internal */
export const INITIAL = 'initial';
/** @internal */
export const REPLACE = 'replace';
/** @internal */
export const _PREFIX = 'commitStyle';

View File

@@ -0,0 +1,33 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
/**
* @module selection
*/
import type { CommitMode, ICommitStyle, IDictionary, IJodit } from "../../../types/index";
export declare const states: {
readonly START: "START";
readonly ELEMENT: "ELEMENT";
readonly UNWRAP: "UNWRAP";
readonly UNWRAP_CHILDREN: "UNWRAP_CHILDREN";
readonly CHANGE: "CHANGE";
readonly REPLACE_DEFAULT: "REPLACE_DEFAULT";
readonly LIST: "LIST";
readonly TOGGLE_LIST: "TOGGLE_LIST";
readonly WRAP: "WRAP";
readonly EXTRACT: "EXTRACT";
readonly END: "END";
};
export interface IStyleTransactionValue {
next: keyof typeof states;
element: HTMLElement;
style: ICommitStyle;
jodit: IJodit;
mode: CommitMode;
collapsed: boolean;
}
type IStyleTransactions = IDictionary<IDictionary<(value: IStyleTransactionValue) => IStyleTransactionValue>, keyof typeof states>;
export declare const transactions: IStyleTransactions;
export {};

View File

@@ -0,0 +1,184 @@
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import { LIST_TAGS } from "../../constants.js";
import { Dom } from "../../dom/dom.js";
import { assert } from "../../helpers/utils/assert.js";
import { INITIAL, REPLACE, UNSET, UNWRAP, WRAP } from "../index.js";
import { extractSelectedPart, getSuitChild, getSuitParent, isInsideInvisibleElement, suitableClosest, toggleAttributes, toggleOrderedList, unwrapChildren, wrap } from "./api/index.js";
export const states = {
START: 'START',
ELEMENT: 'ELEMENT',
UNWRAP: 'UNWRAP',
UNWRAP_CHILDREN: 'UNWRAP_CHILDREN',
CHANGE: 'CHANGE',
REPLACE_DEFAULT: 'REPLACE_DEFAULT',
LIST: 'LIST',
TOGGLE_LIST: 'TOGGLE_LIST',
WRAP: 'WRAP',
EXTRACT: 'EXTRACT',
END: 'END'
};
export const transactions = {
[states.START]: {
exec(value) {
const { element, jodit, style, mode, collapsed } = value;
if (isInsideInvisibleElement(element, jodit.editor) ||
(!collapsed && Dom.isEmptyContent(element))) {
return { ...value, next: states.END };
}
const elm = getSuitParent(style, element, jodit.editor) ||
getSuitChild(style, element);
if (elm) {
return { ...value, next: states.ELEMENT, element: elm };
}
const suit = suitableClosest(style, element, jodit.editor);
if (style.elementIsList && Dom.isList(suit)) {
return { ...value, next: states.LIST };
}
if (suit) {
return {
...value,
next: states.EXTRACT
};
}
return {
...value,
next: mode !== UNWRAP ? states.UNWRAP_CHILDREN : states.END
};
}
},
[states.LIST]: {
exec(value) {
const { element, jodit, mode } = value;
if (mode !== INITIAL && mode !== UNWRAP && mode !== REPLACE) {
return { ...value, next: states.END };
}
const li = Dom.closest(element, 'li', jodit.editor);
if (!li) {
return { ...value, next: states.END };
}
const list = Dom.closest(element, LIST_TAGS, jodit.editor);
if (list) {
return { ...value, element: li, next: states.TOGGLE_LIST };
}
return {
...value,
next: states.END
};
}
},
[states.TOGGLE_LIST]: {
exec(value) {
return {
...value,
mode: toggleOrderedList(value.style, value.element, value.jodit, value.mode),
next: states.END
};
}
},
[states.EXTRACT]: {
exec(value) {
var _a;
const { element, jodit, style } = value;
const suit = suitableClosest(style, element, jodit.editor);
assert(suit, 'This place should have an element');
// If we're applying inline styles to a block element, don't split the block
const isApplyingInlineStyle = !style.elementIsBlock && ((_a = style.options.attributes) === null || _a === void 0 ? void 0 : _a.style);
const shouldNotSplitBlock = isApplyingInlineStyle && Dom.isBlock(suit);
if (!shouldNotSplitBlock) {
if (!style.elementIsBlock) {
extractSelectedPart(suit, element, jodit);
}
return {
...value,
element: suit,
next: states.ELEMENT
};
}
// Create a new wrapper instead of splitting the block
return {
...value,
next: states.WRAP
};
}
},
[states.UNWRAP_CHILDREN]: {
exec(value) {
const { element, style } = value;
if (!unwrapChildren(style, element)) {
return {
...value,
next: states.WRAP
};
}
return {
...value,
mode: UNWRAP,
next: states.END
};
}
},
[states.WRAP]: {
exec(value) {
const { element, jodit, style } = value;
const wrapper = wrap(style, element, jodit);
return {
...value,
next: style.elementIsList ? states.END : states.CHANGE,
mode: WRAP,
element: wrapper
};
}
},
[states.ELEMENT]: {
exec(value) {
const { style, element, jodit } = value;
if (toggleAttributes(style, element, jodit, INITIAL, true) !==
INITIAL) {
return { ...value, next: states.CHANGE };
}
// Apply same color for anchor https://github.com/xdan/jodit/issues/936
if (!Dom.isTag(element, style.element)) {
return { ...value, next: states.END };
}
return { ...value, next: states.UNWRAP };
}
},
[states.CHANGE]: {
exec(value) {
const { style, element, jodit, mode } = value;
const newMode = toggleAttributes(style, element, jodit, value.mode);
if (mode !== WRAP &&
newMode === UNSET &&
!element.attributes.length &&
Dom.isTag(element, style.element)) {
return { ...value, next: states.UNWRAP };
}
return { ...value, mode: newMode, next: states.END };
}
},
[states.UNWRAP]: {
exec(value) {
if (value.element.attributes.length &&
Dom.isTag(value.element, value.style.element)) {
return { ...value, next: states.REPLACE_DEFAULT };
}
Dom.unwrap(value.element);
return { ...value, mode: UNWRAP, next: states.END };
}
},
[states.REPLACE_DEFAULT]: {
exec(value) {
Dom.replace(value.element, value.style.defaultTag, value.jodit.createInside, true);
return { ...value, mode: REPLACE, next: states.END };
}
},
[states.END]: {
exec(value) {
return value;
}
}
};