@tiptap/core
Advanced tools
| /** | ||
| * Compare two attribute objects for equality. | ||
| * Handles null/undefined and asserts key presence in both objects so that | ||
| * `{ foo: undefined }` and `{ bar: undefined }` are not treated as equal. | ||
| */ | ||
| export function attrsEqual( | ||
| a: Record<string, any> | null | undefined, | ||
| b: Record<string, any> | null | undefined, | ||
| ): boolean { | ||
| if (a === b) { | ||
| return true | ||
| } | ||
| if (!a || !b) { | ||
| return false | ||
| } | ||
| const keysA = Object.keys(a) | ||
| const keysB = Object.keys(b) | ||
| if (keysA.length !== keysB.length) { | ||
| return false | ||
| } | ||
| return keysA.every( | ||
| key => Object.prototype.hasOwnProperty.call(b, key) && Object.is(a[key], b[key]), | ||
| ) | ||
| } |
| import { attrsEqual } from './attrsEqual.js' | ||
| /** | ||
| * Compare two arrays of mark objects for equality. | ||
| * Marks are compared by type and attributes (using attrsEqual), | ||
| * so key ordering in attrs does not matter. | ||
| */ | ||
| export function marksEqual( | ||
| a: { type: string; attrs?: Record<string, any> }[], | ||
| b: { type: string; attrs?: Record<string, any> }[], | ||
| ): boolean { | ||
| if (a.length !== b.length) { | ||
| return false | ||
| } | ||
| return a.every((mark, i) => { | ||
| const other = b[i] | ||
| return mark.type === other.type && attrsEqual(mark.attrs, other.attrs) | ||
| }) | ||
| } |
+3
-3
| { | ||
| "name": "@tiptap/core", | ||
| "version": "3.24.0", | ||
| "version": "3.25.0", | ||
| "description": "headless rich text editor", | ||
@@ -61,6 +61,6 @@ "keywords": [ | ||
| "devDependencies": { | ||
| "@tiptap/pm": "^3.24.0" | ||
| "@tiptap/pm": "^3.25.0" | ||
| }, | ||
| "peerDependencies": { | ||
| "@tiptap/pm": "3.24.0" | ||
| "@tiptap/pm": "3.25.0" | ||
| }, | ||
@@ -67,0 +67,0 @@ "scripts": { |
@@ -7,6 +7,9 @@ import type { RawCommands } from '../types.js' | ||
| /** | ||
| * Remove all marks in the current selection. | ||
| * Remove all clearable marks in the current selection. | ||
| * Marks with `clearable: false` are preserved | ||
| * @param options.ignoreClearable If true, removes all marks regardless of `clearable` setting. Defaults to `false`. | ||
| * @example editor.commands.unsetAllMarks() | ||
| * @example editor.commands.unsetAllMarks({ ignoreClearable: true }) | ||
| */ | ||
| unsetAllMarks: () => ReturnType | ||
| unsetAllMarks: (options?: { ignoreClearable?: boolean }) => ReturnType | ||
| } | ||
@@ -17,4 +20,5 @@ } | ||
| export const unsetAllMarks: RawCommands['unsetAllMarks'] = | ||
| () => | ||
| ({ tr, dispatch }) => { | ||
| (options = {}) => | ||
| ({ tr, dispatch, editor }) => { | ||
| const { ignoreClearable = false } = options | ||
| const { selection } = tr | ||
@@ -27,5 +31,13 @@ const { empty, ranges } = selection | ||
| const { nonClearableMarks } = editor.extensionManager | ||
| if (dispatch) { | ||
| const clearableMarkTypes = Object.values(editor.schema.marks).filter( | ||
| markType => ignoreClearable || !nonClearableMarks.includes(markType.name), | ||
| ) | ||
| ranges.forEach(range => { | ||
| tr.removeMark(range.$from.pos, range.$to.pos) | ||
| for (const markType of clearableMarkTypes) { | ||
| tr.removeMark(range.$from.pos, range.$to.pos, markType) | ||
| } | ||
| }) | ||
@@ -32,0 +44,0 @@ } |
@@ -50,2 +50,4 @@ import { keymap } from '@tiptap/pm/keymap' | ||
| nonClearableMarks: string[] = [] | ||
| constructor(extensions: Extensions, editor: Editor) { | ||
@@ -468,2 +470,11 @@ this.editor = editor | ||
| } | ||
| const clearable = | ||
| callOrReturn( | ||
| getExtensionField<MarkConfig['clearable']>(extension, 'clearable', context), | ||
| ) ?? true | ||
| if (!clearable) { | ||
| this.nonClearableMarks.push(extension.name) | ||
| } | ||
| } | ||
@@ -470,0 +481,0 @@ |
@@ -29,14 +29,21 @@ import { Plugin, PluginKey } from '@tiptap/pm/state' | ||
| const { doc, selection } = state | ||
| const { ranges } = selection | ||
| const from = Math.min(...ranges.map(range => range.$from.pos)) | ||
| const to = Math.max(...ranges.map(range => range.$to.pos)) | ||
| const textSerializers = getTextSerializersFromSchema(schema) | ||
| const range = { from, to } | ||
| const { blockSeparator } = this.options | ||
| const options = { | ||
| ...(blockSeparator !== undefined ? { blockSeparator } : {}), | ||
| textSerializers, | ||
| } | ||
| return getTextBetween(doc, range, { | ||
| ...(this.options.blockSeparator !== undefined | ||
| ? { blockSeparator: this.options.blockSeparator } | ||
| : {}), | ||
| textSerializers, | ||
| }) | ||
| // Serialize each selection range independently and join the results. | ||
| // CellSelection exposes one range per selected cell; flattening to | ||
| // min(from)/max(to) would pull in unselected cells between them. | ||
| // Sort by document position so reverse selections (e.g. dragging | ||
| // upward) still emit text in document order. | ||
| const sortedRanges = [...selection.ranges].sort((a, b) => a.$from.pos - b.$from.pos) | ||
| return sortedRanges | ||
| .map(({ $from, $to }) => | ||
| getTextBetween(doc, { from: $from.pos, to: $to.pos }, options), | ||
| ) | ||
| .join(blockSeparator ?? '\n\n') | ||
| }, | ||
@@ -43,0 +50,0 @@ }, |
@@ -408,2 +408,3 @@ import type { Node as PMNode } from '@tiptap/pm/model' | ||
| this.element = options.element | ||
| this.element.draggable = false | ||
| this.contentElement = options.contentElement | ||
@@ -410,0 +411,0 @@ |
+7
-0
@@ -34,2 +34,9 @@ import type { DOMOutputSpec, Mark as ProseMirrorMark, MarkSpec, MarkType } from '@tiptap/pm/model' | ||
| /** | ||
| * Whether this mark is removed by `unsetAllMarks`. | ||
| * Set to `false` for semantic marks (comments, suggestions) that should survive "clear formatting". | ||
| * @default true | ||
| */ | ||
| clearable?: boolean | (() => boolean) | ||
| /** | ||
| * Inclusive | ||
@@ -36,0 +43,0 @@ */ |
@@ -0,1 +1,2 @@ | ||
| export * from './attrsEqual.js' | ||
| export * from './callOrReturn.js' | ||
@@ -24,2 +25,3 @@ export * from './canInsertNode.js' | ||
| export * as markdown from './markdown/index.js' | ||
| export * from './marksEqual.js' | ||
| export * from './mergeAttributes.js' | ||
@@ -26,0 +28,0 @@ export * from './mergeDeep.js' |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
2410573
0.68%217
0.93%33007
0.6%