fix: resolve TypeScript errors in frontend build
This commit is contained in:
21
node_modules/@tiptap/core/LICENSE.md
generated
vendored
Normal file
21
node_modules/@tiptap/core/LICENSE.md
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025, Tiptap GmbH
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
18
node_modules/@tiptap/core/README.md
generated
vendored
Normal file
18
node_modules/@tiptap/core/README.md
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
# @tiptap/core
|
||||
|
||||
[](https://www.npmjs.com/package/@tiptap/core)
|
||||
[](https://npmcharts.com/compare/tiptap?minimal=true)
|
||||
[](https://www.npmjs.com/package/@tiptap/core)
|
||||
[](https://github.com/sponsors/ueberdosis)
|
||||
|
||||
## Introduction
|
||||
|
||||
Tiptap is a headless wrapper around [ProseMirror](https://ProseMirror.net) – a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as _New York Times_, _The Guardian_ or _Atlassian_.
|
||||
|
||||
## Official Documentation
|
||||
|
||||
Documentation can be found on the [Tiptap website](https://tiptap.dev).
|
||||
|
||||
## License
|
||||
|
||||
Tiptap is open sourced software licensed under the [MIT license](https://github.com/ueberdosis/tiptap/blob/main/LICENSE.md).
|
||||
7126
node_modules/@tiptap/core/dist/index.cjs
generated
vendored
Normal file
7126
node_modules/@tiptap/core/dist/index.cjs
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
node_modules/@tiptap/core/dist/index.cjs.map
generated
vendored
Normal file
1
node_modules/@tiptap/core/dist/index.cjs.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
4954
node_modules/@tiptap/core/dist/index.d.cts
generated
vendored
Normal file
4954
node_modules/@tiptap/core/dist/index.d.cts
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
4954
node_modules/@tiptap/core/dist/index.d.ts
generated
vendored
Normal file
4954
node_modules/@tiptap/core/dist/index.d.ts
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
6992
node_modules/@tiptap/core/dist/index.js
generated
vendored
Normal file
6992
node_modules/@tiptap/core/dist/index.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
node_modules/@tiptap/core/dist/index.js.map
generated
vendored
Normal file
1
node_modules/@tiptap/core/dist/index.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
56
node_modules/@tiptap/core/dist/jsx-runtime/jsx-runtime.cjs
generated
vendored
Normal file
56
node_modules/@tiptap/core/dist/jsx-runtime/jsx-runtime.cjs
generated
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
|
||||
// src/jsx-runtime.ts
|
||||
var jsx_runtime_exports = {};
|
||||
__export(jsx_runtime_exports, {
|
||||
Fragment: () => Fragment,
|
||||
createElement: () => h,
|
||||
h: () => h,
|
||||
jsx: () => h,
|
||||
jsxDEV: () => h,
|
||||
jsxs: () => h
|
||||
});
|
||||
module.exports = __toCommonJS(jsx_runtime_exports);
|
||||
function Fragment(props) {
|
||||
return props.children;
|
||||
}
|
||||
var h = (tag, attributes) => {
|
||||
if (tag === "slot") {
|
||||
return 0;
|
||||
}
|
||||
if (tag instanceof Function) {
|
||||
return tag(attributes);
|
||||
}
|
||||
const { children, ...rest } = attributes != null ? attributes : {};
|
||||
if (tag === "svg") {
|
||||
throw new Error("SVG elements are not supported in the JSX syntax, use the array syntax instead");
|
||||
}
|
||||
return [tag, rest, children];
|
||||
};
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
Fragment,
|
||||
createElement,
|
||||
h,
|
||||
jsx,
|
||||
jsxDEV,
|
||||
jsxs
|
||||
});
|
||||
//# sourceMappingURL=jsx-runtime.cjs.map
|
||||
1
node_modules/@tiptap/core/dist/jsx-runtime/jsx-runtime.cjs.map
generated
vendored
Normal file
1
node_modules/@tiptap/core/dist/jsx-runtime/jsx-runtime.cjs.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../src/jsx-runtime.ts"],"sourcesContent":["export type Attributes = Record<string, any>\n\nexport type DOMOutputSpecElement = 0 | Attributes | DOMOutputSpecArray\n/**\n * Better describes the output of a `renderHTML` function in prosemirror\n * @see https://prosemirror.net/docs/ref/#model.DOMOutputSpec\n */\nexport type DOMOutputSpecArray =\n | [string]\n | [string, Attributes]\n | [string, 0]\n | [string, Attributes, 0]\n | [string, Attributes, DOMOutputSpecArray | 0]\n | [string, DOMOutputSpecArray]\n\n// JSX types for Tiptap's JSX runtime\n// These types only apply when using @jsxImportSource @tiptap/core\n// eslint-disable-next-line @typescript-eslint/no-namespace\nexport namespace JSX {\n export type Element = DOMOutputSpecArray\n export interface IntrinsicElements {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [key: string]: any\n }\n export interface ElementChildrenAttribute {\n children: unknown\n }\n}\n\nexport type JSXRenderer = (\n tag: 'slot' | string | ((props?: Attributes) => DOMOutputSpecArray | DOMOutputSpecElement),\n props?: Attributes,\n ...children: JSXRenderer[]\n) => DOMOutputSpecArray | DOMOutputSpecElement\n\nexport function Fragment(props: { children: JSXRenderer[] }) {\n return props.children\n}\n\nexport const h: JSXRenderer = (tag, attributes) => {\n // Treat the slot tag as the Prosemirror hole to render content into\n if (tag === 'slot') {\n return 0\n }\n\n // If the tag is a function, call it with the props\n if (tag instanceof Function) {\n return tag(attributes)\n }\n\n const { children, ...rest } = attributes ?? {}\n\n if (tag === 'svg') {\n throw new Error('SVG elements are not supported in the JSX syntax, use the array syntax instead')\n }\n\n // Otherwise, return the tag, attributes, and children\n return [tag, rest, children]\n}\n\n// See\n// https://esbuild.github.io/api/#jsx-import-source\n// https://www.typescriptlang.org/tsconfig/#jsxImportSource\n\nexport { h as createElement, h as jsx, h as jsxDEV, h as jsxs }\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmCO,SAAS,SAAS,OAAoC;AAC3D,SAAO,MAAM;AACf;AAEO,IAAM,IAAiB,CAAC,KAAK,eAAe;AAEjD,MAAI,QAAQ,QAAQ;AAClB,WAAO;AAAA,EACT;AAGA,MAAI,eAAe,UAAU;AAC3B,WAAO,IAAI,UAAU;AAAA,EACvB;AAEA,QAAM,EAAE,UAAU,GAAG,KAAK,IAAI,kCAAc,CAAC;AAE7C,MAAI,QAAQ,OAAO;AACjB,UAAM,IAAI,MAAM,gFAAgF;AAAA,EAClG;AAGA,SAAO,CAAC,KAAK,MAAM,QAAQ;AAC7B;","names":[]}
|
||||
23
node_modules/@tiptap/core/dist/jsx-runtime/jsx-runtime.d.cts
generated
vendored
Normal file
23
node_modules/@tiptap/core/dist/jsx-runtime/jsx-runtime.d.cts
generated
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
type Attributes = Record<string, any>;
|
||||
type DOMOutputSpecElement = 0 | Attributes | DOMOutputSpecArray;
|
||||
/**
|
||||
* Better describes the output of a `renderHTML` function in prosemirror
|
||||
* @see https://prosemirror.net/docs/ref/#model.DOMOutputSpec
|
||||
*/
|
||||
type DOMOutputSpecArray = [string] | [string, Attributes] | [string, 0] | [string, Attributes, 0] | [string, Attributes, DOMOutputSpecArray | 0] | [string, DOMOutputSpecArray];
|
||||
declare namespace JSX {
|
||||
type Element = DOMOutputSpecArray;
|
||||
interface IntrinsicElements {
|
||||
[key: string]: any;
|
||||
}
|
||||
interface ElementChildrenAttribute {
|
||||
children: unknown;
|
||||
}
|
||||
}
|
||||
type JSXRenderer = (tag: 'slot' | string | ((props?: Attributes) => DOMOutputSpecArray | DOMOutputSpecElement), props?: Attributes, ...children: JSXRenderer[]) => DOMOutputSpecArray | DOMOutputSpecElement;
|
||||
declare function Fragment(props: {
|
||||
children: JSXRenderer[];
|
||||
}): JSXRenderer[];
|
||||
declare const h: JSXRenderer;
|
||||
|
||||
export { type Attributes, type DOMOutputSpecArray, type DOMOutputSpecElement, Fragment, JSX, type JSXRenderer, h as createElement, h, h as jsx, h as jsxDEV, h as jsxs };
|
||||
23
node_modules/@tiptap/core/dist/jsx-runtime/jsx-runtime.d.ts
generated
vendored
Normal file
23
node_modules/@tiptap/core/dist/jsx-runtime/jsx-runtime.d.ts
generated
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
type Attributes = Record<string, any>;
|
||||
type DOMOutputSpecElement = 0 | Attributes | DOMOutputSpecArray;
|
||||
/**
|
||||
* Better describes the output of a `renderHTML` function in prosemirror
|
||||
* @see https://prosemirror.net/docs/ref/#model.DOMOutputSpec
|
||||
*/
|
||||
type DOMOutputSpecArray = [string] | [string, Attributes] | [string, 0] | [string, Attributes, 0] | [string, Attributes, DOMOutputSpecArray | 0] | [string, DOMOutputSpecArray];
|
||||
declare namespace JSX {
|
||||
type Element = DOMOutputSpecArray;
|
||||
interface IntrinsicElements {
|
||||
[key: string]: any;
|
||||
}
|
||||
interface ElementChildrenAttribute {
|
||||
children: unknown;
|
||||
}
|
||||
}
|
||||
type JSXRenderer = (tag: 'slot' | string | ((props?: Attributes) => DOMOutputSpecArray | DOMOutputSpecElement), props?: Attributes, ...children: JSXRenderer[]) => DOMOutputSpecArray | DOMOutputSpecElement;
|
||||
declare function Fragment(props: {
|
||||
children: JSXRenderer[];
|
||||
}): JSXRenderer[];
|
||||
declare const h: JSXRenderer;
|
||||
|
||||
export { type Attributes, type DOMOutputSpecArray, type DOMOutputSpecElement, Fragment, JSX, type JSXRenderer, h as createElement, h, h as jsx, h as jsxDEV, h as jsxs };
|
||||
26
node_modules/@tiptap/core/dist/jsx-runtime/jsx-runtime.js
generated
vendored
Normal file
26
node_modules/@tiptap/core/dist/jsx-runtime/jsx-runtime.js
generated
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
// src/jsx-runtime.ts
|
||||
function Fragment(props) {
|
||||
return props.children;
|
||||
}
|
||||
var h = (tag, attributes) => {
|
||||
if (tag === "slot") {
|
||||
return 0;
|
||||
}
|
||||
if (tag instanceof Function) {
|
||||
return tag(attributes);
|
||||
}
|
||||
const { children, ...rest } = attributes != null ? attributes : {};
|
||||
if (tag === "svg") {
|
||||
throw new Error("SVG elements are not supported in the JSX syntax, use the array syntax instead");
|
||||
}
|
||||
return [tag, rest, children];
|
||||
};
|
||||
export {
|
||||
Fragment,
|
||||
h as createElement,
|
||||
h,
|
||||
h as jsx,
|
||||
h as jsxDEV,
|
||||
h as jsxs
|
||||
};
|
||||
//# sourceMappingURL=jsx-runtime.js.map
|
||||
1
node_modules/@tiptap/core/dist/jsx-runtime/jsx-runtime.js.map
generated
vendored
Normal file
1
node_modules/@tiptap/core/dist/jsx-runtime/jsx-runtime.js.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../src/jsx-runtime.ts"],"sourcesContent":["export type Attributes = Record<string, any>\n\nexport type DOMOutputSpecElement = 0 | Attributes | DOMOutputSpecArray\n/**\n * Better describes the output of a `renderHTML` function in prosemirror\n * @see https://prosemirror.net/docs/ref/#model.DOMOutputSpec\n */\nexport type DOMOutputSpecArray =\n | [string]\n | [string, Attributes]\n | [string, 0]\n | [string, Attributes, 0]\n | [string, Attributes, DOMOutputSpecArray | 0]\n | [string, DOMOutputSpecArray]\n\n// JSX types for Tiptap's JSX runtime\n// These types only apply when using @jsxImportSource @tiptap/core\n// eslint-disable-next-line @typescript-eslint/no-namespace\nexport namespace JSX {\n export type Element = DOMOutputSpecArray\n export interface IntrinsicElements {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [key: string]: any\n }\n export interface ElementChildrenAttribute {\n children: unknown\n }\n}\n\nexport type JSXRenderer = (\n tag: 'slot' | string | ((props?: Attributes) => DOMOutputSpecArray | DOMOutputSpecElement),\n props?: Attributes,\n ...children: JSXRenderer[]\n) => DOMOutputSpecArray | DOMOutputSpecElement\n\nexport function Fragment(props: { children: JSXRenderer[] }) {\n return props.children\n}\n\nexport const h: JSXRenderer = (tag, attributes) => {\n // Treat the slot tag as the Prosemirror hole to render content into\n if (tag === 'slot') {\n return 0\n }\n\n // If the tag is a function, call it with the props\n if (tag instanceof Function) {\n return tag(attributes)\n }\n\n const { children, ...rest } = attributes ?? {}\n\n if (tag === 'svg') {\n throw new Error('SVG elements are not supported in the JSX syntax, use the array syntax instead')\n }\n\n // Otherwise, return the tag, attributes, and children\n return [tag, rest, children]\n}\n\n// See\n// https://esbuild.github.io/api/#jsx-import-source\n// https://www.typescriptlang.org/tsconfig/#jsxImportSource\n\nexport { h as createElement, h as jsx, h as jsxDEV, h as jsxs }\n"],"mappings":";AAmCO,SAAS,SAAS,OAAoC;AAC3D,SAAO,MAAM;AACf;AAEO,IAAM,IAAiB,CAAC,KAAK,eAAe;AAEjD,MAAI,QAAQ,QAAQ;AAClB,WAAO;AAAA,EACT;AAGA,MAAI,eAAe,UAAU;AAC3B,WAAO,IAAI,UAAU;AAAA,EACvB;AAEA,QAAM,EAAE,UAAU,GAAG,KAAK,IAAI,kCAAc,CAAC;AAE7C,MAAI,QAAQ,OAAO;AACjB,UAAM,IAAI,MAAM,gFAAgF;AAAA,EAClG;AAGA,SAAO,CAAC,KAAK,MAAM,QAAQ;AAC7B;","names":[]}
|
||||
1
node_modules/@tiptap/core/jsx-dev-runtime/index.cjs
generated
vendored
Normal file
1
node_modules/@tiptap/core/jsx-dev-runtime/index.cjs
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require('../dist/jsx-runtime/jsx-runtime.cjs')
|
||||
1
node_modules/@tiptap/core/jsx-dev-runtime/index.d.cts
generated
vendored
Normal file
1
node_modules/@tiptap/core/jsx-dev-runtime/index.d.cts
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export * from '../src/jsx-runtime.ts'
|
||||
1
node_modules/@tiptap/core/jsx-dev-runtime/index.d.ts
generated
vendored
Normal file
1
node_modules/@tiptap/core/jsx-dev-runtime/index.d.ts
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export type * from '../src/jsx-runtime.js'
|
||||
1
node_modules/@tiptap/core/jsx-dev-runtime/index.js
generated
vendored
Normal file
1
node_modules/@tiptap/core/jsx-dev-runtime/index.js
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export * from '../dist/jsx-runtime/jsx-runtime.js'
|
||||
1
node_modules/@tiptap/core/jsx-runtime/index.cjs
generated
vendored
Normal file
1
node_modules/@tiptap/core/jsx-runtime/index.cjs
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require('../dist/jsx-runtime/jsx-runtime.cjs')
|
||||
1
node_modules/@tiptap/core/jsx-runtime/index.d.cts
generated
vendored
Normal file
1
node_modules/@tiptap/core/jsx-runtime/index.d.cts
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export * from '../src/jsx-runtime.ts'
|
||||
1
node_modules/@tiptap/core/jsx-runtime/index.d.ts
generated
vendored
Normal file
1
node_modules/@tiptap/core/jsx-runtime/index.d.ts
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export type * from '../src/jsx-runtime.ts'
|
||||
1
node_modules/@tiptap/core/jsx-runtime/index.js
generated
vendored
Normal file
1
node_modules/@tiptap/core/jsx-runtime/index.js
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export * from '../dist/jsx-runtime/jsx-runtime.js'
|
||||
70
node_modules/@tiptap/core/package.json
generated
vendored
Normal file
70
node_modules/@tiptap/core/package.json
generated
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"name": "@tiptap/core",
|
||||
"description": "headless rich text editor",
|
||||
"version": "3.21.0",
|
||||
"homepage": "https://tiptap.dev",
|
||||
"keywords": [
|
||||
"tiptap",
|
||||
"headless",
|
||||
"wysiwyg",
|
||||
"text editor",
|
||||
"prosemirror"
|
||||
],
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": {
|
||||
"import": "./dist/index.d.ts",
|
||||
"require": "./dist/index.d.cts"
|
||||
},
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"./jsx-runtime": {
|
||||
"types": {
|
||||
"import": "./jsx-runtime/index.d.ts",
|
||||
"require": "./jsx-runtime/index.d.cts"
|
||||
},
|
||||
"import": "./jsx-runtime/index.js",
|
||||
"require": "./jsx-runtime/index.cjs"
|
||||
},
|
||||
"./jsx-dev-runtime": {
|
||||
"types": {
|
||||
"import": "./jsx-dev-runtime/index.d.ts",
|
||||
"require": "./jsx-dev-runtime/index.d.cts"
|
||||
},
|
||||
"import": "./jsx-dev-runtime/index.js",
|
||||
"require": "./jsx-dev-runtime/index.cjs"
|
||||
}
|
||||
},
|
||||
"main": "dist/index.cjs",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"src",
|
||||
"dist",
|
||||
"jsx-runtime",
|
||||
"jsx-dev-runtime"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@tiptap/pm": "^3.21.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/pm": "^3.21.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ueberdosis/tiptap",
|
||||
"directory": "packages/core"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"lint": "prettier ./src/ --check && eslint --cache --quiet --no-error-on-unmatched-pattern ./src/"
|
||||
}
|
||||
}
|
||||
138
node_modules/@tiptap/core/src/CommandManager.ts
generated
vendored
Normal file
138
node_modules/@tiptap/core/src/CommandManager.ts
generated
vendored
Normal file
@@ -0,0 +1,138 @@
|
||||
import type { EditorState, Transaction } from '@tiptap/pm/state'
|
||||
|
||||
import type { Editor } from './Editor.js'
|
||||
import { createChainableState } from './helpers/createChainableState.js'
|
||||
import type { AnyCommands, CanCommands, ChainedCommands, CommandProps, SingleCommands } from './types.js'
|
||||
|
||||
export class CommandManager {
|
||||
editor: Editor
|
||||
|
||||
rawCommands: AnyCommands
|
||||
|
||||
customState?: EditorState
|
||||
|
||||
constructor(props: { editor: Editor; state?: EditorState }) {
|
||||
this.editor = props.editor
|
||||
this.rawCommands = this.editor.extensionManager.commands
|
||||
this.customState = props.state
|
||||
}
|
||||
|
||||
get hasCustomState(): boolean {
|
||||
return !!this.customState
|
||||
}
|
||||
|
||||
get state(): EditorState {
|
||||
return this.customState || this.editor.state
|
||||
}
|
||||
|
||||
get commands(): SingleCommands {
|
||||
const { rawCommands, editor, state } = this
|
||||
const { view } = editor
|
||||
const { tr } = state
|
||||
const props = this.buildProps(tr)
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(rawCommands).map(([name, command]) => {
|
||||
const method = (...args: any[]) => {
|
||||
const callback = command(...args)(props)
|
||||
|
||||
if (!tr.getMeta('preventDispatch') && !this.hasCustomState) {
|
||||
view.dispatch(tr)
|
||||
}
|
||||
|
||||
return callback
|
||||
}
|
||||
|
||||
return [name, method]
|
||||
}),
|
||||
) as unknown as SingleCommands
|
||||
}
|
||||
|
||||
get chain(): () => ChainedCommands {
|
||||
return () => this.createChain()
|
||||
}
|
||||
|
||||
get can(): () => CanCommands {
|
||||
return () => this.createCan()
|
||||
}
|
||||
|
||||
public createChain(startTr?: Transaction, shouldDispatch = true): ChainedCommands {
|
||||
const { rawCommands, editor, state } = this
|
||||
const { view } = editor
|
||||
const callbacks: boolean[] = []
|
||||
const hasStartTransaction = !!startTr
|
||||
const tr = startTr || state.tr
|
||||
|
||||
const run = () => {
|
||||
if (!hasStartTransaction && shouldDispatch && !tr.getMeta('preventDispatch') && !this.hasCustomState) {
|
||||
view.dispatch(tr)
|
||||
}
|
||||
|
||||
return callbacks.every(callback => callback === true)
|
||||
}
|
||||
|
||||
const chain = {
|
||||
...Object.fromEntries(
|
||||
Object.entries(rawCommands).map(([name, command]) => {
|
||||
const chainedCommand = (...args: never[]) => {
|
||||
const props = this.buildProps(tr, shouldDispatch)
|
||||
const callback = command(...args)(props)
|
||||
|
||||
callbacks.push(callback)
|
||||
|
||||
return chain
|
||||
}
|
||||
|
||||
return [name, chainedCommand]
|
||||
}),
|
||||
),
|
||||
run,
|
||||
} as unknown as ChainedCommands
|
||||
|
||||
return chain
|
||||
}
|
||||
|
||||
public createCan(startTr?: Transaction): CanCommands {
|
||||
const { rawCommands, state } = this
|
||||
const dispatch = false
|
||||
const tr = startTr || state.tr
|
||||
const props = this.buildProps(tr, dispatch)
|
||||
const formattedCommands = Object.fromEntries(
|
||||
Object.entries(rawCommands).map(([name, command]) => {
|
||||
return [name, (...args: never[]) => command(...args)({ ...props, dispatch: undefined })]
|
||||
}),
|
||||
) as unknown as SingleCommands
|
||||
|
||||
return {
|
||||
...formattedCommands,
|
||||
chain: () => this.createChain(tr, dispatch),
|
||||
} as CanCommands
|
||||
}
|
||||
|
||||
public buildProps(tr: Transaction, shouldDispatch = true): CommandProps {
|
||||
const { rawCommands, editor, state } = this
|
||||
const { view } = editor
|
||||
|
||||
const props: CommandProps = {
|
||||
tr,
|
||||
editor,
|
||||
view,
|
||||
state: createChainableState({
|
||||
state,
|
||||
transaction: tr,
|
||||
}),
|
||||
dispatch: shouldDispatch ? () => undefined : undefined,
|
||||
chain: () => this.createChain(tr, shouldDispatch),
|
||||
can: () => this.createCan(tr),
|
||||
get commands() {
|
||||
return Object.fromEntries(
|
||||
Object.entries(rawCommands).map(([name, command]) => {
|
||||
return [name, (...args: never[]) => command(...args)(props)]
|
||||
}),
|
||||
) as unknown as SingleCommands
|
||||
},
|
||||
}
|
||||
|
||||
return props
|
||||
}
|
||||
}
|
||||
806
node_modules/@tiptap/core/src/Editor.ts
generated
vendored
Normal file
806
node_modules/@tiptap/core/src/Editor.ts
generated
vendored
Normal file
@@ -0,0 +1,806 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-object-type */
|
||||
import type { MarkType, Node as ProseMirrorNode, NodeType, Schema } from '@tiptap/pm/model'
|
||||
import type { Plugin, PluginKey, Transaction } from '@tiptap/pm/state'
|
||||
import { EditorState } from '@tiptap/pm/state'
|
||||
import { type DirectEditorProps, EditorView } from '@tiptap/pm/view'
|
||||
|
||||
import { CommandManager } from './CommandManager.js'
|
||||
import { EventEmitter } from './EventEmitter.js'
|
||||
import { ExtensionManager } from './ExtensionManager.js'
|
||||
import {
|
||||
ClipboardTextSerializer,
|
||||
Commands,
|
||||
Delete,
|
||||
Drop,
|
||||
Editable,
|
||||
FocusEvents,
|
||||
Keymap,
|
||||
Paste,
|
||||
Tabindex,
|
||||
TextDirection,
|
||||
} from './extensions/index.js'
|
||||
import { createDocument } from './helpers/createDocument.js'
|
||||
import { getAttributes } from './helpers/getAttributes.js'
|
||||
import { getHTMLFromFragment } from './helpers/getHTMLFromFragment.js'
|
||||
import { getText } from './helpers/getText.js'
|
||||
import { getTextSerializersFromSchema } from './helpers/getTextSerializersFromSchema.js'
|
||||
import { isActive } from './helpers/isActive.js'
|
||||
import { isNodeEmpty } from './helpers/isNodeEmpty.js'
|
||||
import { createMappablePosition, getUpdatedPosition } from './helpers/MappablePosition.js'
|
||||
import { resolveFocusPosition } from './helpers/resolveFocusPosition.js'
|
||||
import type { Storage } from './index.js'
|
||||
import { NodePos } from './NodePos.js'
|
||||
import { style } from './style.js'
|
||||
import type {
|
||||
CanCommands,
|
||||
ChainedCommands,
|
||||
DocumentType,
|
||||
EditorEvents,
|
||||
EditorOptions,
|
||||
NodeType as TNodeType,
|
||||
SingleCommands,
|
||||
TextSerializer,
|
||||
TextType as TTextType,
|
||||
Utils,
|
||||
} from './types.js'
|
||||
import { createStyleTag } from './utilities/createStyleTag.js'
|
||||
import { isFunction } from './utilities/isFunction.js'
|
||||
|
||||
export * as extensions from './extensions/index.js'
|
||||
|
||||
// @ts-ignore
|
||||
export interface TiptapEditorHTMLElement extends HTMLElement {
|
||||
editor?: Editor
|
||||
}
|
||||
|
||||
export class Editor extends EventEmitter<EditorEvents> {
|
||||
private commandManager!: CommandManager
|
||||
|
||||
public extensionManager!: ExtensionManager
|
||||
|
||||
private css: HTMLStyleElement | null = null
|
||||
|
||||
private className = 'tiptap'
|
||||
|
||||
public schema!: Schema
|
||||
|
||||
private editorView: EditorView | null = null
|
||||
|
||||
public isFocused = false
|
||||
|
||||
private editorState!: EditorState
|
||||
|
||||
/**
|
||||
* The editor is considered initialized after the `create` event has been emitted.
|
||||
*/
|
||||
public isInitialized = false
|
||||
|
||||
public extensionStorage: Storage = {} as Storage
|
||||
|
||||
/**
|
||||
* A unique ID for this editor instance.
|
||||
*/
|
||||
public instanceId = Math.random().toString(36).slice(2, 9)
|
||||
|
||||
public options: EditorOptions = {
|
||||
element: typeof document !== 'undefined' ? document.createElement('div') : null,
|
||||
content: '',
|
||||
injectCSS: true,
|
||||
injectNonce: undefined,
|
||||
extensions: [],
|
||||
autofocus: false,
|
||||
editable: true,
|
||||
textDirection: undefined,
|
||||
editorProps: {},
|
||||
parseOptions: {},
|
||||
coreExtensionOptions: {},
|
||||
enableInputRules: true,
|
||||
enablePasteRules: true,
|
||||
enableCoreExtensions: true,
|
||||
enableContentCheck: false,
|
||||
emitContentError: false,
|
||||
onBeforeCreate: () => null,
|
||||
onCreate: () => null,
|
||||
onMount: () => null,
|
||||
onUnmount: () => null,
|
||||
onUpdate: () => null,
|
||||
onSelectionUpdate: () => null,
|
||||
onTransaction: () => null,
|
||||
onFocus: () => null,
|
||||
onBlur: () => null,
|
||||
onDestroy: () => null,
|
||||
onContentError: ({ error }) => {
|
||||
throw error
|
||||
},
|
||||
onPaste: () => null,
|
||||
onDrop: () => null,
|
||||
onDelete: () => null,
|
||||
enableExtensionDispatchTransaction: true,
|
||||
}
|
||||
|
||||
constructor(options: Partial<EditorOptions> = {}) {
|
||||
super()
|
||||
this.setOptions(options)
|
||||
this.createExtensionManager()
|
||||
this.createCommandManager()
|
||||
this.createSchema()
|
||||
this.on('beforeCreate', this.options.onBeforeCreate)
|
||||
this.emit('beforeCreate', { editor: this })
|
||||
this.on('mount', this.options.onMount)
|
||||
this.on('unmount', this.options.onUnmount)
|
||||
this.on('contentError', this.options.onContentError)
|
||||
this.on('create', this.options.onCreate)
|
||||
this.on('update', this.options.onUpdate)
|
||||
this.on('selectionUpdate', this.options.onSelectionUpdate)
|
||||
this.on('transaction', this.options.onTransaction)
|
||||
this.on('focus', this.options.onFocus)
|
||||
this.on('blur', this.options.onBlur)
|
||||
this.on('destroy', this.options.onDestroy)
|
||||
this.on('drop', ({ event, slice, moved }) => this.options.onDrop(event, slice, moved))
|
||||
this.on('paste', ({ event, slice }) => this.options.onPaste(event, slice))
|
||||
this.on('delete', this.options.onDelete)
|
||||
|
||||
const initialDoc = this.createDoc()
|
||||
const selection = resolveFocusPosition(initialDoc, this.options.autofocus)
|
||||
|
||||
// Set editor state immediately, so that it's available independently from the view
|
||||
this.editorState = EditorState.create({
|
||||
doc: initialDoc,
|
||||
schema: this.schema,
|
||||
selection: selection || undefined,
|
||||
})
|
||||
|
||||
if (this.options.element) {
|
||||
this.mount(this.options.element)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach the editor to the DOM, creating a new editor view.
|
||||
*/
|
||||
public mount(el: NonNullable<EditorOptions['element']> & {}) {
|
||||
if (typeof document === 'undefined') {
|
||||
throw new Error(
|
||||
`[tiptap error]: The editor cannot be mounted because there is no 'document' defined in this environment.`,
|
||||
)
|
||||
}
|
||||
this.createView(el)
|
||||
this.emit('mount', { editor: this })
|
||||
|
||||
if (this.css && !document.head.contains(this.css)) {
|
||||
document.head.appendChild(this.css)
|
||||
}
|
||||
|
||||
window.setTimeout(() => {
|
||||
if (this.isDestroyed) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.options.autofocus !== false && this.options.autofocus !== null) {
|
||||
this.commands.focus(this.options.autofocus)
|
||||
}
|
||||
this.emit('create', { editor: this })
|
||||
this.isInitialized = true
|
||||
}, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the editor from the DOM, but still allow remounting at a different point in time
|
||||
*/
|
||||
public unmount() {
|
||||
if (this.editorView) {
|
||||
// Cleanup our reference to prevent circular references which caused memory leaks
|
||||
// @ts-ignore
|
||||
const dom = this.editorView.dom as TiptapEditorHTMLElement
|
||||
|
||||
if (dom?.editor) {
|
||||
delete dom.editor
|
||||
}
|
||||
this.editorView.destroy()
|
||||
}
|
||||
this.editorView = null
|
||||
this.isInitialized = false
|
||||
|
||||
// Safely remove CSS element with fallback for test environments
|
||||
// Only remove CSS if no other editors exist in the document after unmount
|
||||
if (this.css && !document.querySelectorAll(`.${this.className}`).length) {
|
||||
try {
|
||||
if (typeof this.css.remove === 'function') {
|
||||
this.css.remove()
|
||||
} else if (this.css.parentNode) {
|
||||
this.css.parentNode.removeChild(this.css)
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently handle any unexpected DOM removal errors in test environments
|
||||
console.warn('Failed to remove CSS element:', error)
|
||||
}
|
||||
}
|
||||
this.css = null
|
||||
this.emit('unmount', { editor: this })
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the editor storage.
|
||||
*/
|
||||
public get storage(): Storage {
|
||||
return this.extensionStorage
|
||||
}
|
||||
|
||||
/**
|
||||
* An object of all registered commands.
|
||||
*/
|
||||
public get commands(): SingleCommands {
|
||||
return this.commandManager.commands
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a command chain to call multiple commands at once.
|
||||
*/
|
||||
public chain(): ChainedCommands {
|
||||
return this.commandManager.chain()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a command or a command chain can be executed. Without executing it.
|
||||
*/
|
||||
public can(): CanCommands {
|
||||
return this.commandManager.can()
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject CSS styles.
|
||||
*/
|
||||
private injectCSS(): void {
|
||||
if (this.options.injectCSS && typeof document !== 'undefined') {
|
||||
this.css = createStyleTag(style, this.options.injectNonce)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update editor options.
|
||||
*
|
||||
* @param options A list of options
|
||||
*/
|
||||
public setOptions(options: Partial<EditorOptions> = {}): void {
|
||||
this.options = {
|
||||
...this.options,
|
||||
...options,
|
||||
}
|
||||
|
||||
if (!this.editorView || !this.state || this.isDestroyed) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.options.editorProps) {
|
||||
this.view.setProps(this.options.editorProps)
|
||||
}
|
||||
|
||||
this.view.updateState(this.state)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update editable state of the editor.
|
||||
*/
|
||||
public setEditable(editable: boolean, emitUpdate = true): void {
|
||||
this.setOptions({ editable })
|
||||
|
||||
if (emitUpdate) {
|
||||
this.emit('update', { editor: this, transaction: this.state.tr, appendedTransactions: [] })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the editor is editable.
|
||||
*/
|
||||
public get isEditable(): boolean {
|
||||
// since plugins are applied after creating the view
|
||||
// `editable` is always `true` for one tick.
|
||||
// that’s why we also have to check for `options.editable`
|
||||
return this.options.editable && this.view && this.view.editable
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the editor view.
|
||||
*/
|
||||
public get view(): EditorView {
|
||||
if (this.editorView) {
|
||||
return this.editorView
|
||||
}
|
||||
|
||||
return new Proxy(
|
||||
{
|
||||
state: this.editorState,
|
||||
updateState: (state: EditorState): ReturnType<EditorView['updateState']> => {
|
||||
this.editorState = state
|
||||
},
|
||||
dispatch: (tr: Transaction): ReturnType<EditorView['dispatch']> => {
|
||||
this.dispatchTransaction(tr)
|
||||
},
|
||||
|
||||
// Stub some commonly accessed properties to prevent errors
|
||||
composing: false,
|
||||
dragging: null,
|
||||
editable: true,
|
||||
isDestroyed: false,
|
||||
} as EditorView,
|
||||
{
|
||||
get: (obj, key) => {
|
||||
if (this.editorView) {
|
||||
// If the editor view is available, but the caller has a stale reference to the proxy,
|
||||
// Just return what the editor view has.
|
||||
return this.editorView[key as keyof EditorView]
|
||||
}
|
||||
// Specifically always return the most recent editorState
|
||||
if (key === 'state') {
|
||||
return this.editorState
|
||||
}
|
||||
if (key in obj) {
|
||||
return Reflect.get(obj, key)
|
||||
}
|
||||
|
||||
// We throw an error here, because we know the view is not available
|
||||
throw new Error(
|
||||
`[tiptap error]: The editor view is not available. Cannot access view['${key as string}']. The editor may not be mounted yet.`,
|
||||
)
|
||||
},
|
||||
},
|
||||
) as EditorView
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the editor state.
|
||||
*/
|
||||
public get state(): EditorState {
|
||||
if (this.editorView) {
|
||||
this.editorState = this.view.state
|
||||
}
|
||||
|
||||
return this.editorState
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a ProseMirror plugin.
|
||||
*
|
||||
* @param plugin A ProseMirror plugin
|
||||
* @param handlePlugins Control how to merge the plugin into the existing plugins.
|
||||
* @returns The new editor state
|
||||
*/
|
||||
public registerPlugin(
|
||||
plugin: Plugin,
|
||||
handlePlugins?: (newPlugin: Plugin, plugins: Plugin[]) => Plugin[],
|
||||
): EditorState {
|
||||
const plugins = isFunction(handlePlugins)
|
||||
? handlePlugins(plugin, [...this.state.plugins])
|
||||
: [...this.state.plugins, plugin]
|
||||
|
||||
const state = this.state.reconfigure({ plugins })
|
||||
|
||||
this.view.updateState(state)
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a ProseMirror plugin.
|
||||
*
|
||||
* @param nameOrPluginKeyToRemove The plugins name
|
||||
* @returns The new editor state or undefined if the editor is destroyed
|
||||
*/
|
||||
public unregisterPlugin(
|
||||
nameOrPluginKeyToRemove: string | PluginKey | (string | PluginKey)[],
|
||||
): EditorState | undefined {
|
||||
if (this.isDestroyed) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const prevPlugins = this.state.plugins
|
||||
let plugins = prevPlugins
|
||||
|
||||
;([] as (string | PluginKey)[]).concat(nameOrPluginKeyToRemove).forEach(nameOrPluginKey => {
|
||||
// @ts-ignore
|
||||
const name = typeof nameOrPluginKey === 'string' ? `${nameOrPluginKey}$` : nameOrPluginKey.key
|
||||
|
||||
// @ts-ignore
|
||||
plugins = plugins.filter(plugin => !plugin.key.startsWith(name))
|
||||
})
|
||||
|
||||
if (prevPlugins.length === plugins.length) {
|
||||
// No plugin was removed, so we don’t need to update the state
|
||||
return undefined
|
||||
}
|
||||
|
||||
const state = this.state.reconfigure({
|
||||
plugins,
|
||||
})
|
||||
|
||||
this.view.updateState(state)
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an extension manager.
|
||||
*/
|
||||
private createExtensionManager(): void {
|
||||
const coreExtensions = this.options.enableCoreExtensions
|
||||
? [
|
||||
Editable,
|
||||
ClipboardTextSerializer.configure({
|
||||
blockSeparator: this.options.coreExtensionOptions?.clipboardTextSerializer?.blockSeparator,
|
||||
}),
|
||||
Commands,
|
||||
FocusEvents,
|
||||
Keymap,
|
||||
Tabindex,
|
||||
Drop,
|
||||
Paste,
|
||||
Delete,
|
||||
TextDirection.configure({
|
||||
direction: this.options.textDirection,
|
||||
}),
|
||||
].filter(ext => {
|
||||
if (typeof this.options.enableCoreExtensions === 'object') {
|
||||
return (
|
||||
this.options.enableCoreExtensions[ext.name as keyof typeof this.options.enableCoreExtensions] !== false
|
||||
)
|
||||
}
|
||||
return true
|
||||
})
|
||||
: []
|
||||
const allExtensions = [...coreExtensions, ...this.options.extensions].filter(extension => {
|
||||
return ['extension', 'node', 'mark'].includes(extension?.type)
|
||||
})
|
||||
|
||||
this.extensionManager = new ExtensionManager(allExtensions, this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an command manager.
|
||||
*/
|
||||
private createCommandManager(): void {
|
||||
this.commandManager = new CommandManager({
|
||||
editor: this,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a ProseMirror schema.
|
||||
*/
|
||||
private createSchema(): void {
|
||||
this.schema = this.extensionManager.schema
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the initial document.
|
||||
*/
|
||||
private createDoc(): ProseMirrorNode {
|
||||
let doc: ProseMirrorNode
|
||||
|
||||
try {
|
||||
doc = createDocument(this.options.content, this.schema, this.options.parseOptions, {
|
||||
errorOnInvalidContent: this.options.enableContentCheck,
|
||||
})
|
||||
} catch (e) {
|
||||
if (
|
||||
!(e instanceof Error) ||
|
||||
!['[tiptap error]: Invalid JSON content', '[tiptap error]: Invalid HTML content'].includes(e.message)
|
||||
) {
|
||||
// Not the content error we were expecting
|
||||
throw e
|
||||
}
|
||||
this.emit('contentError', {
|
||||
editor: this,
|
||||
error: e as Error,
|
||||
disableCollaboration: () => {
|
||||
if (
|
||||
'collaboration' in this.storage &&
|
||||
typeof this.storage.collaboration === 'object' &&
|
||||
this.storage.collaboration
|
||||
) {
|
||||
;(this.storage.collaboration as any).isDisabled = true
|
||||
}
|
||||
// To avoid syncing back invalid content, reinitialize the extensions without the collaboration extension
|
||||
this.options.extensions = this.options.extensions.filter(extension => extension.name !== 'collaboration')
|
||||
|
||||
// Restart the initialization process by recreating the extension manager with the new set of extensions
|
||||
this.createExtensionManager()
|
||||
},
|
||||
})
|
||||
|
||||
// Content is invalid, but attempt to create it anyway, stripping out the invalid parts
|
||||
doc = createDocument(this.options.content, this.schema, this.options.parseOptions, {
|
||||
errorOnInvalidContent: false,
|
||||
})
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a ProseMirror view.
|
||||
*/
|
||||
private createView(element: NonNullable<EditorOptions['element']>): void {
|
||||
const { editorProps, enableExtensionDispatchTransaction } = this.options
|
||||
// If a user provided a custom `dispatchTransaction` through `editorProps`,
|
||||
// we use that as the base dispatch function.
|
||||
// Otherwise, we use Tiptap's internal `dispatchTransaction` method.
|
||||
const baseDispatch = (editorProps as DirectEditorProps).dispatchTransaction || this.dispatchTransaction.bind(this)
|
||||
const dispatch = enableExtensionDispatchTransaction
|
||||
? this.extensionManager.dispatchTransaction(baseDispatch)
|
||||
: baseDispatch
|
||||
|
||||
// Compose transformPastedHTML from extensions and user-provided editorProps
|
||||
const baseTransformPastedHTML = (editorProps as DirectEditorProps).transformPastedHTML
|
||||
const transformPastedHTML = this.extensionManager.transformPastedHTML(baseTransformPastedHTML)
|
||||
|
||||
this.editorView = new EditorView(element, {
|
||||
...editorProps,
|
||||
attributes: {
|
||||
// add `role="textbox"` to the editor element
|
||||
role: 'textbox',
|
||||
...editorProps?.attributes,
|
||||
},
|
||||
dispatchTransaction: dispatch,
|
||||
transformPastedHTML,
|
||||
state: this.editorState,
|
||||
markViews: this.extensionManager.markViews,
|
||||
nodeViews: this.extensionManager.nodeViews,
|
||||
})
|
||||
|
||||
// `editor.view` is not yet available at this time.
|
||||
// Therefore we will add all plugins and node views directly afterwards.
|
||||
const newState = this.state.reconfigure({
|
||||
plugins: this.extensionManager.plugins,
|
||||
})
|
||||
|
||||
this.view.updateState(newState)
|
||||
|
||||
this.prependClass()
|
||||
this.injectCSS()
|
||||
|
||||
// Let’s store the editor instance in the DOM element.
|
||||
// So we’ll have access to it for tests.
|
||||
// @ts-ignore
|
||||
const dom = this.view.dom as TiptapEditorHTMLElement
|
||||
|
||||
dom.editor = this
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates all node and mark views.
|
||||
*/
|
||||
public createNodeViews(): void {
|
||||
if (this.view.isDestroyed) {
|
||||
return
|
||||
}
|
||||
|
||||
this.view.setProps({
|
||||
markViews: this.extensionManager.markViews,
|
||||
nodeViews: this.extensionManager.nodeViews,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepend class name to element.
|
||||
*/
|
||||
public prependClass(): void {
|
||||
this.view.dom.className = `${this.className} ${this.view.dom.className}`
|
||||
}
|
||||
|
||||
public isCapturingTransaction = false
|
||||
|
||||
private capturedTransaction: Transaction | null = null
|
||||
|
||||
public captureTransaction(fn: () => void) {
|
||||
this.isCapturingTransaction = true
|
||||
fn()
|
||||
this.isCapturingTransaction = false
|
||||
|
||||
const tr = this.capturedTransaction
|
||||
|
||||
this.capturedTransaction = null
|
||||
|
||||
return tr
|
||||
}
|
||||
|
||||
/**
|
||||
* The callback over which to send transactions (state updates) produced by the view.
|
||||
*
|
||||
* @param transaction An editor state transaction
|
||||
*/
|
||||
private dispatchTransaction(transaction: Transaction): void {
|
||||
// if the editor / the view of the editor was destroyed
|
||||
// the transaction should not be dispatched as there is no view anymore.
|
||||
if (this.view.isDestroyed) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.isCapturingTransaction) {
|
||||
if (!this.capturedTransaction) {
|
||||
this.capturedTransaction = transaction
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
transaction.steps.forEach(step => this.capturedTransaction?.step(step))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Apply transaction and get resulting state and transactions
|
||||
const { state, transactions } = this.state.applyTransaction(transaction)
|
||||
const selectionHasChanged = !this.state.selection.eq(state.selection)
|
||||
const rootTrWasApplied = transactions.includes(transaction)
|
||||
const prevState = this.state
|
||||
|
||||
this.emit('beforeTransaction', {
|
||||
editor: this,
|
||||
transaction,
|
||||
nextState: state,
|
||||
})
|
||||
|
||||
// If transaction was filtered out, we can return early
|
||||
if (!rootTrWasApplied) {
|
||||
return
|
||||
}
|
||||
|
||||
this.view.updateState(state)
|
||||
|
||||
// Emit transaction event with appended transactions info
|
||||
this.emit('transaction', {
|
||||
editor: this,
|
||||
transaction,
|
||||
appendedTransactions: transactions.slice(1),
|
||||
})
|
||||
|
||||
if (selectionHasChanged) {
|
||||
this.emit('selectionUpdate', {
|
||||
editor: this,
|
||||
transaction,
|
||||
})
|
||||
}
|
||||
|
||||
// Only emit the latest between focus and blur events
|
||||
const mostRecentFocusTr = transactions.findLast(tr => tr.getMeta('focus') || tr.getMeta('blur'))
|
||||
const focus = mostRecentFocusTr?.getMeta('focus')
|
||||
const blur = mostRecentFocusTr?.getMeta('blur')
|
||||
|
||||
if (focus) {
|
||||
this.emit('focus', {
|
||||
editor: this,
|
||||
event: focus.event,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
transaction: mostRecentFocusTr!,
|
||||
})
|
||||
}
|
||||
|
||||
if (blur) {
|
||||
this.emit('blur', {
|
||||
editor: this,
|
||||
event: blur.event,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
transaction: mostRecentFocusTr!,
|
||||
})
|
||||
}
|
||||
|
||||
// Compare states for update event
|
||||
if (
|
||||
transaction.getMeta('preventUpdate') ||
|
||||
!transactions.some(tr => tr.docChanged) ||
|
||||
prevState.doc.eq(state.doc)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
this.emit('update', {
|
||||
editor: this,
|
||||
transaction,
|
||||
appendedTransactions: transactions.slice(1),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attributes of the currently selected node or mark.
|
||||
*/
|
||||
public getAttributes(nameOrType: string | NodeType | MarkType): Record<string, any> {
|
||||
return getAttributes(this.state, nameOrType)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the currently selected node or mark is active.
|
||||
*
|
||||
* @param name Name of the node or mark
|
||||
* @param attributes Attributes of the node or mark
|
||||
*/
|
||||
public isActive(name: string, attributes?: {}): boolean
|
||||
public isActive(attributes: {}): boolean
|
||||
public isActive(nameOrAttributes: string, attributesOrUndefined?: {}): boolean {
|
||||
const name = typeof nameOrAttributes === 'string' ? nameOrAttributes : null
|
||||
|
||||
const attributes = typeof nameOrAttributes === 'string' ? attributesOrUndefined : nameOrAttributes
|
||||
|
||||
return isActive(this.state, name, attributes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the document as JSON.
|
||||
*/
|
||||
public getJSON(): DocumentType<
|
||||
Record<string, any> | undefined,
|
||||
TNodeType<string, undefined | Record<string, any>, any, (TNodeType | TTextType)[]>[]
|
||||
> {
|
||||
return this.state.doc.toJSON()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the document as HTML.
|
||||
*/
|
||||
public getHTML(): string {
|
||||
return getHTMLFromFragment(this.state.doc.content, this.schema)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the document as text.
|
||||
*/
|
||||
public getText(options?: { blockSeparator?: string; textSerializers?: Record<string, TextSerializer> }): string {
|
||||
const { blockSeparator = '\n\n', textSerializers = {} } = options || {}
|
||||
|
||||
return getText(this.state.doc, {
|
||||
blockSeparator,
|
||||
textSerializers: {
|
||||
...getTextSerializersFromSchema(this.schema),
|
||||
...textSerializers,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is no content.
|
||||
*/
|
||||
public get isEmpty(): boolean {
|
||||
return isNodeEmpty(this.state.doc)
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the editor.
|
||||
*/
|
||||
public destroy(): void {
|
||||
this.emit('destroy')
|
||||
|
||||
this.unmount()
|
||||
|
||||
this.removeAllListeners()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the editor is already destroyed.
|
||||
*/
|
||||
public get isDestroyed(): boolean {
|
||||
return this.editorView?.isDestroyed ?? true
|
||||
}
|
||||
|
||||
public $node(selector: string, attributes?: { [key: string]: any }): NodePos | null {
|
||||
return this.$doc?.querySelector(selector, attributes) || null
|
||||
}
|
||||
|
||||
public $nodes(selector: string, attributes?: { [key: string]: any }): NodePos[] | null {
|
||||
return this.$doc?.querySelectorAll(selector, attributes) || null
|
||||
}
|
||||
|
||||
public $pos(pos: number) {
|
||||
const $pos = this.state.doc.resolve(pos)
|
||||
|
||||
return new NodePos($pos, this)
|
||||
}
|
||||
|
||||
get $doc() {
|
||||
return this.$pos(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a set of utilities for working with positions and ranges.
|
||||
*/
|
||||
public utils: Utils = {
|
||||
getUpdatedPosition,
|
||||
createMappablePosition,
|
||||
}
|
||||
}
|
||||
58
node_modules/@tiptap/core/src/EventEmitter.ts
generated
vendored
Normal file
58
node_modules/@tiptap/core/src/EventEmitter.ts
generated
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
type StringKeyOf<T> = Extract<keyof T, string>
|
||||
type CallbackType<T extends Record<string, any>, EventName extends StringKeyOf<T>> = T[EventName] extends any[]
|
||||
? T[EventName]
|
||||
: [T[EventName]]
|
||||
type CallbackFunction<T extends Record<string, any>, EventName extends StringKeyOf<T>> = (
|
||||
...props: CallbackType<T, EventName>
|
||||
) => any
|
||||
|
||||
export class EventEmitter<T extends Record<string, any>> {
|
||||
private callbacks: { [key: string]: Array<(...args: any[]) => void> } = {}
|
||||
|
||||
public on<EventName extends StringKeyOf<T>>(event: EventName, fn: CallbackFunction<T, EventName>): this {
|
||||
if (!this.callbacks[event]) {
|
||||
this.callbacks[event] = []
|
||||
}
|
||||
|
||||
this.callbacks[event].push(fn)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public emit<EventName extends StringKeyOf<T>>(event: EventName, ...args: CallbackType<T, EventName>): this {
|
||||
const callbacks = this.callbacks[event]
|
||||
|
||||
if (callbacks) {
|
||||
callbacks.forEach(callback => callback.apply(this, args))
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public off<EventName extends StringKeyOf<T>>(event: EventName, fn?: CallbackFunction<T, EventName>): this {
|
||||
const callbacks = this.callbacks[event]
|
||||
|
||||
if (callbacks) {
|
||||
if (fn) {
|
||||
this.callbacks[event] = callbacks.filter(callback => callback !== fn)
|
||||
} else {
|
||||
delete this.callbacks[event]
|
||||
}
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public once<EventName extends StringKeyOf<T>>(event: EventName, fn: CallbackFunction<T, EventName>): this {
|
||||
const onceFn = (...args: CallbackType<T, EventName>) => {
|
||||
this.off(event, onceFn)
|
||||
fn.apply(this, args)
|
||||
}
|
||||
|
||||
return this.on(event, onceFn)
|
||||
}
|
||||
|
||||
public removeAllListeners(): void {
|
||||
this.callbacks = {}
|
||||
}
|
||||
}
|
||||
602
node_modules/@tiptap/core/src/Extendable.ts
generated
vendored
Normal file
602
node_modules/@tiptap/core/src/Extendable.ts
generated
vendored
Normal file
@@ -0,0 +1,602 @@
|
||||
import type { Plugin } from '@tiptap/pm/state'
|
||||
|
||||
import type { Editor } from './Editor.js'
|
||||
import { getExtensionField } from './helpers/getExtensionField.js'
|
||||
import type { ExtensionConfig, MarkConfig, NodeConfig } from './index.js'
|
||||
import type { InputRule } from './InputRule.js'
|
||||
import type { Mark } from './Mark.js'
|
||||
import type { Node } from './Node.js'
|
||||
import type { PasteRule } from './PasteRule.js'
|
||||
import type {
|
||||
AnyConfig,
|
||||
DispatchTransactionProps,
|
||||
EditorEvents,
|
||||
Extensions,
|
||||
GlobalAttributes,
|
||||
JSONContent,
|
||||
KeyboardShortcutCommand,
|
||||
MarkdownParseHelpers,
|
||||
MarkdownParseResult,
|
||||
MarkdownRendererHelpers,
|
||||
MarkdownToken,
|
||||
MarkdownTokenizer,
|
||||
ParentConfig,
|
||||
RawCommands,
|
||||
RenderContext,
|
||||
} from './types.js'
|
||||
import { callOrReturn } from './utilities/callOrReturn.js'
|
||||
import { mergeDeep } from './utilities/mergeDeep.js'
|
||||
|
||||
export interface ExtendableConfig<
|
||||
Options = any,
|
||||
Storage = any,
|
||||
Config extends
|
||||
| ExtensionConfig<Options, Storage>
|
||||
| NodeConfig<Options, Storage>
|
||||
| MarkConfig<Options, Storage>
|
||||
| ExtendableConfig<Options, Storage> = ExtendableConfig<Options, Storage, any, any>,
|
||||
PMType = any,
|
||||
> {
|
||||
/**
|
||||
* The extension name - this must be unique.
|
||||
* It will be used to identify the extension.
|
||||
*
|
||||
* @example 'myExtension'
|
||||
*/
|
||||
name: string
|
||||
|
||||
/**
|
||||
* The priority of your extension. The higher, the earlier it will be called
|
||||
* and will take precedence over other extensions with a lower priority.
|
||||
* @default 100
|
||||
* @example 101
|
||||
*/
|
||||
priority?: number
|
||||
|
||||
/**
|
||||
* This method will add options to this extension
|
||||
* @see https://tiptap.dev/docs/editor/guide/custom-extensions#settings
|
||||
* @example
|
||||
* addOptions() {
|
||||
* return {
|
||||
* myOption: 'foo',
|
||||
* myOtherOption: 10,
|
||||
* }
|
||||
*/
|
||||
addOptions?: (this: { name: string; parent: ParentConfig<Config>['addOptions'] }) => Options
|
||||
|
||||
/**
|
||||
* The default storage this extension can save data to.
|
||||
* @see https://tiptap.dev/docs/editor/guide/custom-extensions#storage
|
||||
* @example
|
||||
* defaultStorage: {
|
||||
* prefetchedUsers: [],
|
||||
* loading: false,
|
||||
* }
|
||||
*/
|
||||
addStorage?: (this: { name: string; options: Options; parent: ParentConfig<Config>['addStorage'] }) => Storage
|
||||
|
||||
/**
|
||||
* This function adds globalAttributes to specific nodes.
|
||||
* @see https://tiptap.dev/docs/editor/guide/custom-extensions#global-attributes
|
||||
* @example
|
||||
* addGlobalAttributes() {
|
||||
* return [
|
||||
* {
|
||||
// Extend the following extensions
|
||||
* types: [
|
||||
* 'heading',
|
||||
* 'paragraph',
|
||||
* ],
|
||||
* // … with those attributes
|
||||
* attributes: {
|
||||
* textAlign: {
|
||||
* default: 'left',
|
||||
* renderHTML: attributes => ({
|
||||
* style: `text-align: ${attributes.textAlign}`,
|
||||
* }),
|
||||
* parseHTML: element => element.style.textAlign || 'left',
|
||||
* },
|
||||
* },
|
||||
* },
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
addGlobalAttributes?: (this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
extensions: (Node | Mark)[]
|
||||
parent: ParentConfig<Config>['addGlobalAttributes']
|
||||
}) => GlobalAttributes
|
||||
|
||||
/**
|
||||
* This function adds commands to the editor
|
||||
* @see https://tiptap.dev/docs/editor/guide/custom-extensions#commands
|
||||
* @example
|
||||
* addCommands() {
|
||||
* return {
|
||||
* myCommand: () => ({ chain }) => chain().setMark('type', 'foo').run(),
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
addCommands?: (this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
editor: Editor
|
||||
type: PMType
|
||||
parent: ParentConfig<Config>['addCommands']
|
||||
}) => Partial<RawCommands>
|
||||
|
||||
/**
|
||||
* This function registers keyboard shortcuts.
|
||||
* @see https://tiptap.dev/docs/editor/guide/custom-extensions#keyboard-shortcuts
|
||||
* @example
|
||||
* addKeyboardShortcuts() {
|
||||
* return {
|
||||
* 'Mod-l': () => this.editor.commands.toggleBulletList(),
|
||||
* }
|
||||
* },
|
||||
*/
|
||||
addKeyboardShortcuts?: (this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
editor: Editor
|
||||
type: PMType
|
||||
parent: ParentConfig<Config>['addKeyboardShortcuts']
|
||||
}) => {
|
||||
[key: string]: KeyboardShortcutCommand
|
||||
}
|
||||
|
||||
/**
|
||||
* This function adds input rules to the editor.
|
||||
* @see https://tiptap.dev/docs/editor/guide/custom-extensions#input-rules
|
||||
* @example
|
||||
* addInputRules() {
|
||||
* return [
|
||||
* markInputRule({
|
||||
* find: inputRegex,
|
||||
* type: this.type,
|
||||
* }),
|
||||
* ]
|
||||
* },
|
||||
*/
|
||||
addInputRules?: (this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
editor: Editor
|
||||
type: PMType
|
||||
parent: ParentConfig<Config>['addInputRules']
|
||||
}) => InputRule[]
|
||||
|
||||
/**
|
||||
* This function adds paste rules to the editor.
|
||||
* @see https://tiptap.dev/docs/editor/guide/custom-extensions#paste-rules
|
||||
* @example
|
||||
* addPasteRules() {
|
||||
* return [
|
||||
* markPasteRule({
|
||||
* find: pasteRegex,
|
||||
* type: this.type,
|
||||
* }),
|
||||
* ]
|
||||
* },
|
||||
*/
|
||||
addPasteRules?: (this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
editor: Editor
|
||||
type: PMType
|
||||
parent: ParentConfig<Config>['addPasteRules']
|
||||
}) => PasteRule[]
|
||||
|
||||
/**
|
||||
* This function adds Prosemirror plugins to the editor
|
||||
* @see https://tiptap.dev/docs/editor/guide/custom-extensions#prosemirror-plugins
|
||||
* @example
|
||||
* addProseMirrorPlugins() {
|
||||
* return [
|
||||
* customPlugin(),
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
addProseMirrorPlugins?: (this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
editor: Editor
|
||||
type: PMType
|
||||
parent: ParentConfig<Config>['addProseMirrorPlugins']
|
||||
}) => Plugin[]
|
||||
|
||||
/**
|
||||
* This function transforms pasted HTML content before it's parsed.
|
||||
* Extensions can use this to modify or clean up pasted HTML.
|
||||
* The transformations are chained - each extension's transform receives
|
||||
* the output from the previous extension's transform.
|
||||
* @see https://tiptap.dev/docs/editor/guide/custom-extensions#transform-pasted-html
|
||||
* @example
|
||||
* transformPastedHTML(html) {
|
||||
* // Remove all style attributes
|
||||
* return html.replace(/style="[^"]*"/g, '')
|
||||
* }
|
||||
*/
|
||||
transformPastedHTML?: (
|
||||
this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
editor: Editor
|
||||
type: PMType
|
||||
parent: ParentConfig<Config>['transformPastedHTML']
|
||||
},
|
||||
html: string,
|
||||
) => string
|
||||
|
||||
/**
|
||||
* This function adds additional extensions to the editor. This is useful for
|
||||
* building extension kits.
|
||||
* @example
|
||||
* addExtensions() {
|
||||
* return [
|
||||
* BulletList,
|
||||
* OrderedList,
|
||||
* ListItem
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
addExtensions?: (this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
parent: ParentConfig<Config>['addExtensions']
|
||||
}) => Extensions
|
||||
|
||||
/**
|
||||
* The markdown token name
|
||||
*
|
||||
* This is the name of the token that this extension uses to parse and render markdown and comes from the Marked Lexer.
|
||||
*
|
||||
* @see https://github.com/markedjs/marked/blob/master/src/Tokens.ts
|
||||
*
|
||||
*/
|
||||
markdownTokenName?: string
|
||||
|
||||
/**
|
||||
* The parse function used by the markdown parser to convert markdown tokens to ProseMirror nodes.
|
||||
*/
|
||||
parseMarkdown?: (token: MarkdownToken, helpers: MarkdownParseHelpers) => MarkdownParseResult
|
||||
|
||||
/**
|
||||
* The serializer function used by the markdown serializer to convert ProseMirror nodes to markdown tokens.
|
||||
*/
|
||||
renderMarkdown?: (node: JSONContent, helpers: MarkdownRendererHelpers, ctx: RenderContext) => string
|
||||
|
||||
/**
|
||||
* The markdown tokenizer responsible for turning a markdown string into tokens
|
||||
*
|
||||
* Custom tokenizers are only needed when you want to parse non-standard markdown token.
|
||||
*/
|
||||
markdownTokenizer?: MarkdownTokenizer
|
||||
|
||||
/**
|
||||
* Optional markdown options for indentation
|
||||
*/
|
||||
markdownOptions?: {
|
||||
/**
|
||||
* Defines if this markdown element should indent it's child elements
|
||||
*/
|
||||
indentsContent?: boolean
|
||||
|
||||
/**
|
||||
* Lets a mark tell the Markdown serializer which inline HTML tags it can
|
||||
* safely use when plain markdown delimiters would become ambiguous.
|
||||
*
|
||||
* This is mainly useful for overlapping marks. For example, bold followed
|
||||
* by bold+italic followed by italic cannot always be written back with only
|
||||
* `*` and `**` in a way that still parses correctly. In that case, the
|
||||
* serializer can close the overlapping section with markdown and reopen the
|
||||
* remaining tail with HTML instead.
|
||||
*
|
||||
* Example:
|
||||
* - desired formatting: `**123` + `*456*` + `789 italic`
|
||||
* - serialized result: `**123*456***<em>789</em>`
|
||||
*
|
||||
* If your extension defines custom mark names, set `htmlReopen` on that
|
||||
* extension so the serializer can reuse its HTML form for overlap cases.
|
||||
*/
|
||||
htmlReopen?: {
|
||||
open: string
|
||||
close: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function extends the schema of the node.
|
||||
* @example
|
||||
* extendNodeSchema() {
|
||||
* return {
|
||||
* group: 'inline',
|
||||
* selectable: false,
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
extendNodeSchema?:
|
||||
| ((
|
||||
this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
parent: ParentConfig<Config>['extendNodeSchema']
|
||||
},
|
||||
extension: Node,
|
||||
) => Record<string, any>)
|
||||
| null
|
||||
|
||||
/**
|
||||
* This function extends the schema of the mark.
|
||||
* @example
|
||||
* extendMarkSchema() {
|
||||
* return {
|
||||
* group: 'inline',
|
||||
* selectable: false,
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
extendMarkSchema?:
|
||||
| ((
|
||||
this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
parent: ParentConfig<Config>['extendMarkSchema']
|
||||
},
|
||||
extension: Mark,
|
||||
) => Record<string, any>)
|
||||
| null
|
||||
|
||||
/**
|
||||
* The editor is not ready yet.
|
||||
*/
|
||||
onBeforeCreate?:
|
||||
| ((
|
||||
this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
editor: Editor
|
||||
type: PMType
|
||||
parent: ParentConfig<Config>['onBeforeCreate']
|
||||
},
|
||||
event: EditorEvents['beforeCreate'],
|
||||
) => void)
|
||||
| null
|
||||
|
||||
/**
|
||||
* The editor is ready.
|
||||
*/
|
||||
onCreate?:
|
||||
| ((
|
||||
this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
editor: Editor
|
||||
type: PMType
|
||||
parent: ParentConfig<Config>['onCreate']
|
||||
},
|
||||
event: EditorEvents['create'],
|
||||
) => void)
|
||||
| null
|
||||
|
||||
/**
|
||||
* The content has changed.
|
||||
*/
|
||||
onUpdate?:
|
||||
| ((
|
||||
this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
editor: Editor
|
||||
type: PMType
|
||||
parent: ParentConfig<Config>['onUpdate']
|
||||
},
|
||||
event: EditorEvents['update'],
|
||||
) => void)
|
||||
| null
|
||||
|
||||
/**
|
||||
* The selection has changed.
|
||||
*/
|
||||
onSelectionUpdate?:
|
||||
| ((
|
||||
this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
editor: Editor
|
||||
type: PMType
|
||||
parent: ParentConfig<Config>['onSelectionUpdate']
|
||||
},
|
||||
event: EditorEvents['selectionUpdate'],
|
||||
) => void)
|
||||
| null
|
||||
|
||||
/**
|
||||
* The editor state has changed.
|
||||
*/
|
||||
onTransaction?:
|
||||
| ((
|
||||
this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
editor: Editor
|
||||
type: PMType
|
||||
parent: ParentConfig<Config>['onTransaction']
|
||||
},
|
||||
event: EditorEvents['transaction'],
|
||||
) => void)
|
||||
| null
|
||||
|
||||
/**
|
||||
* The editor is focused.
|
||||
*/
|
||||
onFocus?:
|
||||
| ((
|
||||
this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
editor: Editor
|
||||
type: PMType
|
||||
parent: ParentConfig<Config>['onFocus']
|
||||
},
|
||||
event: EditorEvents['focus'],
|
||||
) => void)
|
||||
| null
|
||||
|
||||
/**
|
||||
* The editor isn’t focused anymore.
|
||||
*/
|
||||
onBlur?:
|
||||
| ((
|
||||
this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
editor: Editor
|
||||
type: PMType
|
||||
parent: ParentConfig<Config>['onBlur']
|
||||
},
|
||||
event: EditorEvents['blur'],
|
||||
) => void)
|
||||
| null
|
||||
|
||||
/**
|
||||
* The editor is destroyed.
|
||||
*/
|
||||
onDestroy?:
|
||||
| ((
|
||||
this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
editor: Editor
|
||||
type: PMType
|
||||
parent: ParentConfig<Config>['onDestroy']
|
||||
},
|
||||
event: EditorEvents['destroy'],
|
||||
) => void)
|
||||
| null
|
||||
|
||||
/**
|
||||
* This hook allows you to intercept and modify transactions before they are dispatched.
|
||||
*
|
||||
* Example
|
||||
* ```ts
|
||||
* dispatchTransaction({ transaction, next }) {
|
||||
* console.log('Dispatching transaction:', transaction)
|
||||
* next(transaction)
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param props - The dispatch transaction props
|
||||
*/
|
||||
dispatchTransaction?:
|
||||
| ((
|
||||
this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
editor: Editor
|
||||
type: PMType
|
||||
parent: ParentConfig<Config>['dispatchTransaction']
|
||||
},
|
||||
props: DispatchTransactionProps,
|
||||
) => void)
|
||||
| null
|
||||
}
|
||||
|
||||
export class Extendable<
|
||||
Options = any,
|
||||
Storage = any,
|
||||
Config = ExtensionConfig<Options, Storage> | NodeConfig<Options, Storage> | MarkConfig<Options, Storage>,
|
||||
> {
|
||||
type = 'extendable'
|
||||
parent: Extendable | null = null
|
||||
|
||||
child: Extendable | null = null
|
||||
|
||||
name = ''
|
||||
|
||||
config: Config = {
|
||||
name: this.name,
|
||||
} as Config
|
||||
|
||||
constructor(config: Partial<Config> = {}) {
|
||||
this.config = {
|
||||
...this.config,
|
||||
...config,
|
||||
}
|
||||
|
||||
this.name = (this.config as any).name
|
||||
}
|
||||
|
||||
get options(): Options {
|
||||
return {
|
||||
...(callOrReturn(
|
||||
getExtensionField<AnyConfig['addOptions']>(this as any, 'addOptions', {
|
||||
name: this.name,
|
||||
}),
|
||||
) || {}),
|
||||
}
|
||||
}
|
||||
|
||||
get storage(): Readonly<Storage> {
|
||||
return {
|
||||
...(callOrReturn(
|
||||
getExtensionField<AnyConfig['addStorage']>(this as any, 'addStorage', {
|
||||
name: this.name,
|
||||
options: this.options,
|
||||
}),
|
||||
) || {}),
|
||||
}
|
||||
}
|
||||
|
||||
configure(options: Partial<Options> = {}) {
|
||||
const extension = this.extend<Options, Storage, Config>({
|
||||
...this.config,
|
||||
addOptions: () => {
|
||||
return mergeDeep(this.options as Record<string, any>, options) as Options
|
||||
},
|
||||
})
|
||||
|
||||
extension.name = this.name
|
||||
extension.parent = this.parent
|
||||
|
||||
return extension
|
||||
}
|
||||
|
||||
extend<
|
||||
ExtendedOptions = Options,
|
||||
ExtendedStorage = Storage,
|
||||
ExtendedConfig =
|
||||
| ExtensionConfig<ExtendedOptions, ExtendedStorage>
|
||||
| NodeConfig<ExtendedOptions, ExtendedStorage>
|
||||
| MarkConfig<ExtendedOptions, ExtendedStorage>,
|
||||
>(extendedConfig: Partial<ExtendedConfig> = {}): Extendable<ExtendedOptions, ExtendedStorage> {
|
||||
const extension = new (this.constructor as any)({ ...this.config, ...extendedConfig })
|
||||
|
||||
extension.parent = this
|
||||
this.child = extension
|
||||
extension.name = 'name' in extendedConfig ? extendedConfig.name : extension.parent.name
|
||||
|
||||
return extension
|
||||
}
|
||||
}
|
||||
55
node_modules/@tiptap/core/src/Extension.ts
generated
vendored
Normal file
55
node_modules/@tiptap/core/src/Extension.ts
generated
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { Editor } from './Editor.js'
|
||||
import { type ExtendableConfig, Extendable } from './Extendable.js'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
export interface ExtensionConfig<Options = any, Storage = any>
|
||||
extends ExtendableConfig<Options, Storage, ExtensionConfig<Options, Storage>, null> {}
|
||||
|
||||
/**
|
||||
* The Extension class is the base class for all extensions.
|
||||
* @see https://tiptap.dev/api/extensions#create-a-new-extension
|
||||
*/
|
||||
export class Extension<Options = any, Storage = any> extends Extendable<
|
||||
Options,
|
||||
Storage,
|
||||
ExtensionConfig<Options, Storage>
|
||||
> {
|
||||
type = 'extension'
|
||||
|
||||
/**
|
||||
* Create a new Extension instance
|
||||
* @param config - Extension configuration object or a function that returns a configuration object
|
||||
*/
|
||||
static create<O = any, S = any>(
|
||||
config: Partial<ExtensionConfig<O, S>> | (() => Partial<ExtensionConfig<O, S>>) = {},
|
||||
) {
|
||||
// If the config is a function, execute it to get the configuration object
|
||||
const resolvedConfig = typeof config === 'function' ? config() : config
|
||||
return new Extension<O, S>(resolvedConfig)
|
||||
}
|
||||
|
||||
configure(options?: Partial<Options>) {
|
||||
return super.configure(options) as Extension<Options, Storage>
|
||||
}
|
||||
|
||||
extend<
|
||||
ExtendedOptions = Options,
|
||||
ExtendedStorage = Storage,
|
||||
ExtendedConfig = ExtensionConfig<ExtendedOptions, ExtendedStorage>,
|
||||
>(
|
||||
extendedConfig?:
|
||||
| (() => Partial<ExtendedConfig>)
|
||||
| (Partial<ExtendedConfig> &
|
||||
ThisType<{
|
||||
name: string
|
||||
options: ExtendedOptions
|
||||
storage: ExtendedStorage
|
||||
editor: Editor
|
||||
type: null
|
||||
}>),
|
||||
): Extension<ExtendedOptions, ExtendedStorage> {
|
||||
// If the extended config is a function, execute it to get the configuration object
|
||||
const resolvedConfig = typeof extendedConfig === 'function' ? extendedConfig() : extendedConfig
|
||||
return super.extend(resolvedConfig) as Extension<ExtendedOptions, ExtendedStorage>
|
||||
}
|
||||
}
|
||||
440
node_modules/@tiptap/core/src/ExtensionManager.ts
generated
vendored
Normal file
440
node_modules/@tiptap/core/src/ExtensionManager.ts
generated
vendored
Normal file
@@ -0,0 +1,440 @@
|
||||
import { keymap } from '@tiptap/pm/keymap'
|
||||
import type { Schema } from '@tiptap/pm/model'
|
||||
import type { Plugin, Transaction } from '@tiptap/pm/state'
|
||||
import type { EditorView, MarkViewConstructor, NodeViewConstructor } from '@tiptap/pm/view'
|
||||
|
||||
import type { Editor } from './Editor.js'
|
||||
import {
|
||||
flattenExtensions,
|
||||
getAttributesFromExtensions,
|
||||
getExtensionField,
|
||||
getNodeType,
|
||||
getRenderedAttributes,
|
||||
getSchemaByResolvedExtensions,
|
||||
getSchemaTypeByName,
|
||||
isExtensionRulesEnabled,
|
||||
resolveExtensions,
|
||||
sortExtensions,
|
||||
splitExtensions,
|
||||
} from './helpers/index.js'
|
||||
import { type MarkConfig, type NodeConfig, type Storage, getMarkType, updateMarkViewAttributes } from './index.js'
|
||||
import { inputRulesPlugin } from './InputRule.js'
|
||||
import { Mark } from './Mark.js'
|
||||
import { pasteRulesPlugin } from './PasteRule.js'
|
||||
import type { AnyConfig, Extensions, RawCommands } from './types.js'
|
||||
import { callOrReturn } from './utilities/callOrReturn.js'
|
||||
|
||||
export class ExtensionManager {
|
||||
editor: Editor
|
||||
|
||||
schema: Schema
|
||||
|
||||
/**
|
||||
* A flattened and sorted array of all extensions
|
||||
*/
|
||||
extensions: Extensions
|
||||
|
||||
/**
|
||||
* A non-flattened array of base extensions (no sub-extensions)
|
||||
*/
|
||||
baseExtensions: Extensions
|
||||
|
||||
splittableMarks: string[] = []
|
||||
|
||||
constructor(extensions: Extensions, editor: Editor) {
|
||||
this.editor = editor
|
||||
this.baseExtensions = extensions
|
||||
this.extensions = resolveExtensions(extensions)
|
||||
this.schema = getSchemaByResolvedExtensions(this.extensions, editor)
|
||||
this.setupExtensions()
|
||||
}
|
||||
|
||||
static resolve = resolveExtensions
|
||||
|
||||
static sort = sortExtensions
|
||||
|
||||
static flatten = flattenExtensions
|
||||
|
||||
/**
|
||||
* Get all commands from the extensions.
|
||||
* @returns An object with all commands where the key is the command name and the value is the command function
|
||||
*/
|
||||
get commands(): RawCommands {
|
||||
return this.extensions.reduce((commands, extension) => {
|
||||
const context = {
|
||||
name: extension.name,
|
||||
options: extension.options,
|
||||
storage: this.editor.extensionStorage[extension.name as keyof Storage],
|
||||
editor: this.editor,
|
||||
type: getSchemaTypeByName(extension.name, this.schema),
|
||||
}
|
||||
|
||||
const addCommands = getExtensionField<AnyConfig['addCommands']>(extension, 'addCommands', context)
|
||||
|
||||
if (!addCommands) {
|
||||
return commands
|
||||
}
|
||||
|
||||
return {
|
||||
...commands,
|
||||
...addCommands(),
|
||||
}
|
||||
}, {} as RawCommands)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered Prosemirror plugins from the extensions.
|
||||
* @returns An array of Prosemirror plugins
|
||||
*/
|
||||
get plugins(): Plugin[] {
|
||||
const { editor } = this
|
||||
|
||||
// With ProseMirror, first plugins within an array are executed first.
|
||||
// In Tiptap, we provide the ability to override plugins,
|
||||
// so it feels more natural to run plugins at the end of an array first.
|
||||
// That’s why we have to reverse the `extensions` array and sort again
|
||||
// based on the `priority` option.
|
||||
const extensions = sortExtensions([...this.extensions].reverse())
|
||||
|
||||
const allPlugins = extensions.flatMap(extension => {
|
||||
const context = {
|
||||
name: extension.name,
|
||||
options: extension.options,
|
||||
storage: this.editor.extensionStorage[extension.name as keyof Storage],
|
||||
editor,
|
||||
type: getSchemaTypeByName(extension.name, this.schema),
|
||||
}
|
||||
|
||||
const plugins: Plugin[] = []
|
||||
|
||||
const addKeyboardShortcuts = getExtensionField<AnyConfig['addKeyboardShortcuts']>(
|
||||
extension,
|
||||
'addKeyboardShortcuts',
|
||||
context,
|
||||
)
|
||||
|
||||
let defaultBindings: Record<string, () => boolean> = {}
|
||||
|
||||
// bind exit handling
|
||||
if (extension.type === 'mark' && getExtensionField<MarkConfig['exitable']>(extension, 'exitable', context)) {
|
||||
defaultBindings.ArrowRight = () => Mark.handleExit({ editor, mark: extension as Mark })
|
||||
}
|
||||
|
||||
if (addKeyboardShortcuts) {
|
||||
const bindings = Object.fromEntries(
|
||||
Object.entries(addKeyboardShortcuts()).map(([shortcut, method]) => {
|
||||
return [shortcut, () => method({ editor })]
|
||||
}),
|
||||
)
|
||||
|
||||
defaultBindings = { ...defaultBindings, ...bindings }
|
||||
}
|
||||
|
||||
const keyMapPlugin = keymap(defaultBindings)
|
||||
|
||||
plugins.push(keyMapPlugin)
|
||||
|
||||
const addInputRules = getExtensionField<AnyConfig['addInputRules']>(extension, 'addInputRules', context)
|
||||
|
||||
if (isExtensionRulesEnabled(extension, editor.options.enableInputRules) && addInputRules) {
|
||||
const rules = addInputRules()
|
||||
|
||||
if (rules && rules.length) {
|
||||
const inputResult = inputRulesPlugin({
|
||||
editor,
|
||||
rules,
|
||||
})
|
||||
|
||||
const inputPlugins = Array.isArray(inputResult) ? inputResult : [inputResult]
|
||||
|
||||
plugins.push(...inputPlugins)
|
||||
}
|
||||
}
|
||||
|
||||
const addPasteRules = getExtensionField<AnyConfig['addPasteRules']>(extension, 'addPasteRules', context)
|
||||
|
||||
if (isExtensionRulesEnabled(extension, editor.options.enablePasteRules) && addPasteRules) {
|
||||
const rules = addPasteRules()
|
||||
|
||||
if (rules && rules.length) {
|
||||
const pasteRules = pasteRulesPlugin({ editor, rules })
|
||||
|
||||
plugins.push(...pasteRules)
|
||||
}
|
||||
}
|
||||
|
||||
const addProseMirrorPlugins = getExtensionField<AnyConfig['addProseMirrorPlugins']>(
|
||||
extension,
|
||||
'addProseMirrorPlugins',
|
||||
context,
|
||||
)
|
||||
|
||||
if (addProseMirrorPlugins) {
|
||||
const proseMirrorPlugins = addProseMirrorPlugins()
|
||||
|
||||
plugins.push(...proseMirrorPlugins)
|
||||
}
|
||||
|
||||
return plugins
|
||||
})
|
||||
|
||||
return allPlugins
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all attributes from the extensions.
|
||||
* @returns An array of attributes
|
||||
*/
|
||||
get attributes() {
|
||||
return getAttributesFromExtensions(this.extensions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all node views from the extensions.
|
||||
* @returns An object with all node views where the key is the node name and the value is the node view function
|
||||
*/
|
||||
get nodeViews(): Record<string, NodeViewConstructor> {
|
||||
const { editor } = this
|
||||
const { nodeExtensions } = splitExtensions(this.extensions)
|
||||
|
||||
return Object.fromEntries(
|
||||
nodeExtensions
|
||||
.filter(extension => !!getExtensionField(extension, 'addNodeView'))
|
||||
.map(extension => {
|
||||
const extensionAttributes = this.attributes.filter(attribute => attribute.type === extension.name)
|
||||
const context = {
|
||||
name: extension.name,
|
||||
options: extension.options,
|
||||
storage: this.editor.extensionStorage[extension.name as keyof Storage],
|
||||
editor,
|
||||
type: getNodeType(extension.name, this.schema),
|
||||
}
|
||||
const addNodeView = getExtensionField<NodeConfig['addNodeView']>(extension, 'addNodeView', context)
|
||||
|
||||
if (!addNodeView) {
|
||||
return []
|
||||
}
|
||||
|
||||
const nodeViewResult = addNodeView()
|
||||
|
||||
if (!nodeViewResult) {
|
||||
return []
|
||||
}
|
||||
|
||||
const nodeview: NodeViewConstructor = (node, view, getPos, decorations, innerDecorations) => {
|
||||
const HTMLAttributes = getRenderedAttributes(node, extensionAttributes)
|
||||
|
||||
return nodeViewResult({
|
||||
// pass-through
|
||||
node,
|
||||
view,
|
||||
getPos: getPos as () => number,
|
||||
decorations,
|
||||
innerDecorations,
|
||||
// tiptap-specific
|
||||
editor,
|
||||
extension,
|
||||
HTMLAttributes,
|
||||
})
|
||||
}
|
||||
|
||||
return [extension.name, nodeview]
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the composed dispatchTransaction function from all extensions.
|
||||
* @param baseDispatch The base dispatch function (e.g. from the editor or user props)
|
||||
* @returns A composed dispatch function
|
||||
*/
|
||||
dispatchTransaction(baseDispatch: (tr: Transaction) => void): (tr: Transaction) => void {
|
||||
const { editor } = this
|
||||
const extensions = sortExtensions([...this.extensions].reverse())
|
||||
|
||||
return extensions.reduceRight((next, extension) => {
|
||||
const context = {
|
||||
name: extension.name,
|
||||
options: extension.options,
|
||||
storage: this.editor.extensionStorage[extension.name as keyof Storage],
|
||||
editor,
|
||||
type: getSchemaTypeByName(extension.name, this.schema),
|
||||
}
|
||||
|
||||
const dispatchTransaction = getExtensionField<AnyConfig['dispatchTransaction']>(
|
||||
extension,
|
||||
'dispatchTransaction',
|
||||
context,
|
||||
)
|
||||
|
||||
if (!dispatchTransaction) {
|
||||
return next
|
||||
}
|
||||
|
||||
return (transaction: Transaction) => {
|
||||
dispatchTransaction.call(context, { transaction, next })
|
||||
}
|
||||
}, baseDispatch)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the composed transformPastedHTML function from all extensions.
|
||||
* @param baseTransform The base transform function (e.g. from the editor props)
|
||||
* @returns A composed transform function that chains all extension transforms
|
||||
*/
|
||||
transformPastedHTML(
|
||||
baseTransform?: (html: string, view?: any) => string,
|
||||
): (html: string, view?: EditorView) => string {
|
||||
const { editor } = this
|
||||
const extensions = sortExtensions([...this.extensions])
|
||||
|
||||
return extensions.reduce(
|
||||
(transform, extension) => {
|
||||
const context = {
|
||||
name: extension.name,
|
||||
options: extension.options,
|
||||
storage: this.editor.extensionStorage[extension.name as keyof Storage],
|
||||
editor,
|
||||
type: getSchemaTypeByName(extension.name, this.schema),
|
||||
}
|
||||
|
||||
const extensionTransform = getExtensionField<AnyConfig['transformPastedHTML']>(
|
||||
extension,
|
||||
'transformPastedHTML',
|
||||
context,
|
||||
)
|
||||
|
||||
if (!extensionTransform) {
|
||||
return transform
|
||||
}
|
||||
|
||||
return (html: string, view?: any) => {
|
||||
// Chain the transforms: pass the result of the previous transform to the next
|
||||
const transformedHtml = transform(html, view)
|
||||
return extensionTransform.call(context, transformedHtml)
|
||||
}
|
||||
},
|
||||
baseTransform || ((html: string) => html),
|
||||
)
|
||||
}
|
||||
|
||||
get markViews(): Record<string, MarkViewConstructor> {
|
||||
const { editor } = this
|
||||
const { markExtensions } = splitExtensions(this.extensions)
|
||||
|
||||
return Object.fromEntries(
|
||||
markExtensions
|
||||
.filter(extension => !!getExtensionField(extension, 'addMarkView'))
|
||||
.map(extension => {
|
||||
const extensionAttributes = this.attributes.filter(attribute => attribute.type === extension.name)
|
||||
const context = {
|
||||
name: extension.name,
|
||||
options: extension.options,
|
||||
storage: this.editor.extensionStorage[extension.name as keyof Storage],
|
||||
editor,
|
||||
type: getMarkType(extension.name, this.schema),
|
||||
}
|
||||
const addMarkView = getExtensionField<MarkConfig['addMarkView']>(extension, 'addMarkView', context)
|
||||
|
||||
if (!addMarkView) {
|
||||
return []
|
||||
}
|
||||
|
||||
const markView: MarkViewConstructor = (mark, view, inline) => {
|
||||
const HTMLAttributes = getRenderedAttributes(mark, extensionAttributes)
|
||||
|
||||
return addMarkView()({
|
||||
// pass-through
|
||||
mark,
|
||||
view,
|
||||
inline,
|
||||
// tiptap-specific
|
||||
editor,
|
||||
extension,
|
||||
HTMLAttributes,
|
||||
updateAttributes: (attrs: Record<string, any>) => {
|
||||
updateMarkViewAttributes(mark, editor, attrs)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return [extension.name, markView]
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Go through all extensions, create extension storages & setup marks
|
||||
* & bind editor event listener.
|
||||
*/
|
||||
private setupExtensions() {
|
||||
const extensions = this.extensions
|
||||
// re-initialize the extension storage object instance
|
||||
this.editor.extensionStorage = Object.fromEntries(
|
||||
extensions.map(extension => [extension.name, extension.storage]),
|
||||
) as unknown as Storage
|
||||
|
||||
extensions.forEach(extension => {
|
||||
const context = {
|
||||
name: extension.name,
|
||||
options: extension.options,
|
||||
storage: this.editor.extensionStorage[extension.name as keyof Storage],
|
||||
editor: this.editor,
|
||||
type: getSchemaTypeByName(extension.name, this.schema),
|
||||
}
|
||||
|
||||
if (extension.type === 'mark') {
|
||||
const keepOnSplit = callOrReturn(getExtensionField(extension, 'keepOnSplit', context)) ?? true
|
||||
|
||||
if (keepOnSplit) {
|
||||
this.splittableMarks.push(extension.name)
|
||||
}
|
||||
}
|
||||
|
||||
const onBeforeCreate = getExtensionField<AnyConfig['onBeforeCreate']>(extension, 'onBeforeCreate', context)
|
||||
const onCreate = getExtensionField<AnyConfig['onCreate']>(extension, 'onCreate', context)
|
||||
const onUpdate = getExtensionField<AnyConfig['onUpdate']>(extension, 'onUpdate', context)
|
||||
const onSelectionUpdate = getExtensionField<AnyConfig['onSelectionUpdate']>(
|
||||
extension,
|
||||
'onSelectionUpdate',
|
||||
context,
|
||||
)
|
||||
const onTransaction = getExtensionField<AnyConfig['onTransaction']>(extension, 'onTransaction', context)
|
||||
const onFocus = getExtensionField<AnyConfig['onFocus']>(extension, 'onFocus', context)
|
||||
const onBlur = getExtensionField<AnyConfig['onBlur']>(extension, 'onBlur', context)
|
||||
const onDestroy = getExtensionField<AnyConfig['onDestroy']>(extension, 'onDestroy', context)
|
||||
|
||||
if (onBeforeCreate) {
|
||||
this.editor.on('beforeCreate', onBeforeCreate)
|
||||
}
|
||||
|
||||
if (onCreate) {
|
||||
this.editor.on('create', onCreate)
|
||||
}
|
||||
|
||||
if (onUpdate) {
|
||||
this.editor.on('update', onUpdate)
|
||||
}
|
||||
|
||||
if (onSelectionUpdate) {
|
||||
this.editor.on('selectionUpdate', onSelectionUpdate)
|
||||
}
|
||||
|
||||
if (onTransaction) {
|
||||
this.editor.on('transaction', onTransaction)
|
||||
}
|
||||
|
||||
if (onFocus) {
|
||||
this.editor.on('focus', onFocus)
|
||||
}
|
||||
|
||||
if (onBlur) {
|
||||
this.editor.on('blur', onBlur)
|
||||
}
|
||||
|
||||
if (onDestroy) {
|
||||
this.editor.on('destroy', onDestroy)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
290
node_modules/@tiptap/core/src/InputRule.ts
generated
vendored
Normal file
290
node_modules/@tiptap/core/src/InputRule.ts
generated
vendored
Normal file
@@ -0,0 +1,290 @@
|
||||
import type { Node as ProseMirrorNode } from '@tiptap/pm/model'
|
||||
import { Fragment } from '@tiptap/pm/model'
|
||||
import type { EditorState, TextSelection } from '@tiptap/pm/state'
|
||||
import { Plugin } from '@tiptap/pm/state'
|
||||
|
||||
import { CommandManager } from './CommandManager.js'
|
||||
import type { Editor } from './Editor.js'
|
||||
import { createChainableState } from './helpers/createChainableState.js'
|
||||
import { getHTMLFromFragment } from './helpers/getHTMLFromFragment.js'
|
||||
import { getTextContentFromNodes } from './helpers/getTextContentFromNodes.js'
|
||||
import type { CanCommands, ChainedCommands, ExtendedRegExpMatchArray, Range, SingleCommands } from './types.js'
|
||||
import { isRegExp } from './utilities/isRegExp.js'
|
||||
|
||||
export type InputRuleMatch = {
|
||||
index: number
|
||||
text: string
|
||||
replaceWith?: string
|
||||
match?: RegExpMatchArray
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
export type InputRuleFinder = RegExp | ((text: string) => InputRuleMatch | null)
|
||||
|
||||
export class InputRule {
|
||||
find: InputRuleFinder
|
||||
|
||||
handler: (props: {
|
||||
state: EditorState
|
||||
range: Range
|
||||
match: ExtendedRegExpMatchArray
|
||||
commands: SingleCommands
|
||||
chain: () => ChainedCommands
|
||||
can: () => CanCommands
|
||||
}) => void | null
|
||||
|
||||
undoable: boolean
|
||||
|
||||
constructor(config: {
|
||||
find: InputRuleFinder
|
||||
handler: (props: {
|
||||
state: EditorState
|
||||
range: Range
|
||||
match: ExtendedRegExpMatchArray
|
||||
commands: SingleCommands
|
||||
chain: () => ChainedCommands
|
||||
can: () => CanCommands
|
||||
}) => void | null
|
||||
undoable?: boolean
|
||||
}) {
|
||||
this.find = config.find
|
||||
this.handler = config.handler
|
||||
this.undoable = config.undoable ?? true
|
||||
}
|
||||
}
|
||||
|
||||
const inputRuleMatcherHandler = (text: string, find: InputRuleFinder): ExtendedRegExpMatchArray | null => {
|
||||
if (isRegExp(find)) {
|
||||
return find.exec(text)
|
||||
}
|
||||
|
||||
const inputRuleMatch = find(text)
|
||||
|
||||
if (!inputRuleMatch) {
|
||||
return null
|
||||
}
|
||||
|
||||
const result: ExtendedRegExpMatchArray = [inputRuleMatch.text]
|
||||
|
||||
result.index = inputRuleMatch.index
|
||||
result.input = text
|
||||
result.data = inputRuleMatch.data
|
||||
|
||||
if (inputRuleMatch.replaceWith) {
|
||||
if (!inputRuleMatch.text.includes(inputRuleMatch.replaceWith)) {
|
||||
console.warn('[tiptap warn]: "inputRuleMatch.replaceWith" must be part of "inputRuleMatch.text".')
|
||||
}
|
||||
|
||||
result.push(inputRuleMatch.replaceWith)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function run(config: {
|
||||
editor: Editor
|
||||
from: number
|
||||
to: number
|
||||
text: string
|
||||
rules: InputRule[]
|
||||
plugin: Plugin
|
||||
}): boolean {
|
||||
const { editor, from, to, text, rules, plugin } = config
|
||||
const { view } = editor
|
||||
|
||||
if (view.composing) {
|
||||
return false
|
||||
}
|
||||
|
||||
const $from = view.state.doc.resolve(from)
|
||||
|
||||
if (
|
||||
// check for code node
|
||||
$from.parent.type.spec.code ||
|
||||
// check for code mark
|
||||
!!($from.nodeBefore || $from.nodeAfter)?.marks.find(mark => mark.type.spec.code)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
let matched = false
|
||||
|
||||
const textBefore = getTextContentFromNodes($from) + text
|
||||
|
||||
rules.forEach(rule => {
|
||||
if (matched) {
|
||||
return
|
||||
}
|
||||
|
||||
const match = inputRuleMatcherHandler(textBefore, rule.find)
|
||||
|
||||
if (!match) {
|
||||
return
|
||||
}
|
||||
|
||||
const tr = view.state.tr
|
||||
const state = createChainableState({
|
||||
state: view.state,
|
||||
transaction: tr,
|
||||
})
|
||||
const range = {
|
||||
from: from - (match[0].length - text.length),
|
||||
to,
|
||||
}
|
||||
|
||||
const { commands, chain, can } = new CommandManager({
|
||||
editor,
|
||||
state,
|
||||
})
|
||||
|
||||
const handler = rule.handler({
|
||||
state,
|
||||
range,
|
||||
match,
|
||||
commands,
|
||||
chain,
|
||||
can,
|
||||
})
|
||||
|
||||
// stop if there are no changes
|
||||
if (handler === null || !tr.steps.length) {
|
||||
return
|
||||
}
|
||||
|
||||
// store transform as meta data
|
||||
// so we can undo input rules within the `undoInputRules` command
|
||||
if (rule.undoable) {
|
||||
tr.setMeta(plugin, {
|
||||
transform: tr,
|
||||
from,
|
||||
to,
|
||||
text,
|
||||
})
|
||||
}
|
||||
|
||||
view.dispatch(tr)
|
||||
matched = true
|
||||
})
|
||||
|
||||
return matched
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an input rules plugin. When enabled, it will cause text
|
||||
* input that matches any of the given rules to trigger the rule’s
|
||||
* action.
|
||||
*/
|
||||
export function inputRulesPlugin(props: { editor: Editor; rules: InputRule[] }): Plugin {
|
||||
const { editor, rules } = props
|
||||
const plugin = new Plugin({
|
||||
state: {
|
||||
init() {
|
||||
return null
|
||||
},
|
||||
apply(tr, prev, state) {
|
||||
const stored = tr.getMeta(plugin)
|
||||
|
||||
if (stored) {
|
||||
return stored
|
||||
}
|
||||
|
||||
// if InputRule is triggered by insertContent()
|
||||
const simulatedInputMeta = tr.getMeta('applyInputRules') as
|
||||
| undefined
|
||||
| {
|
||||
from: number
|
||||
text: string | ProseMirrorNode | Fragment
|
||||
}
|
||||
const isSimulatedInput = !!simulatedInputMeta
|
||||
|
||||
if (isSimulatedInput) {
|
||||
setTimeout(() => {
|
||||
let { text } = simulatedInputMeta
|
||||
|
||||
if (typeof text === 'string') {
|
||||
text = text as string
|
||||
} else {
|
||||
text = getHTMLFromFragment(Fragment.from(text), state.schema)
|
||||
}
|
||||
|
||||
const { from } = simulatedInputMeta
|
||||
const to = from + text.length
|
||||
|
||||
run({
|
||||
editor,
|
||||
from,
|
||||
to,
|
||||
text,
|
||||
rules,
|
||||
plugin,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return tr.selectionSet || tr.docChanged ? null : prev
|
||||
},
|
||||
},
|
||||
|
||||
props: {
|
||||
handleTextInput(view, from, to, text) {
|
||||
return run({
|
||||
editor,
|
||||
from,
|
||||
to,
|
||||
text,
|
||||
rules,
|
||||
plugin,
|
||||
})
|
||||
},
|
||||
|
||||
handleDOMEvents: {
|
||||
compositionend: view => {
|
||||
setTimeout(() => {
|
||||
const { $cursor } = view.state.selection as TextSelection
|
||||
|
||||
if ($cursor) {
|
||||
run({
|
||||
editor,
|
||||
from: $cursor.pos,
|
||||
to: $cursor.pos,
|
||||
text: '',
|
||||
rules,
|
||||
plugin,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return false
|
||||
},
|
||||
},
|
||||
|
||||
// add support for input rules to trigger on enter
|
||||
// this is useful for example for code blocks
|
||||
handleKeyDown(view, event) {
|
||||
if (event.key !== 'Enter') {
|
||||
return false
|
||||
}
|
||||
|
||||
const { $cursor } = view.state.selection as TextSelection
|
||||
|
||||
if ($cursor) {
|
||||
return run({
|
||||
editor,
|
||||
from: $cursor.pos,
|
||||
to: $cursor.pos,
|
||||
text: '\n',
|
||||
rules,
|
||||
plugin,
|
||||
})
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
},
|
||||
|
||||
// @ts-ignore
|
||||
isInputRules: true,
|
||||
}) as Plugin
|
||||
|
||||
return plugin
|
||||
}
|
||||
211
node_modules/@tiptap/core/src/Mark.ts
generated
vendored
Normal file
211
node_modules/@tiptap/core/src/Mark.ts
generated
vendored
Normal file
@@ -0,0 +1,211 @@
|
||||
import type { DOMOutputSpec, Mark as ProseMirrorMark, MarkSpec, MarkType } from '@tiptap/pm/model'
|
||||
|
||||
import type { Editor } from './Editor.js'
|
||||
import type { ExtendableConfig } from './Extendable.js'
|
||||
import { Extendable } from './Extendable.js'
|
||||
import type { Attributes, MarkViewRenderer, ParentConfig } from './types.js'
|
||||
|
||||
export interface MarkConfig<Options = any, Storage = any>
|
||||
extends ExtendableConfig<Options, Storage, MarkConfig<Options, Storage>, MarkType> {
|
||||
/**
|
||||
* Mark View
|
||||
*/
|
||||
addMarkView?:
|
||||
| ((this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
editor: Editor
|
||||
type: MarkType
|
||||
parent: ParentConfig<MarkConfig<Options, Storage>>['addMarkView']
|
||||
}) => MarkViewRenderer)
|
||||
| null
|
||||
|
||||
/**
|
||||
* Keep mark after split node
|
||||
*/
|
||||
keepOnSplit?: boolean | (() => boolean)
|
||||
|
||||
/**
|
||||
* Inclusive
|
||||
*/
|
||||
inclusive?:
|
||||
| MarkSpec['inclusive']
|
||||
| ((this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
parent: ParentConfig<MarkConfig<Options, Storage>>['inclusive']
|
||||
editor?: Editor
|
||||
}) => MarkSpec['inclusive'])
|
||||
|
||||
/**
|
||||
* Excludes
|
||||
*/
|
||||
excludes?:
|
||||
| MarkSpec['excludes']
|
||||
| ((this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
parent: ParentConfig<MarkConfig<Options, Storage>>['excludes']
|
||||
editor?: Editor
|
||||
}) => MarkSpec['excludes'])
|
||||
|
||||
/**
|
||||
* Marks this Mark as exitable
|
||||
*/
|
||||
exitable?: boolean | (() => boolean)
|
||||
|
||||
/**
|
||||
* Group
|
||||
*/
|
||||
group?:
|
||||
| MarkSpec['group']
|
||||
| ((this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
parent: ParentConfig<MarkConfig<Options, Storage>>['group']
|
||||
editor?: Editor
|
||||
}) => MarkSpec['group'])
|
||||
|
||||
/**
|
||||
* Spanning
|
||||
*/
|
||||
spanning?:
|
||||
| MarkSpec['spanning']
|
||||
| ((this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
parent: ParentConfig<MarkConfig<Options, Storage>>['spanning']
|
||||
editor?: Editor
|
||||
}) => MarkSpec['spanning'])
|
||||
|
||||
/**
|
||||
* Code
|
||||
*/
|
||||
code?:
|
||||
| boolean
|
||||
| ((this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
parent: ParentConfig<MarkConfig<Options, Storage>>['code']
|
||||
editor?: Editor
|
||||
}) => boolean)
|
||||
|
||||
/**
|
||||
* Parse HTML
|
||||
*/
|
||||
parseHTML?: (this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
parent: ParentConfig<MarkConfig<Options, Storage>>['parseHTML']
|
||||
editor?: Editor
|
||||
}) => MarkSpec['parseDOM']
|
||||
|
||||
/**
|
||||
* Render HTML
|
||||
*/
|
||||
renderHTML?:
|
||||
| ((
|
||||
this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
parent: ParentConfig<MarkConfig<Options, Storage>>['renderHTML']
|
||||
editor?: Editor
|
||||
},
|
||||
props: {
|
||||
mark: ProseMirrorMark
|
||||
HTMLAttributes: Record<string, any>
|
||||
},
|
||||
) => DOMOutputSpec)
|
||||
| null
|
||||
|
||||
/**
|
||||
* Attributes
|
||||
*/
|
||||
addAttributes?: (this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
parent: ParentConfig<MarkConfig<Options, Storage>>['addAttributes']
|
||||
editor?: Editor
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
}) => Attributes | {}
|
||||
}
|
||||
|
||||
/**
|
||||
* The Mark class is used to create custom mark extensions.
|
||||
* @see https://tiptap.dev/api/extensions#create-a-new-extension
|
||||
*/
|
||||
export class Mark<Options = any, Storage = any> extends Extendable<Options, Storage, MarkConfig<Options, Storage>> {
|
||||
type = 'mark'
|
||||
|
||||
/**
|
||||
* Create a new Mark instance
|
||||
* @param config - Mark configuration object or a function that returns a configuration object
|
||||
*/
|
||||
static create<O = any, S = any>(config: Partial<MarkConfig<O, S>> | (() => Partial<MarkConfig<O, S>>) = {}) {
|
||||
// If the config is a function, execute it to get the configuration object
|
||||
const resolvedConfig = typeof config === 'function' ? config() : config
|
||||
return new Mark<O, S>(resolvedConfig)
|
||||
}
|
||||
|
||||
static handleExit({ editor, mark }: { editor: Editor; mark: Mark }) {
|
||||
const { tr } = editor.state
|
||||
const currentPos = editor.state.selection.$from
|
||||
const isAtEnd = currentPos.pos === currentPos.end()
|
||||
|
||||
if (isAtEnd) {
|
||||
const currentMarks = currentPos.marks()
|
||||
const isInMark = !!currentMarks.find(m => m?.type.name === mark.name)
|
||||
|
||||
if (!isInMark) {
|
||||
return false
|
||||
}
|
||||
|
||||
const removeMark = currentMarks.find(m => m?.type.name === mark.name)
|
||||
|
||||
if (removeMark) {
|
||||
tr.removeStoredMark(removeMark)
|
||||
}
|
||||
tr.insertText(' ', currentPos.pos)
|
||||
|
||||
editor.view.dispatch(tr)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
configure(options?: Partial<Options>) {
|
||||
return super.configure(options) as Mark<Options, Storage>
|
||||
}
|
||||
|
||||
extend<
|
||||
ExtendedOptions = Options,
|
||||
ExtendedStorage = Storage,
|
||||
ExtendedConfig extends MarkConfig<ExtendedOptions, ExtendedStorage> = MarkConfig<ExtendedOptions, ExtendedStorage>,
|
||||
>(
|
||||
extendedConfig?:
|
||||
| (() => Partial<ExtendedConfig>)
|
||||
| (Partial<ExtendedConfig> &
|
||||
ThisType<{
|
||||
name: string
|
||||
options: ExtendedOptions
|
||||
storage: ExtendedStorage
|
||||
editor: Editor
|
||||
type: MarkType
|
||||
}>),
|
||||
): Mark<ExtendedOptions, ExtendedStorage> {
|
||||
// If the extended config is a function, execute it to get the configuration object
|
||||
const resolvedConfig = typeof extendedConfig === 'function' ? extendedConfig() : extendedConfig
|
||||
return super.extend(resolvedConfig) as Mark<ExtendedOptions, ExtendedStorage>
|
||||
}
|
||||
}
|
||||
122
node_modules/@tiptap/core/src/MarkView.ts
generated
vendored
Normal file
122
node_modules/@tiptap/core/src/MarkView.ts
generated
vendored
Normal file
@@ -0,0 +1,122 @@
|
||||
import type { Mark } from '@tiptap/pm/model'
|
||||
import type { ViewMutationRecord } from '@tiptap/pm/view'
|
||||
|
||||
import type { Editor } from './Editor.js'
|
||||
import type { MarkViewProps, MarkViewRendererOptions } from './types.js'
|
||||
import { isAndroid, isiOS } from './utilities/index.js'
|
||||
|
||||
export function updateMarkViewAttributes(checkMark: Mark, editor: Editor, attrs: Record<string, any> = {}): void {
|
||||
const { state } = editor
|
||||
const { doc, tr } = state
|
||||
const thisMark = checkMark
|
||||
|
||||
doc.descendants((node, pos) => {
|
||||
const from = tr.mapping.map(pos)
|
||||
const to = tr.mapping.map(pos) + node.nodeSize
|
||||
let foundMark: Mark | null = null
|
||||
|
||||
// find the mark on the current node
|
||||
node.marks.forEach(mark => {
|
||||
if (mark !== thisMark) {
|
||||
return false
|
||||
}
|
||||
|
||||
foundMark = mark
|
||||
})
|
||||
|
||||
if (!foundMark) {
|
||||
return
|
||||
}
|
||||
|
||||
// check if we need to update given the attributes
|
||||
let needsUpdate = false
|
||||
Object.keys(attrs).forEach(k => {
|
||||
if (attrs[k] !== foundMark!.attrs[k]) {
|
||||
needsUpdate = true
|
||||
}
|
||||
})
|
||||
|
||||
if (needsUpdate) {
|
||||
const updatedMark = checkMark.type.create({
|
||||
...checkMark.attrs,
|
||||
...attrs,
|
||||
})
|
||||
|
||||
tr.removeMark(from, to, checkMark.type)
|
||||
tr.addMark(from, to, updatedMark)
|
||||
}
|
||||
})
|
||||
|
||||
if (tr.docChanged) {
|
||||
editor.view.dispatch(tr)
|
||||
}
|
||||
}
|
||||
|
||||
export class MarkView<Component, Options extends MarkViewRendererOptions = MarkViewRendererOptions> {
|
||||
component: Component
|
||||
editor: Editor
|
||||
options: Options
|
||||
mark: MarkViewProps['mark']
|
||||
HTMLAttributes: MarkViewProps['HTMLAttributes']
|
||||
|
||||
constructor(component: Component, props: MarkViewProps, options?: Partial<Options>) {
|
||||
this.component = component
|
||||
this.editor = props.editor
|
||||
this.options = { ...options } as Options
|
||||
this.mark = props.mark
|
||||
this.HTMLAttributes = props.HTMLAttributes
|
||||
}
|
||||
|
||||
get dom(): HTMLElement {
|
||||
return this.editor.view.dom
|
||||
}
|
||||
|
||||
get contentDOM(): HTMLElement | null {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the attributes of the mark in the document.
|
||||
* @param attrs The attributes to update.
|
||||
*/
|
||||
updateAttributes(attrs: Record<string, any>, checkMark?: Mark): void {
|
||||
updateMarkViewAttributes(checkMark || this.mark, this.editor, attrs)
|
||||
}
|
||||
|
||||
ignoreMutation(mutation: ViewMutationRecord): boolean {
|
||||
if (!this.dom || !this.contentDOM) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (typeof this.options.ignoreMutation === 'function') {
|
||||
return this.options.ignoreMutation({ mutation })
|
||||
}
|
||||
|
||||
if (mutation.type === 'selection') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
this.dom.contains(mutation.target) &&
|
||||
mutation.type === 'childList' &&
|
||||
(isiOS() || isAndroid()) &&
|
||||
this.editor.isFocused
|
||||
) {
|
||||
const changedNodes = [...Array.from(mutation.addedNodes), ...Array.from(mutation.removedNodes)] as HTMLElement[]
|
||||
|
||||
if (changedNodes.every(node => node.isContentEditable)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (this.contentDOM === mutation.target && mutation.type === 'attributes') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (this.contentDOM.contains(mutation.target)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
377
node_modules/@tiptap/core/src/Node.ts
generated
vendored
Normal file
377
node_modules/@tiptap/core/src/Node.ts
generated
vendored
Normal file
@@ -0,0 +1,377 @@
|
||||
import type { DOMOutputSpec, Node as ProseMirrorNode, NodeSpec, NodeType } from '@tiptap/pm/model'
|
||||
|
||||
import type { Editor } from './Editor.js'
|
||||
import type { ExtendableConfig } from './Extendable.js'
|
||||
import { Extendable } from './Extendable.js'
|
||||
import type { Attributes, NodeViewRenderer, ParentConfig } from './types.js'
|
||||
|
||||
export interface NodeConfig<Options = any, Storage = any>
|
||||
extends ExtendableConfig<Options, Storage, NodeConfig<Options, Storage>, NodeType> {
|
||||
/**
|
||||
* Node View
|
||||
*/
|
||||
addNodeView?:
|
||||
| ((this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
editor: Editor
|
||||
type: NodeType
|
||||
parent: ParentConfig<NodeConfig<Options, Storage>>['addNodeView']
|
||||
}) => NodeViewRenderer | null)
|
||||
| null
|
||||
|
||||
/**
|
||||
* Defines if this node should be a top level node (doc)
|
||||
* @default false
|
||||
* @example true
|
||||
*/
|
||||
topNode?: boolean
|
||||
|
||||
/**
|
||||
* The content expression for this node, as described in the [schema
|
||||
* guide](/docs/guide/#schema.content_expressions). When not given,
|
||||
* the node does not allow any content.
|
||||
*
|
||||
* You can read more about it on the Prosemirror documentation here
|
||||
* @see https://prosemirror.net/docs/guide/#schema.content_expressions
|
||||
* @default undefined
|
||||
* @example content: 'block+'
|
||||
* @example content: 'headline paragraph block*'
|
||||
*/
|
||||
content?:
|
||||
| NodeSpec['content']
|
||||
| ((this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
parent: ParentConfig<NodeConfig<Options, Storage>>['content']
|
||||
editor?: Editor
|
||||
}) => NodeSpec['content'])
|
||||
|
||||
/**
|
||||
* The marks that are allowed inside of this node. May be a
|
||||
* space-separated string referring to mark names or groups, `"_"`
|
||||
* to explicitly allow all marks, or `""` to disallow marks. When
|
||||
* not given, nodes with inline content default to allowing all
|
||||
* marks, other nodes default to not allowing marks.
|
||||
*
|
||||
* @example marks: 'strong em'
|
||||
*/
|
||||
marks?:
|
||||
| NodeSpec['marks']
|
||||
| ((this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
parent: ParentConfig<NodeConfig<Options, Storage>>['marks']
|
||||
editor?: Editor
|
||||
}) => NodeSpec['marks'])
|
||||
|
||||
/**
|
||||
* The group or space-separated groups to which this node belongs,
|
||||
* which can be referred to in the content expressions for the
|
||||
* schema.
|
||||
*
|
||||
* By default Tiptap uses the groups 'block' and 'inline' for nodes. You
|
||||
* can also use custom groups if you want to group specific nodes together
|
||||
* and handle them in your schema.
|
||||
* @example group: 'block'
|
||||
* @example group: 'inline'
|
||||
* @example group: 'customBlock' // this uses a custom group
|
||||
*/
|
||||
group?:
|
||||
| NodeSpec['group']
|
||||
| ((this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
parent: ParentConfig<NodeConfig<Options, Storage>>['group']
|
||||
editor?: Editor
|
||||
}) => NodeSpec['group'])
|
||||
|
||||
/**
|
||||
* Should be set to true for inline nodes. (Implied for text nodes.)
|
||||
*/
|
||||
inline?:
|
||||
| NodeSpec['inline']
|
||||
| ((this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
parent: ParentConfig<NodeConfig<Options, Storage>>['inline']
|
||||
editor?: Editor
|
||||
}) => NodeSpec['inline'])
|
||||
|
||||
/**
|
||||
* Can be set to true to indicate that, though this isn't a [leaf
|
||||
* node](https://prosemirror.net/docs/ref/#model.NodeType.isLeaf), it doesn't have directly editable
|
||||
* content and should be treated as a single unit in the view.
|
||||
*
|
||||
* @example atom: true
|
||||
*/
|
||||
atom?:
|
||||
| NodeSpec['atom']
|
||||
| ((this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
parent: ParentConfig<NodeConfig<Options, Storage>>['atom']
|
||||
editor?: Editor
|
||||
}) => NodeSpec['atom'])
|
||||
|
||||
/**
|
||||
* Controls whether nodes of this type can be selected as a [node
|
||||
* selection](https://prosemirror.net/docs/ref/#state.NodeSelection). Defaults to true for non-text
|
||||
* nodes.
|
||||
*
|
||||
* @default true
|
||||
* @example selectable: false
|
||||
*/
|
||||
selectable?:
|
||||
| NodeSpec['selectable']
|
||||
| ((this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
parent: ParentConfig<NodeConfig<Options, Storage>>['selectable']
|
||||
editor?: Editor
|
||||
}) => NodeSpec['selectable'])
|
||||
|
||||
/**
|
||||
* Determines whether nodes of this type can be dragged without
|
||||
* being selected. Defaults to false.
|
||||
*
|
||||
* @default: false
|
||||
* @example: draggable: true
|
||||
*/
|
||||
draggable?:
|
||||
| NodeSpec['draggable']
|
||||
| ((this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
parent: ParentConfig<NodeConfig<Options, Storage>>['draggable']
|
||||
editor?: Editor
|
||||
}) => NodeSpec['draggable'])
|
||||
|
||||
/**
|
||||
* Can be used to indicate that this node contains code, which
|
||||
* causes some commands to behave differently.
|
||||
*/
|
||||
code?:
|
||||
| NodeSpec['code']
|
||||
| ((this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
parent: ParentConfig<NodeConfig<Options, Storage>>['code']
|
||||
editor?: Editor
|
||||
}) => NodeSpec['code'])
|
||||
|
||||
/**
|
||||
* Controls way whitespace in this a node is parsed. The default is
|
||||
* `"normal"`, which causes the [DOM parser](https://prosemirror.net/docs/ref/#model.DOMParser) to
|
||||
* collapse whitespace in normal mode, and normalize it (replacing
|
||||
* newlines and such with spaces) otherwise. `"pre"` causes the
|
||||
* parser to preserve spaces inside the node. When this option isn't
|
||||
* given, but [`code`](https://prosemirror.net/docs/ref/#model.NodeSpec.code) is true, `whitespace`
|
||||
* will default to `"pre"`. Note that this option doesn't influence
|
||||
* the way the node is rendered—that should be handled by `toDOM`
|
||||
* and/or styling.
|
||||
*/
|
||||
whitespace?:
|
||||
| NodeSpec['whitespace']
|
||||
| ((this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
parent: ParentConfig<NodeConfig<Options, Storage>>['whitespace']
|
||||
editor?: Editor
|
||||
}) => NodeSpec['whitespace'])
|
||||
|
||||
/**
|
||||
* Allows a **single** node to be set as linebreak equivalent (e.g. hardBreak).
|
||||
* When converting between block types that have whitespace set to "pre"
|
||||
* and don't support the linebreak node (e.g. codeBlock) and other block types
|
||||
* that do support the linebreak node (e.g. paragraphs) - this node will be used
|
||||
* as the linebreak instead of stripping the newline.
|
||||
*
|
||||
* See [linebreakReplacement](https://prosemirror.net/docs/ref/#model.NodeSpec.linebreakReplacement).
|
||||
*/
|
||||
linebreakReplacement?:
|
||||
| NodeSpec['linebreakReplacement']
|
||||
| ((this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
parent: ParentConfig<NodeConfig<Options, Storage>>['linebreakReplacement']
|
||||
editor?: Editor
|
||||
}) => NodeSpec['linebreakReplacement'])
|
||||
|
||||
/**
|
||||
* When enabled, enables both
|
||||
* [`definingAsContext`](https://prosemirror.net/docs/ref/#model.NodeSpec.definingAsContext) and
|
||||
* [`definingForContent`](https://prosemirror.net/docs/ref/#model.NodeSpec.definingForContent).
|
||||
*
|
||||
* @default false
|
||||
* @example isolating: true
|
||||
*/
|
||||
defining?:
|
||||
| NodeSpec['defining']
|
||||
| ((this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
parent: ParentConfig<NodeConfig<Options, Storage>>['defining']
|
||||
editor?: Editor
|
||||
}) => NodeSpec['defining'])
|
||||
|
||||
/**
|
||||
* When enabled (default is false), the sides of nodes of this type
|
||||
* count as boundaries that regular editing operations, like
|
||||
* backspacing or lifting, won't cross. An example of a node that
|
||||
* should probably have this enabled is a table cell.
|
||||
*/
|
||||
isolating?:
|
||||
| NodeSpec['isolating']
|
||||
| ((this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
parent: ParentConfig<NodeConfig<Options, Storage>>['isolating']
|
||||
editor?: Editor
|
||||
}) => NodeSpec['isolating'])
|
||||
|
||||
/**
|
||||
* Associates DOM parser information with this node, which can be
|
||||
* used by [`DOMParser.fromSchema`](https://prosemirror.net/docs/ref/#model.DOMParser^fromSchema) to
|
||||
* automatically derive a parser. The `node` field in the rules is
|
||||
* implied (the name of this node will be filled in automatically).
|
||||
* If you supply your own parser, you do not need to also specify
|
||||
* parsing rules in your schema.
|
||||
*
|
||||
* @example parseHTML: [{ tag: 'div', attrs: { 'data-id': 'my-block' } }]
|
||||
*/
|
||||
parseHTML?: (this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
parent: ParentConfig<NodeConfig<Options, Storage>>['parseHTML']
|
||||
editor?: Editor
|
||||
}) => NodeSpec['parseDOM']
|
||||
|
||||
/**
|
||||
* A description of a DOM structure. Can be either a string, which is
|
||||
* interpreted as a text node, a DOM node, which is interpreted as
|
||||
* itself, a `{dom, contentDOM}` object, or an array.
|
||||
*
|
||||
* An array describes a DOM element. The first value in the array
|
||||
* should be a string—the name of the DOM element, optionally prefixed
|
||||
* by a namespace URL and a space. If the second element is plain
|
||||
* object, it is interpreted as a set of attributes for the element.
|
||||
* Any elements after that (including the 2nd if it's not an attribute
|
||||
* object) are interpreted as children of the DOM elements, and must
|
||||
* either be valid `DOMOutputSpec` values, or the number zero.
|
||||
*
|
||||
* The number zero (pronounced “hole”) is used to indicate the place
|
||||
* where a node's child nodes should be inserted. If it occurs in an
|
||||
* output spec, it should be the only child element in its parent
|
||||
* node.
|
||||
*
|
||||
* @example toDOM: ['div[data-id="my-block"]', { class: 'my-block' }, 0]
|
||||
*/
|
||||
renderHTML?:
|
||||
| ((
|
||||
this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
parent: ParentConfig<NodeConfig<Options, Storage>>['renderHTML']
|
||||
editor?: Editor
|
||||
},
|
||||
props: {
|
||||
node: ProseMirrorNode
|
||||
HTMLAttributes: Record<string, any>
|
||||
},
|
||||
) => DOMOutputSpec)
|
||||
| null
|
||||
|
||||
/**
|
||||
* renders the node as text
|
||||
* @example renderText: () => 'foo
|
||||
*/
|
||||
renderText?:
|
||||
| ((
|
||||
this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
parent: ParentConfig<NodeConfig<Options, Storage>>['renderText']
|
||||
editor?: Editor
|
||||
},
|
||||
props: {
|
||||
node: ProseMirrorNode
|
||||
pos: number
|
||||
parent: ProseMirrorNode
|
||||
index: number
|
||||
},
|
||||
) => string)
|
||||
| null
|
||||
|
||||
/**
|
||||
* Add attributes to the node
|
||||
* @example addAttributes: () => ({ class: 'foo' })
|
||||
*/
|
||||
addAttributes?: (this: {
|
||||
name: string
|
||||
options: Options
|
||||
storage: Storage
|
||||
parent: ParentConfig<NodeConfig<Options, Storage>>['addAttributes']
|
||||
editor?: Editor
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
}) => Attributes | {}
|
||||
}
|
||||
|
||||
/**
|
||||
* The Node class is used to create custom node extensions.
|
||||
* @see https://tiptap.dev/api/extensions#create-a-new-extension
|
||||
*/
|
||||
export class Node<Options = any, Storage = any> extends Extendable<Options, Storage, NodeConfig<Options, Storage>> {
|
||||
type = 'node'
|
||||
|
||||
/**
|
||||
* Create a new Node instance
|
||||
* @param config - Node configuration object or a function that returns a configuration object
|
||||
*/
|
||||
static create<O = any, S = any>(config: Partial<NodeConfig<O, S>> | (() => Partial<NodeConfig<O, S>>) = {}) {
|
||||
// If the config is a function, execute it to get the configuration object
|
||||
const resolvedConfig = typeof config === 'function' ? config() : config
|
||||
return new Node<O, S>(resolvedConfig)
|
||||
}
|
||||
|
||||
configure(options?: Partial<Options>) {
|
||||
return super.configure(options) as Node<Options, Storage>
|
||||
}
|
||||
|
||||
extend<
|
||||
ExtendedOptions = Options,
|
||||
ExtendedStorage = Storage,
|
||||
ExtendedConfig extends NodeConfig<ExtendedOptions, ExtendedStorage> = NodeConfig<ExtendedOptions, ExtendedStorage>,
|
||||
>(
|
||||
extendedConfig?:
|
||||
| (() => Partial<ExtendedConfig>)
|
||||
| (Partial<ExtendedConfig> &
|
||||
ThisType<{
|
||||
name: string
|
||||
options: ExtendedOptions
|
||||
storage: ExtendedStorage
|
||||
editor: Editor
|
||||
type: NodeType
|
||||
}>),
|
||||
): Node<ExtendedOptions, ExtendedStorage> {
|
||||
// If the extended config is a function, execute it to get the configuration object
|
||||
const resolvedConfig = typeof extendedConfig === 'function' ? extendedConfig() : extendedConfig
|
||||
return super.extend(resolvedConfig) as Node<ExtendedOptions, ExtendedStorage>
|
||||
}
|
||||
}
|
||||
257
node_modules/@tiptap/core/src/NodePos.ts
generated
vendored
Normal file
257
node_modules/@tiptap/core/src/NodePos.ts
generated
vendored
Normal file
@@ -0,0 +1,257 @@
|
||||
import type { Fragment, Node, ResolvedPos } from '@tiptap/pm/model'
|
||||
|
||||
import type { Editor } from './Editor.js'
|
||||
import type { Content, Range } from './types.js'
|
||||
|
||||
export class NodePos {
|
||||
private resolvedPos: ResolvedPos
|
||||
|
||||
private isBlock: boolean
|
||||
|
||||
private editor: Editor
|
||||
|
||||
private get name(): string {
|
||||
return this.node.type.name
|
||||
}
|
||||
|
||||
constructor(pos: ResolvedPos, editor: Editor, isBlock = false, node: Node | null = null) {
|
||||
this.isBlock = isBlock
|
||||
this.resolvedPos = pos
|
||||
this.editor = editor
|
||||
this.currentNode = node
|
||||
}
|
||||
|
||||
private currentNode: Node | null = null
|
||||
|
||||
get node(): Node {
|
||||
return this.currentNode || this.resolvedPos.node()
|
||||
}
|
||||
|
||||
get element(): HTMLElement {
|
||||
return this.editor.view.domAtPos(this.pos).node as HTMLElement
|
||||
}
|
||||
|
||||
public actualDepth: number | null = null
|
||||
|
||||
get depth(): number {
|
||||
return this.actualDepth ?? this.resolvedPos.depth
|
||||
}
|
||||
|
||||
get pos(): number {
|
||||
return this.resolvedPos.pos
|
||||
}
|
||||
|
||||
get content(): Fragment {
|
||||
return this.node.content
|
||||
}
|
||||
|
||||
set content(content: Content) {
|
||||
let from = this.from
|
||||
let to = this.to
|
||||
|
||||
if (this.isBlock) {
|
||||
if (this.content.size === 0) {
|
||||
console.error(`You can’t set content on a block node. Tried to set content on ${this.name} at ${this.pos}`)
|
||||
return
|
||||
}
|
||||
|
||||
from = this.from + 1
|
||||
to = this.to - 1
|
||||
}
|
||||
|
||||
this.editor.commands.insertContentAt({ from, to }, content)
|
||||
}
|
||||
|
||||
get attributes(): { [key: string]: any } {
|
||||
return this.node.attrs
|
||||
}
|
||||
|
||||
get textContent(): string {
|
||||
return this.node.textContent
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.node.nodeSize
|
||||
}
|
||||
|
||||
get from(): number {
|
||||
if (this.isBlock) {
|
||||
return this.pos
|
||||
}
|
||||
|
||||
return this.resolvedPos.start(this.resolvedPos.depth)
|
||||
}
|
||||
|
||||
get range(): Range {
|
||||
return {
|
||||
from: this.from,
|
||||
to: this.to,
|
||||
}
|
||||
}
|
||||
|
||||
get to(): number {
|
||||
if (this.isBlock) {
|
||||
return this.pos + this.size
|
||||
}
|
||||
|
||||
return this.resolvedPos.end(this.resolvedPos.depth) + (this.node.isText ? 0 : 1)
|
||||
}
|
||||
|
||||
get parent(): NodePos | null {
|
||||
if (this.depth === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const parentPos = this.resolvedPos.start(this.resolvedPos.depth - 1)
|
||||
const $pos = this.resolvedPos.doc.resolve(parentPos)
|
||||
|
||||
return new NodePos($pos, this.editor)
|
||||
}
|
||||
|
||||
get before(): NodePos | null {
|
||||
let $pos = this.resolvedPos.doc.resolve(this.from - (this.isBlock ? 1 : 2))
|
||||
|
||||
if ($pos.depth !== this.depth) {
|
||||
$pos = this.resolvedPos.doc.resolve(this.from - 3)
|
||||
}
|
||||
|
||||
return new NodePos($pos, this.editor)
|
||||
}
|
||||
|
||||
get after(): NodePos | null {
|
||||
let $pos = this.resolvedPos.doc.resolve(this.to + (this.isBlock ? 2 : 1))
|
||||
|
||||
if ($pos.depth !== this.depth) {
|
||||
$pos = this.resolvedPos.doc.resolve(this.to + 3)
|
||||
}
|
||||
|
||||
return new NodePos($pos, this.editor)
|
||||
}
|
||||
|
||||
get children(): NodePos[] {
|
||||
const children: NodePos[] = []
|
||||
|
||||
this.node.content.forEach((node, offset) => {
|
||||
const isBlock = node.isBlock && !node.isTextblock
|
||||
const isNonTextAtom = node.isAtom && !node.isText
|
||||
const isInline = node.isInline
|
||||
|
||||
const targetPos = this.pos + offset + (isNonTextAtom ? 0 : 1)
|
||||
|
||||
// Check if targetPos is within valid document range
|
||||
if (targetPos < 0 || targetPos > this.resolvedPos.doc.nodeSize - 2) {
|
||||
return
|
||||
}
|
||||
|
||||
const $pos = this.resolvedPos.doc.resolve(targetPos)
|
||||
|
||||
// Only apply depth check for non-block, non-inline nodes (i.e., textblocks)
|
||||
// Inline nodes should always be included as children since we're iterating
|
||||
// over direct children via this.node.content
|
||||
if (!isBlock && !isInline && $pos.depth <= this.depth) {
|
||||
return
|
||||
}
|
||||
|
||||
// Pass the node for both block and inline nodes to ensure correct node reference
|
||||
const childNodePos = new NodePos($pos, this.editor, isBlock, isBlock || isInline ? node : null)
|
||||
|
||||
if (isBlock) {
|
||||
childNodePos.actualDepth = this.depth + 1
|
||||
}
|
||||
|
||||
children.push(childNodePos)
|
||||
})
|
||||
|
||||
return children
|
||||
}
|
||||
|
||||
get firstChild(): NodePos | null {
|
||||
return this.children[0] || null
|
||||
}
|
||||
|
||||
get lastChild(): NodePos | null {
|
||||
const children = this.children
|
||||
|
||||
return children[children.length - 1] || null
|
||||
}
|
||||
|
||||
closest(selector: string, attributes: { [key: string]: any } = {}): NodePos | null {
|
||||
let node: NodePos | null = null
|
||||
let currentNode = this.parent
|
||||
|
||||
while (currentNode && !node) {
|
||||
if (currentNode.node.type.name === selector) {
|
||||
if (Object.keys(attributes).length > 0) {
|
||||
const nodeAttributes = currentNode.node.attrs
|
||||
const attrKeys = Object.keys(attributes)
|
||||
|
||||
for (let index = 0; index < attrKeys.length; index += 1) {
|
||||
const key = attrKeys[index]
|
||||
|
||||
if (nodeAttributes[key] !== attributes[key]) {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
node = currentNode
|
||||
}
|
||||
}
|
||||
|
||||
currentNode = currentNode.parent
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
querySelector(selector: string, attributes: { [key: string]: any } = {}): NodePos | null {
|
||||
return this.querySelectorAll(selector, attributes, true)[0] || null
|
||||
}
|
||||
|
||||
querySelectorAll(selector: string, attributes: { [key: string]: any } = {}, firstItemOnly = false): NodePos[] {
|
||||
let nodes: NodePos[] = []
|
||||
|
||||
if (!this.children || this.children.length === 0) {
|
||||
return nodes
|
||||
}
|
||||
const attrKeys = Object.keys(attributes)
|
||||
|
||||
/**
|
||||
* Finds all children recursively that match the selector and attributes
|
||||
* If firstItemOnly is true, it will return the first item found
|
||||
*/
|
||||
this.children.forEach(childPos => {
|
||||
// If we already found a node and we only want the first item, we dont need to keep going
|
||||
if (firstItemOnly && nodes.length > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (childPos.node.type.name === selector) {
|
||||
const doesAllAttributesMatch = attrKeys.every(key => attributes[key] === childPos.node.attrs[key])
|
||||
|
||||
if (doesAllAttributesMatch) {
|
||||
nodes.push(childPos)
|
||||
}
|
||||
}
|
||||
|
||||
// If we already found a node and we only want the first item, we can stop here and skip the recursion
|
||||
if (firstItemOnly && nodes.length > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
nodes = nodes.concat(childPos.querySelectorAll(selector, attributes, firstItemOnly))
|
||||
})
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
setAttribute(attributes: { [key: string]: any }) {
|
||||
const { tr } = this.editor.state
|
||||
|
||||
tr.setNodeMarkup(this.from, undefined, {
|
||||
...this.node.attrs,
|
||||
...attributes,
|
||||
})
|
||||
|
||||
this.editor.view.dispatch(tr)
|
||||
}
|
||||
}
|
||||
339
node_modules/@tiptap/core/src/NodeView.ts
generated
vendored
Normal file
339
node_modules/@tiptap/core/src/NodeView.ts
generated
vendored
Normal file
@@ -0,0 +1,339 @@
|
||||
import { NodeSelection } from '@tiptap/pm/state'
|
||||
import type { NodeView as ProseMirrorNodeView, ViewMutationRecord } from '@tiptap/pm/view'
|
||||
|
||||
import type { Editor as CoreEditor } from './Editor.js'
|
||||
import type { DecorationWithType, NodeViewRendererOptions, NodeViewRendererProps } from './types.js'
|
||||
import { isAndroid } from './utilities/isAndroid.js'
|
||||
import { isiOS } from './utilities/isiOS.js'
|
||||
|
||||
/**
|
||||
* Node views are used to customize the rendered DOM structure of a node.
|
||||
* @see https://tiptap.dev/guide/node-views
|
||||
*/
|
||||
export class NodeView<
|
||||
Component,
|
||||
NodeEditor extends CoreEditor = CoreEditor,
|
||||
Options extends NodeViewRendererOptions = NodeViewRendererOptions,
|
||||
> implements ProseMirrorNodeView
|
||||
{
|
||||
component: Component
|
||||
|
||||
editor: NodeEditor
|
||||
|
||||
options: Options
|
||||
|
||||
extension: NodeViewRendererProps['extension']
|
||||
|
||||
node: NodeViewRendererProps['node']
|
||||
|
||||
decorations: NodeViewRendererProps['decorations']
|
||||
|
||||
innerDecorations: NodeViewRendererProps['innerDecorations']
|
||||
|
||||
view: NodeViewRendererProps['view']
|
||||
|
||||
getPos: NodeViewRendererProps['getPos']
|
||||
|
||||
HTMLAttributes: NodeViewRendererProps['HTMLAttributes']
|
||||
|
||||
isDragging = false
|
||||
|
||||
constructor(component: Component, props: NodeViewRendererProps, options?: Partial<Options>) {
|
||||
this.component = component
|
||||
this.editor = props.editor as NodeEditor
|
||||
this.options = {
|
||||
stopEvent: null,
|
||||
ignoreMutation: null,
|
||||
...options,
|
||||
} as Options
|
||||
this.extension = props.extension
|
||||
this.node = props.node
|
||||
this.decorations = props.decorations as DecorationWithType[]
|
||||
this.innerDecorations = props.innerDecorations
|
||||
this.view = props.view
|
||||
this.HTMLAttributes = props.HTMLAttributes
|
||||
this.getPos = props.getPos
|
||||
this.mount()
|
||||
}
|
||||
|
||||
mount() {
|
||||
// eslint-disable-next-line
|
||||
return
|
||||
}
|
||||
|
||||
get dom(): HTMLElement {
|
||||
return this.editor.view.dom as HTMLElement
|
||||
}
|
||||
|
||||
get contentDOM(): HTMLElement | null {
|
||||
return null
|
||||
}
|
||||
|
||||
onDragStart(event: DragEvent) {
|
||||
const { view } = this.editor
|
||||
const target = event.target as HTMLElement
|
||||
|
||||
// get the drag handle element
|
||||
// `closest` is not available for text nodes so we may have to use its parent
|
||||
const dragHandle =
|
||||
target.nodeType === 3 ? target.parentElement?.closest('[data-drag-handle]') : target.closest('[data-drag-handle]')
|
||||
|
||||
if (!this.dom || this.contentDOM?.contains(target) || !dragHandle) {
|
||||
return
|
||||
}
|
||||
|
||||
let x = 0
|
||||
let y = 0
|
||||
|
||||
// calculate offset for drag element if we use a different drag handle element
|
||||
if (this.dom !== dragHandle) {
|
||||
const domBox = this.dom.getBoundingClientRect()
|
||||
const handleBox = dragHandle.getBoundingClientRect()
|
||||
|
||||
// In React, we have to go through nativeEvent to reach offsetX/offsetY.
|
||||
const offsetX = event.offsetX ?? (event as any).nativeEvent?.offsetX
|
||||
const offsetY = event.offsetY ?? (event as any).nativeEvent?.offsetY
|
||||
|
||||
x = handleBox.x - domBox.x + offsetX
|
||||
y = handleBox.y - domBox.y + offsetY
|
||||
}
|
||||
|
||||
const clonedNode = this.dom.cloneNode(true) as HTMLElement
|
||||
|
||||
// Preserve the visual size of the original when using the clone as
|
||||
// the drag image.
|
||||
try {
|
||||
const domBox = this.dom.getBoundingClientRect()
|
||||
clonedNode.style.width = `${Math.round(domBox.width)}px`
|
||||
clonedNode.style.height = `${Math.round(domBox.height)}px`
|
||||
clonedNode.style.boxSizing = 'border-box'
|
||||
// Ensure the clone doesn't capture pointer events while offscreen
|
||||
clonedNode.style.pointerEvents = 'none'
|
||||
} catch {
|
||||
// ignore measurement errors (e.g. if element not in DOM)
|
||||
}
|
||||
|
||||
// Some browsers (notably Safari) require the element passed to
|
||||
// setDragImage to be present in the DOM. Using a detached node can
|
||||
// cause the drag to immediately end.
|
||||
let dragImageWrapper: HTMLElement | null = null
|
||||
|
||||
try {
|
||||
dragImageWrapper = document.createElement('div')
|
||||
dragImageWrapper.style.position = 'absolute'
|
||||
dragImageWrapper.style.top = '-9999px'
|
||||
dragImageWrapper.style.left = '-9999px'
|
||||
dragImageWrapper.style.pointerEvents = 'none'
|
||||
dragImageWrapper.appendChild(clonedNode)
|
||||
document.body.appendChild(dragImageWrapper)
|
||||
|
||||
event.dataTransfer?.setDragImage(clonedNode, x, y)
|
||||
} finally {
|
||||
// Remove the wrapper on the next tick so the browser can use the
|
||||
// element as the drag image. A 0ms timeout is enough in practice.
|
||||
if (dragImageWrapper) {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
dragImageWrapper?.remove()
|
||||
} catch {
|
||||
// ignore removal errors
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
const pos = this.getPos()
|
||||
|
||||
if (typeof pos !== 'number') {
|
||||
return
|
||||
}
|
||||
// we need to tell ProseMirror that we want to move the whole node
|
||||
// so we create a NodeSelection
|
||||
const selection = NodeSelection.create(view.state.doc, pos)
|
||||
const transaction = view.state.tr.setSelection(selection)
|
||||
|
||||
view.dispatch(transaction)
|
||||
}
|
||||
|
||||
stopEvent(event: Event) {
|
||||
if (!this.dom) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof this.options.stopEvent === 'function') {
|
||||
return this.options.stopEvent({ event })
|
||||
}
|
||||
|
||||
const target = event.target as HTMLElement
|
||||
const isInElement = this.dom.contains(target) && !this.contentDOM?.contains(target)
|
||||
|
||||
// any event from child nodes should be handled by ProseMirror
|
||||
if (!isInElement) {
|
||||
return false
|
||||
}
|
||||
|
||||
const isDragEvent = event.type.startsWith('drag')
|
||||
const isDropEvent = event.type === 'drop'
|
||||
const isInput = ['INPUT', 'BUTTON', 'SELECT', 'TEXTAREA'].includes(target.tagName) || target.isContentEditable
|
||||
|
||||
// any input event within node views should be ignored by ProseMirror
|
||||
if (isInput && !isDropEvent && !isDragEvent) {
|
||||
return true
|
||||
}
|
||||
|
||||
const { isEditable } = this.editor
|
||||
const { isDragging } = this
|
||||
const isDraggable = !!this.node.type.spec.draggable
|
||||
const isSelectable = NodeSelection.isSelectable(this.node)
|
||||
const isCopyEvent = event.type === 'copy'
|
||||
const isPasteEvent = event.type === 'paste'
|
||||
const isCutEvent = event.type === 'cut'
|
||||
const isClickEvent = event.type === 'mousedown'
|
||||
|
||||
// ProseMirror tries to drag selectable nodes
|
||||
// even if `draggable` is set to `false`
|
||||
// this fix prevents that
|
||||
if (!isDraggable && isSelectable && isDragEvent && event.target === this.dom) {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
if (isDraggable && isDragEvent && !isDragging && event.target === this.dom) {
|
||||
event.preventDefault()
|
||||
return false
|
||||
}
|
||||
|
||||
// we have to store that dragging started
|
||||
if (isDraggable && isEditable && !isDragging && isClickEvent) {
|
||||
const dragHandle = target.closest('[data-drag-handle]')
|
||||
const isValidDragHandle = dragHandle && (this.dom === dragHandle || this.dom.contains(dragHandle))
|
||||
|
||||
if (isValidDragHandle) {
|
||||
this.isDragging = true
|
||||
|
||||
document.addEventListener(
|
||||
'dragend',
|
||||
() => {
|
||||
this.isDragging = false
|
||||
},
|
||||
{ once: true },
|
||||
)
|
||||
|
||||
document.addEventListener(
|
||||
'drop',
|
||||
() => {
|
||||
this.isDragging = false
|
||||
},
|
||||
{ once: true },
|
||||
)
|
||||
|
||||
document.addEventListener(
|
||||
'mouseup',
|
||||
() => {
|
||||
this.isDragging = false
|
||||
},
|
||||
{ once: true },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// these events are handled by prosemirror
|
||||
if (isDragging || isDropEvent || isCopyEvent || isPasteEvent || isCutEvent || (isClickEvent && isSelectable)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a DOM [mutation](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) or a selection change happens within the view.
|
||||
* @return `false` if the editor should re-read the selection or re-parse the range around the mutation
|
||||
* @return `true` if it can safely be ignored.
|
||||
*/
|
||||
ignoreMutation(mutation: ViewMutationRecord) {
|
||||
if (!this.dom || !this.contentDOM) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (typeof this.options.ignoreMutation === 'function') {
|
||||
return this.options.ignoreMutation({ mutation })
|
||||
}
|
||||
|
||||
// a leaf/atom node is like a black box for ProseMirror
|
||||
// and should be fully handled by the node view
|
||||
if (this.node.isLeaf || this.node.isAtom) {
|
||||
return true
|
||||
}
|
||||
|
||||
// ProseMirror should handle any selections
|
||||
if (mutation.type === 'selection') {
|
||||
return false
|
||||
}
|
||||
|
||||
// try to prevent a bug on iOS and Android that will break node views on enter
|
||||
// this is because ProseMirror can’t preventDispatch on enter
|
||||
// this will lead to a re-render of the node view on enter
|
||||
// see: https://github.com/ueberdosis/tiptap/issues/1214
|
||||
// see: https://github.com/ueberdosis/tiptap/issues/2534
|
||||
if (
|
||||
this.dom.contains(mutation.target) &&
|
||||
mutation.type === 'childList' &&
|
||||
(isiOS() || isAndroid()) &&
|
||||
this.editor.isFocused
|
||||
) {
|
||||
const changedNodes = [...Array.from(mutation.addedNodes), ...Array.from(mutation.removedNodes)] as HTMLElement[]
|
||||
|
||||
// we’ll check if every changed node is contentEditable
|
||||
// to make sure it’s probably mutated by ProseMirror
|
||||
if (changedNodes.every(node => node.isContentEditable)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// we will allow mutation contentDOM with attributes
|
||||
// so we can for example adding classes within our node view
|
||||
if (this.contentDOM === mutation.target && mutation.type === 'attributes') {
|
||||
return true
|
||||
}
|
||||
|
||||
// ProseMirror should handle any changes within contentDOM
|
||||
if (this.contentDOM.contains(mutation.target)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the attributes of the prosemirror node.
|
||||
*/
|
||||
updateAttributes(attributes: Record<string, any>): void {
|
||||
this.editor.commands.command(({ tr }) => {
|
||||
const pos = this.getPos()
|
||||
|
||||
if (typeof pos !== 'number') {
|
||||
return false
|
||||
}
|
||||
|
||||
tr.setNodeMarkup(pos, undefined, {
|
||||
...this.node.attrs,
|
||||
...attributes,
|
||||
})
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the node.
|
||||
*/
|
||||
deleteNode(): void {
|
||||
const from = this.getPos()
|
||||
|
||||
if (typeof from !== 'number') {
|
||||
return
|
||||
}
|
||||
const to = from + this.node.nodeSize
|
||||
|
||||
this.editor.commands.deleteRange({ from, to })
|
||||
}
|
||||
}
|
||||
373
node_modules/@tiptap/core/src/PasteRule.ts
generated
vendored
Normal file
373
node_modules/@tiptap/core/src/PasteRule.ts
generated
vendored
Normal file
@@ -0,0 +1,373 @@
|
||||
import type { Node as ProseMirrorNode } from '@tiptap/pm/model'
|
||||
import { Fragment } from '@tiptap/pm/model'
|
||||
import type { EditorState } from '@tiptap/pm/state'
|
||||
import { Plugin } from '@tiptap/pm/state'
|
||||
|
||||
import { CommandManager } from './CommandManager.js'
|
||||
import type { Editor } from './Editor.js'
|
||||
import { createChainableState } from './helpers/createChainableState.js'
|
||||
import { getHTMLFromFragment } from './helpers/getHTMLFromFragment.js'
|
||||
import type { CanCommands, ChainedCommands, ExtendedRegExpMatchArray, Range, SingleCommands } from './types.js'
|
||||
import { isNumber } from './utilities/isNumber.js'
|
||||
import { isRegExp } from './utilities/isRegExp.js'
|
||||
|
||||
export type PasteRuleMatch = {
|
||||
index: number
|
||||
text: string
|
||||
replaceWith?: string
|
||||
match?: RegExpMatchArray
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
export type PasteRuleFinder =
|
||||
| RegExp
|
||||
| ((text: string, event?: ClipboardEvent | null) => PasteRuleMatch[] | null | undefined)
|
||||
|
||||
/**
|
||||
* Paste rules are used to react to pasted content.
|
||||
* @see https://tiptap.dev/docs/editor/extensions/custom-extensions/extend-existing#paste-rules
|
||||
*/
|
||||
export class PasteRule {
|
||||
find: PasteRuleFinder
|
||||
|
||||
handler: (props: {
|
||||
state: EditorState
|
||||
range: Range
|
||||
match: ExtendedRegExpMatchArray
|
||||
commands: SingleCommands
|
||||
chain: () => ChainedCommands
|
||||
can: () => CanCommands
|
||||
pasteEvent: ClipboardEvent | null
|
||||
dropEvent: DragEvent | null
|
||||
}) => void | null
|
||||
|
||||
constructor(config: {
|
||||
find: PasteRuleFinder
|
||||
handler: (props: {
|
||||
can: () => CanCommands
|
||||
chain: () => ChainedCommands
|
||||
commands: SingleCommands
|
||||
dropEvent: DragEvent | null
|
||||
match: ExtendedRegExpMatchArray
|
||||
pasteEvent: ClipboardEvent | null
|
||||
range: Range
|
||||
state: EditorState
|
||||
}) => void | null
|
||||
}) {
|
||||
this.find = config.find
|
||||
this.handler = config.handler
|
||||
}
|
||||
}
|
||||
|
||||
const pasteRuleMatcherHandler = (
|
||||
text: string,
|
||||
find: PasteRuleFinder,
|
||||
event?: ClipboardEvent | null,
|
||||
): ExtendedRegExpMatchArray[] => {
|
||||
if (isRegExp(find)) {
|
||||
return [...text.matchAll(find)]
|
||||
}
|
||||
|
||||
const matches = find(text, event)
|
||||
|
||||
if (!matches) {
|
||||
return []
|
||||
}
|
||||
|
||||
return matches.map(pasteRuleMatch => {
|
||||
const result: ExtendedRegExpMatchArray = [pasteRuleMatch.text]
|
||||
|
||||
result.index = pasteRuleMatch.index
|
||||
result.input = text
|
||||
result.data = pasteRuleMatch.data
|
||||
|
||||
if (pasteRuleMatch.replaceWith) {
|
||||
if (!pasteRuleMatch.text.includes(pasteRuleMatch.replaceWith)) {
|
||||
console.warn('[tiptap warn]: "pasteRuleMatch.replaceWith" must be part of "pasteRuleMatch.text".')
|
||||
}
|
||||
|
||||
result.push(pasteRuleMatch.replaceWith)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
function run(config: {
|
||||
editor: Editor
|
||||
state: EditorState
|
||||
from: number
|
||||
to: number
|
||||
rule: PasteRule
|
||||
pasteEvent: ClipboardEvent | null
|
||||
dropEvent: DragEvent | null
|
||||
}): boolean {
|
||||
const { editor, state, from, to, rule, pasteEvent, dropEvent } = config
|
||||
|
||||
const { commands, chain, can } = new CommandManager({
|
||||
editor,
|
||||
state,
|
||||
})
|
||||
|
||||
const handlers: (void | null)[] = []
|
||||
|
||||
state.doc.nodesBetween(from, to, (node, pos) => {
|
||||
// Skip code blocks and non-textual nodes.
|
||||
// Be defensive: `node` may be a Fragment without a `type`. Only text,
|
||||
// inline, or textblock nodes are processed by paste rules.
|
||||
if (node.type?.spec?.code || !(node.isText || node.isTextblock || node.isInline)) {
|
||||
return
|
||||
}
|
||||
|
||||
// For textblock and inline/text nodes, compute the range relative to the node.
|
||||
// Prefer `node.nodeSize` when available (some Node shapes expose this),
|
||||
// otherwise fall back to `node.content?.size`. Default to 0 if neither exists.
|
||||
const contentSize = node.content?.size ?? node.nodeSize ?? 0
|
||||
const resolvedFrom = Math.max(from, pos)
|
||||
const resolvedTo = Math.min(to, pos + contentSize)
|
||||
|
||||
// If the resolved range is empty or invalid for this node, skip it. This
|
||||
// avoids calling `textBetween` with start > end which can cause internal
|
||||
// Fragment/Node traversal to access undefined `nodeSize` values.
|
||||
if (resolvedFrom >= resolvedTo) {
|
||||
return
|
||||
}
|
||||
|
||||
const textToMatch = node.isText
|
||||
? node.text || ''
|
||||
: node.textBetween(resolvedFrom - pos, resolvedTo - pos, undefined, '\ufffc')
|
||||
|
||||
const matches = pasteRuleMatcherHandler(textToMatch, rule.find, pasteEvent)
|
||||
|
||||
matches.forEach(match => {
|
||||
if (match.index === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const start = resolvedFrom + match.index + 1
|
||||
const end = start + match[0].length
|
||||
const range = {
|
||||
from: state.tr.mapping.map(start),
|
||||
to: state.tr.mapping.map(end),
|
||||
}
|
||||
|
||||
const handler = rule.handler({
|
||||
state,
|
||||
range,
|
||||
match,
|
||||
commands,
|
||||
chain,
|
||||
can,
|
||||
pasteEvent,
|
||||
dropEvent,
|
||||
})
|
||||
|
||||
handlers.push(handler)
|
||||
})
|
||||
})
|
||||
|
||||
const success = handlers.every(handler => handler !== null)
|
||||
|
||||
return success
|
||||
}
|
||||
|
||||
// When dragging across editors, must get another editor instance to delete selection content.
|
||||
let tiptapDragFromOtherEditor: Editor | null = null
|
||||
|
||||
const createClipboardPasteEvent = (text: string) => {
|
||||
const event = new ClipboardEvent('paste', {
|
||||
clipboardData: new DataTransfer(),
|
||||
})
|
||||
|
||||
event.clipboardData?.setData('text/html', text)
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an paste rules plugin. When enabled, it will cause pasted
|
||||
* text that matches any of the given rules to trigger the rule’s
|
||||
* action.
|
||||
*/
|
||||
export function pasteRulesPlugin(props: { editor: Editor; rules: PasteRule[] }): Plugin[] {
|
||||
const { editor, rules } = props
|
||||
let dragSourceElement: Element | null = null
|
||||
let isPastedFromProseMirror = false
|
||||
let isDroppedFromProseMirror = false
|
||||
let pasteEvent = typeof ClipboardEvent !== 'undefined' ? new ClipboardEvent('paste') : null
|
||||
let dropEvent: DragEvent | null
|
||||
|
||||
try {
|
||||
dropEvent = typeof DragEvent !== 'undefined' ? new DragEvent('drop') : null
|
||||
} catch {
|
||||
dropEvent = null
|
||||
}
|
||||
|
||||
const processEvent = ({
|
||||
state,
|
||||
from,
|
||||
to,
|
||||
rule,
|
||||
pasteEvt,
|
||||
}: {
|
||||
state: EditorState
|
||||
from: number
|
||||
to: { b: number }
|
||||
rule: PasteRule
|
||||
pasteEvt: ClipboardEvent | null
|
||||
}) => {
|
||||
const tr = state.tr
|
||||
const chainableState = createChainableState({
|
||||
state,
|
||||
transaction: tr,
|
||||
})
|
||||
|
||||
const handler = run({
|
||||
editor,
|
||||
state: chainableState,
|
||||
from: Math.max(from - 1, 0),
|
||||
to: to.b - 1,
|
||||
rule,
|
||||
pasteEvent: pasteEvt,
|
||||
dropEvent,
|
||||
})
|
||||
|
||||
if (!handler || !tr.steps.length) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
dropEvent = typeof DragEvent !== 'undefined' ? new DragEvent('drop') : null
|
||||
} catch {
|
||||
dropEvent = null
|
||||
}
|
||||
pasteEvent = typeof ClipboardEvent !== 'undefined' ? new ClipboardEvent('paste') : null
|
||||
|
||||
return tr
|
||||
}
|
||||
|
||||
const plugins = rules.map(rule => {
|
||||
return new Plugin({
|
||||
// we register a global drag handler to track the current drag source element
|
||||
view(view) {
|
||||
const handleDragstart = (event: DragEvent) => {
|
||||
dragSourceElement = view.dom.parentElement?.contains(event.target as Element) ? view.dom.parentElement : null
|
||||
|
||||
if (dragSourceElement) {
|
||||
tiptapDragFromOtherEditor = editor
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragend = () => {
|
||||
if (tiptapDragFromOtherEditor) {
|
||||
tiptapDragFromOtherEditor = null
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('dragstart', handleDragstart)
|
||||
window.addEventListener('dragend', handleDragend)
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
window.removeEventListener('dragstart', handleDragstart)
|
||||
window.removeEventListener('dragend', handleDragend)
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
props: {
|
||||
handleDOMEvents: {
|
||||
drop: (view, event: Event) => {
|
||||
isDroppedFromProseMirror = dragSourceElement === view.dom.parentElement
|
||||
dropEvent = event as DragEvent
|
||||
|
||||
if (!isDroppedFromProseMirror) {
|
||||
const dragFromOtherEditor = tiptapDragFromOtherEditor
|
||||
|
||||
if (dragFromOtherEditor?.isEditable) {
|
||||
// setTimeout to avoid the wrong content after drop, timeout arg can't be empty or 0
|
||||
setTimeout(() => {
|
||||
const selection = dragFromOtherEditor.state.selection
|
||||
|
||||
if (selection) {
|
||||
dragFromOtherEditor.commands.deleteRange({ from: selection.from, to: selection.to })
|
||||
}
|
||||
}, 10)
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
|
||||
paste: (_view, event: Event) => {
|
||||
const html = (event as ClipboardEvent).clipboardData?.getData('text/html')
|
||||
|
||||
pasteEvent = event as ClipboardEvent
|
||||
|
||||
isPastedFromProseMirror = !!html?.includes('data-pm-slice')
|
||||
|
||||
return false
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
appendTransaction: (transactions, oldState, state) => {
|
||||
const transaction = transactions[0]
|
||||
const isPaste = transaction.getMeta('uiEvent') === 'paste' && !isPastedFromProseMirror
|
||||
const isDrop = transaction.getMeta('uiEvent') === 'drop' && !isDroppedFromProseMirror
|
||||
|
||||
// if PasteRule is triggered by insertContent()
|
||||
const simulatedPasteMeta = transaction.getMeta('applyPasteRules') as
|
||||
| undefined
|
||||
| { from: number; text: string | ProseMirrorNode | Fragment }
|
||||
const isSimulatedPaste = !!simulatedPasteMeta
|
||||
|
||||
if (!isPaste && !isDrop && !isSimulatedPaste) {
|
||||
return
|
||||
}
|
||||
|
||||
// Handle simulated paste
|
||||
if (isSimulatedPaste) {
|
||||
let { text } = simulatedPasteMeta
|
||||
|
||||
if (typeof text === 'string') {
|
||||
text = text as string
|
||||
} else {
|
||||
text = getHTMLFromFragment(Fragment.from(text), state.schema)
|
||||
}
|
||||
|
||||
const { from } = simulatedPasteMeta
|
||||
const to = from + text.length
|
||||
|
||||
const pasteEvt = createClipboardPasteEvent(text)
|
||||
|
||||
return processEvent({
|
||||
rule,
|
||||
state,
|
||||
from,
|
||||
to: { b: to },
|
||||
pasteEvt,
|
||||
})
|
||||
}
|
||||
|
||||
// handle actual paste/drop
|
||||
const from = oldState.doc.content.findDiffStart(state.doc.content)
|
||||
const to = oldState.doc.content.findDiffEnd(state.doc.content)
|
||||
|
||||
// stop if there is no changed range
|
||||
if (!isNumber(from) || !to || from === to.b) {
|
||||
return
|
||||
}
|
||||
|
||||
return processEvent({
|
||||
rule,
|
||||
state,
|
||||
from,
|
||||
to,
|
||||
pasteEvt: pasteEvent,
|
||||
})
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
return plugins
|
||||
}
|
||||
36
node_modules/@tiptap/core/src/Tracker.ts
generated
vendored
Normal file
36
node_modules/@tiptap/core/src/Tracker.ts
generated
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { Transaction } from '@tiptap/pm/state'
|
||||
|
||||
export interface TrackerResult {
|
||||
position: number
|
||||
deleted: boolean
|
||||
}
|
||||
|
||||
export class Tracker {
|
||||
transaction: Transaction
|
||||
|
||||
currentStep: number
|
||||
|
||||
constructor(transaction: Transaction) {
|
||||
this.transaction = transaction
|
||||
this.currentStep = this.transaction.steps.length
|
||||
}
|
||||
|
||||
map(position: number): TrackerResult {
|
||||
let deleted = false
|
||||
|
||||
const mappedPosition = this.transaction.steps.slice(this.currentStep).reduce((newPosition, step) => {
|
||||
const mapResult = step.getMap().mapResult(newPosition)
|
||||
|
||||
if (mapResult.deleted) {
|
||||
deleted = true
|
||||
}
|
||||
|
||||
return mapResult.pos
|
||||
}, position)
|
||||
|
||||
return {
|
||||
position: mappedPosition,
|
||||
deleted,
|
||||
}
|
||||
}
|
||||
}
|
||||
575
node_modules/@tiptap/core/src/__tests__/transformPastedHTML.test.ts
generated
vendored
Normal file
575
node_modules/@tiptap/core/src/__tests__/transformPastedHTML.test.ts
generated
vendored
Normal file
@@ -0,0 +1,575 @@
|
||||
import { Editor, Extension } from '@tiptap/core'
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Text from '@tiptap/extension-text'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
describe('transformPastedHTML', () => {
|
||||
describe('priority ordering', () => {
|
||||
it('should execute transforms in priority order (higher priority first)', () => {
|
||||
const executionOrder: number[] = []
|
||||
|
||||
const editor = new Editor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Extension.create({
|
||||
name: 'low-priority',
|
||||
priority: 50,
|
||||
transformPastedHTML(html) {
|
||||
executionOrder.push(3)
|
||||
return html
|
||||
},
|
||||
}),
|
||||
Extension.create({
|
||||
name: 'high-priority',
|
||||
priority: 200,
|
||||
transformPastedHTML(html) {
|
||||
executionOrder.push(1)
|
||||
return html
|
||||
},
|
||||
}),
|
||||
Extension.create({
|
||||
name: 'medium-priority',
|
||||
priority: 100,
|
||||
transformPastedHTML(html) {
|
||||
executionOrder.push(2)
|
||||
return html
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
editor.view.props.transformPastedHTML?.('<p>test</p>')
|
||||
|
||||
expect(executionOrder).toEqual([1, 2, 3])
|
||||
|
||||
editor.destroy()
|
||||
})
|
||||
|
||||
it('should execute transforms in default priority order when priorities are equal', () => {
|
||||
const executionOrder: string[] = []
|
||||
|
||||
const editor = new Editor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Extension.create({
|
||||
name: 'first',
|
||||
transformPastedHTML(html) {
|
||||
executionOrder.push('first')
|
||||
return html
|
||||
},
|
||||
}),
|
||||
Extension.create({
|
||||
name: 'second',
|
||||
transformPastedHTML(html) {
|
||||
executionOrder.push('second')
|
||||
return html
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
editor.view.props.transformPastedHTML?.('<p>test</p>')
|
||||
|
||||
expect(executionOrder).toEqual(['first', 'second'])
|
||||
|
||||
editor.destroy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('transform chaining', () => {
|
||||
it('should chain transforms correctly', () => {
|
||||
const editor = new Editor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Extension.create({
|
||||
name: 'first-transform',
|
||||
priority: 100,
|
||||
transformPastedHTML(html) {
|
||||
return html.replace(/foo/g, 'bar')
|
||||
},
|
||||
}),
|
||||
Extension.create({
|
||||
name: 'second-transform',
|
||||
priority: 90,
|
||||
transformPastedHTML(html) {
|
||||
return html.replace(/bar/g, 'baz')
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
const result = editor.view.props.transformPastedHTML?.('<p>foo</p>')
|
||||
|
||||
expect(result).toBe('<p>baz</p>')
|
||||
|
||||
editor.destroy()
|
||||
})
|
||||
|
||||
it('should pass transformed HTML through entire chain', () => {
|
||||
const editor = new Editor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Extension.create({
|
||||
name: 'add-prefix',
|
||||
priority: 100,
|
||||
transformPastedHTML(html) {
|
||||
return `PREFIX-${html}`
|
||||
},
|
||||
}),
|
||||
Extension.create({
|
||||
name: 'add-suffix',
|
||||
priority: 90,
|
||||
transformPastedHTML(html) {
|
||||
return `${html}-SUFFIX`
|
||||
},
|
||||
}),
|
||||
Extension.create({
|
||||
name: 'add-wrapper',
|
||||
priority: 80,
|
||||
transformPastedHTML(html) {
|
||||
return `[${html}]`
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
const result = editor.view.props.transformPastedHTML?.('TEST')
|
||||
|
||||
expect(result).toBe('[PREFIX-TEST-SUFFIX]')
|
||||
|
||||
editor.destroy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('baseTransform integration', () => {
|
||||
it('should run baseTransform before extension transforms', () => {
|
||||
const editor = new Editor({
|
||||
editorProps: {
|
||||
transformPastedHTML(html) {
|
||||
return html.replace(/original/g, 'base')
|
||||
},
|
||||
},
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Extension.create({
|
||||
name: 'extension-transform',
|
||||
transformPastedHTML(html) {
|
||||
return html.replace(/base/g, 'final')
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
const result = editor.view.props.transformPastedHTML?.('<p>original</p>')
|
||||
|
||||
expect(result).toBe('<p>final</p>')
|
||||
|
||||
editor.destroy()
|
||||
})
|
||||
|
||||
it('should work when baseTransform is undefined', () => {
|
||||
const editor = new Editor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Extension.create({
|
||||
name: 'extension-transform',
|
||||
transformPastedHTML(html) {
|
||||
return html.replace(/test/g, 'success')
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
const result = editor.view.props.transformPastedHTML?.('<p>test</p>')
|
||||
|
||||
expect(result).toBe('<p>success</p>')
|
||||
|
||||
editor.destroy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('extensions without transforms', () => {
|
||||
it('should skip extensions without transformPastedHTML', () => {
|
||||
const editor = new Editor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Extension.create({
|
||||
name: 'no-transform',
|
||||
// No transformPastedHTML defined
|
||||
}),
|
||||
Extension.create({
|
||||
name: 'with-transform',
|
||||
transformPastedHTML(html) {
|
||||
return html.replace(/test/g, 'success')
|
||||
},
|
||||
}),
|
||||
Extension.create({
|
||||
name: 'another-no-transform',
|
||||
// No transformPastedHTML defined
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
const result = editor.view.props.transformPastedHTML?.('<p>test</p>')
|
||||
|
||||
expect(result).toBe('<p>success</p>')
|
||||
|
||||
editor.destroy()
|
||||
})
|
||||
|
||||
it('should return original HTML when no transforms are defined', () => {
|
||||
const editor = new Editor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Extension.create({
|
||||
name: 'extension-1',
|
||||
}),
|
||||
Extension.create({
|
||||
name: 'extension-2',
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
const result = editor.view.props.transformPastedHTML?.('<p>unchanged</p>')
|
||||
|
||||
expect(result).toBe('<p>unchanged</p>')
|
||||
|
||||
editor.destroy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('extension context', () => {
|
||||
it('should provide correct context to transformPastedHTML', () => {
|
||||
let capturedContext: any = null
|
||||
|
||||
const editor = new Editor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Extension.create({
|
||||
name: 'test-extension',
|
||||
addOptions() {
|
||||
return {
|
||||
customOption: 'value',
|
||||
}
|
||||
},
|
||||
addStorage() {
|
||||
return {
|
||||
customStorage: 'stored',
|
||||
}
|
||||
},
|
||||
transformPastedHTML(html) {
|
||||
capturedContext = {
|
||||
name: this.name,
|
||||
options: this.options,
|
||||
storage: this.storage,
|
||||
editor: this.editor,
|
||||
}
|
||||
return html
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
editor.view.props.transformPastedHTML?.('<p>test</p>')
|
||||
|
||||
expect(capturedContext).toBeDefined()
|
||||
expect(capturedContext.name).toBe('test-extension')
|
||||
expect(capturedContext.options).toMatchObject({ customOption: 'value' })
|
||||
expect(capturedContext.storage).toMatchObject({ customStorage: 'stored' })
|
||||
expect(capturedContext.editor).toBe(editor)
|
||||
|
||||
editor.destroy()
|
||||
})
|
||||
|
||||
it('should allow accessing editor state in transformPastedHTML', () => {
|
||||
const editor = new Editor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Extension.create({
|
||||
name: 'state-aware',
|
||||
transformPastedHTML(html) {
|
||||
const isEmpty = this.editor.isEmpty
|
||||
return isEmpty ? `${html}<!-- empty -->` : html
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
const result = editor.view.props.transformPastedHTML?.('<p>test</p>')
|
||||
|
||||
expect(result).toContain('<!-- empty -->')
|
||||
|
||||
editor.destroy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle empty HTML string', () => {
|
||||
const editor = new Editor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Extension.create({
|
||||
name: 'transform',
|
||||
transformPastedHTML(html) {
|
||||
return html || '<p>default</p>'
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
const result = editor.view.props.transformPastedHTML?.('')
|
||||
|
||||
expect(result).toBe('<p>default</p>')
|
||||
|
||||
editor.destroy()
|
||||
})
|
||||
|
||||
it('should handle HTML with special characters', () => {
|
||||
const editor = new Editor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Extension.create({
|
||||
name: 'preserve-special',
|
||||
transformPastedHTML(html) {
|
||||
return html.replace(/&/g, '&')
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
const result = editor.view.props.transformPastedHTML?.('<p>&test&</p>')
|
||||
|
||||
expect(result).toBe('<p>&test&</p>')
|
||||
|
||||
editor.destroy()
|
||||
})
|
||||
|
||||
it('should handle very long HTML strings', () => {
|
||||
const editor = new Editor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Extension.create({
|
||||
name: 'transform',
|
||||
transformPastedHTML(html) {
|
||||
return html.replace(/test/g, 'success')
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
const longHtml = `<p>${'test '.repeat(10000)}</p>`
|
||||
const result = editor.view.props.transformPastedHTML?.(longHtml)
|
||||
|
||||
expect(result).toContain('success')
|
||||
expect(result).not.toContain('test')
|
||||
|
||||
editor.destroy()
|
||||
})
|
||||
|
||||
it('should handle malformed HTML gracefully', () => {
|
||||
const editor = new Editor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Extension.create({
|
||||
name: 'transform',
|
||||
transformPastedHTML(html) {
|
||||
return html.replace(/test/g, 'success')
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
const malformedHtml = '<p>test</span>'
|
||||
const result = editor.view.props.transformPastedHTML?.(malformedHtml)
|
||||
|
||||
expect(result).toBe('<p>success</span>')
|
||||
|
||||
editor.destroy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('view parameter', () => {
|
||||
it('should pass view parameter to baseTransform', () => {
|
||||
let viewReceived: any = null
|
||||
|
||||
const editor = new Editor({
|
||||
editorProps: {
|
||||
transformPastedHTML(html, view) {
|
||||
viewReceived = view
|
||||
return html
|
||||
},
|
||||
},
|
||||
extensions: [Document, Paragraph, Text],
|
||||
})
|
||||
|
||||
editor.view.props.transformPastedHTML?.('<p>test</p>', editor.view)
|
||||
|
||||
expect(viewReceived).toBe(editor.view)
|
||||
|
||||
editor.destroy()
|
||||
})
|
||||
|
||||
it('should work when view parameter is undefined', () => {
|
||||
const editor = new Editor({
|
||||
editorProps: {
|
||||
transformPastedHTML(html, view) {
|
||||
return view ? html : `${html}<!-- no view -->`
|
||||
},
|
||||
},
|
||||
extensions: [Document, Paragraph, Text],
|
||||
})
|
||||
|
||||
const result = editor.view.props.transformPastedHTML?.('<p>test</p>')
|
||||
|
||||
expect(result).toContain('<!-- no view -->')
|
||||
|
||||
editor.destroy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('real-world scenarios', () => {
|
||||
it('should remove inline styles and dangerous attributes', () => {
|
||||
const editor = new Editor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Extension.create({
|
||||
name: 'security',
|
||||
priority: 100,
|
||||
transformPastedHTML(html) {
|
||||
return html.replace(/\s+style="[^"]*"/gi, '').replace(/\s+on\w+="[^"]*"/gi, '')
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
const result = editor.view.props.transformPastedHTML?.('<p style="color: red;" onclick="alert(\'xss\')">test</p>')
|
||||
|
||||
expect(result).toBe('<p>test</p>')
|
||||
|
||||
editor.destroy()
|
||||
})
|
||||
|
||||
it('should normalize whitespace from word processors', () => {
|
||||
const editor = new Editor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Extension.create({
|
||||
name: 'normalize-whitespace',
|
||||
transformPastedHTML(html) {
|
||||
return html
|
||||
.replace(/\t/g, ' ')
|
||||
.replace(/\u00a0/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
const result = editor.view.props.transformPastedHTML?.('<p>test\t\u00a0 multiple spaces</p>')
|
||||
|
||||
expect(result).toBe('<p>test multiple spaces</p>')
|
||||
|
||||
editor.destroy()
|
||||
})
|
||||
|
||||
it('should chain multiple practical transforms', () => {
|
||||
const editor = new Editor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Extension.create({
|
||||
name: 'remove-styles',
|
||||
priority: 100,
|
||||
transformPastedHTML(html) {
|
||||
return html.replace(/\s+style="[^"]*"/gi, '')
|
||||
},
|
||||
}),
|
||||
Extension.create({
|
||||
name: 'normalize-tags',
|
||||
priority: 90,
|
||||
transformPastedHTML(html) {
|
||||
return html.replace(/<b>/g, '<strong>').replace(/<\/b>/g, '</strong>')
|
||||
},
|
||||
}),
|
||||
Extension.create({
|
||||
name: 'add-classes',
|
||||
priority: 80,
|
||||
transformPastedHTML(html) {
|
||||
return html.replace(/<p>/g, '<p class="editor-paragraph">')
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
const result = editor.view.props.transformPastedHTML?.('<p style="color: red;"><b>test</b></p>')
|
||||
|
||||
expect(result).toBe('<p class="editor-paragraph"><strong>test</strong></p>')
|
||||
|
||||
editor.destroy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('performance', () => {
|
||||
it('should handle many extensions efficiently', () => {
|
||||
const extensions = [Document, Paragraph, Text]
|
||||
|
||||
// Add 50 extensions with transforms
|
||||
for (let i = 0; i < 50; i += 1) {
|
||||
extensions.push(
|
||||
Extension.create({
|
||||
name: `extension-${i}`,
|
||||
priority: 1000 - i,
|
||||
transformPastedHTML(html) {
|
||||
return html // Pass through
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const editor = new Editor({ extensions })
|
||||
|
||||
const start = Date.now()
|
||||
const result = editor.view.props.transformPastedHTML?.('<p>test</p>')
|
||||
const duration = Date.now() - start
|
||||
|
||||
expect(result).toBe('<p>test</p>')
|
||||
expect(duration).toBeLessThan(100) // Should complete quickly
|
||||
|
||||
editor.destroy()
|
||||
})
|
||||
})
|
||||
})
|
||||
29
node_modules/@tiptap/core/src/commands/blur.ts
generated
vendored
Normal file
29
node_modules/@tiptap/core/src/commands/blur.ts
generated
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
blur: {
|
||||
/**
|
||||
* Removes focus from the editor.
|
||||
* @example editor.commands.blur()
|
||||
*/
|
||||
blur: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const blur: RawCommands['blur'] =
|
||||
() =>
|
||||
({ editor, view }) => {
|
||||
requestAnimationFrame(() => {
|
||||
if (!editor.isDestroyed) {
|
||||
;(view.dom as HTMLElement).blur()
|
||||
|
||||
// Browsers should remove the caret on blur but safari does not.
|
||||
// See: https://github.com/ueberdosis/tiptap/issues/2405
|
||||
window?.getSelection()?.removeAllRanges()
|
||||
}
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
25
node_modules/@tiptap/core/src/commands/clearContent.ts
generated
vendored
Normal file
25
node_modules/@tiptap/core/src/commands/clearContent.ts
generated
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
clearContent: {
|
||||
/**
|
||||
* Clear the whole document.
|
||||
* @example editor.commands.clearContent()
|
||||
*/
|
||||
clearContent: (
|
||||
/**
|
||||
* Whether to emit an update event.
|
||||
* @default true
|
||||
*/
|
||||
emitUpdate?: boolean,
|
||||
) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const clearContent: RawCommands['clearContent'] =
|
||||
(emitUpdate = true) =>
|
||||
({ commands }) => {
|
||||
return commands.setContent('', { emitUpdate })
|
||||
}
|
||||
57
node_modules/@tiptap/core/src/commands/clearNodes.ts
generated
vendored
Normal file
57
node_modules/@tiptap/core/src/commands/clearNodes.ts
generated
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
import { liftTarget } from '@tiptap/pm/transform'
|
||||
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
clearNodes: {
|
||||
/**
|
||||
* Normalize nodes to a simple paragraph.
|
||||
* @example editor.commands.clearNodes()
|
||||
*/
|
||||
clearNodes: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const clearNodes: RawCommands['clearNodes'] =
|
||||
() =>
|
||||
({ state, tr, dispatch }) => {
|
||||
const { selection } = tr
|
||||
const { ranges } = selection
|
||||
|
||||
if (!dispatch) {
|
||||
return true
|
||||
}
|
||||
|
||||
ranges.forEach(({ $from, $to }) => {
|
||||
state.doc.nodesBetween($from.pos, $to.pos, (node, pos) => {
|
||||
if (node.type.isText) {
|
||||
return
|
||||
}
|
||||
|
||||
const { doc, mapping } = tr
|
||||
const $mappedFrom = doc.resolve(mapping.map(pos))
|
||||
const $mappedTo = doc.resolve(mapping.map(pos + node.nodeSize))
|
||||
const nodeRange = $mappedFrom.blockRange($mappedTo)
|
||||
|
||||
if (!nodeRange) {
|
||||
return
|
||||
}
|
||||
|
||||
const targetLiftDepth = liftTarget(nodeRange)
|
||||
|
||||
if (node.type.isTextblock) {
|
||||
const { defaultType } = $mappedFrom.parent.contentMatchAt($mappedFrom.index())
|
||||
|
||||
tr.setNodeMarkup(nodeRange.start, defaultType)
|
||||
}
|
||||
|
||||
if (targetLiftDepth || targetLiftDepth === 0) {
|
||||
tr.lift(nodeRange, targetLiftDepth)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
22
node_modules/@tiptap/core/src/commands/command.ts
generated
vendored
Normal file
22
node_modules/@tiptap/core/src/commands/command.ts
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Command, RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
command: {
|
||||
/**
|
||||
* Define a command inline.
|
||||
* @param fn The command function.
|
||||
* @example
|
||||
* editor.commands.command(({ tr, state }) => {
|
||||
* ...
|
||||
* return true
|
||||
* })
|
||||
*/
|
||||
command: (fn: (props: Parameters<Command>[0]) => boolean) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const command: RawCommands['command'] = fn => props => {
|
||||
return fn(props)
|
||||
}
|
||||
21
node_modules/@tiptap/core/src/commands/createParagraphNear.ts
generated
vendored
Normal file
21
node_modules/@tiptap/core/src/commands/createParagraphNear.ts
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
import { createParagraphNear as originalCreateParagraphNear } from '@tiptap/pm/commands'
|
||||
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
createParagraphNear: {
|
||||
/**
|
||||
* Create a paragraph nearby.
|
||||
* @example editor.commands.createParagraphNear()
|
||||
*/
|
||||
createParagraphNear: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const createParagraphNear: RawCommands['createParagraphNear'] =
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
return originalCreateParagraphNear(state, dispatch)
|
||||
}
|
||||
36
node_modules/@tiptap/core/src/commands/cut.ts
generated
vendored
Normal file
36
node_modules/@tiptap/core/src/commands/cut.ts
generated
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
import { TextSelection } from '@tiptap/pm/state'
|
||||
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
cut: {
|
||||
/**
|
||||
* Cuts content from a range and inserts it at a given position.
|
||||
* @param range The range to cut.
|
||||
* @param range.from The start position of the range.
|
||||
* @param range.to The end position of the range.
|
||||
* @param targetPos The position to insert the content at.
|
||||
* @example editor.commands.cut({ from: 1, to: 3 }, 5)
|
||||
*/
|
||||
cut: ({ from, to }: { from: number; to: number }, targetPos: number) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const cut: RawCommands['cut'] =
|
||||
(originRange, targetPos) =>
|
||||
({ editor, tr }) => {
|
||||
const { state } = editor
|
||||
|
||||
const contentSlice = state.doc.slice(originRange.from, originRange.to)
|
||||
|
||||
tr.deleteRange(originRange.from, originRange.to)
|
||||
const newPos = tr.mapping.map(targetPos)
|
||||
|
||||
tr.insert(newPos, contentSlice.content)
|
||||
|
||||
tr.setSelection(new TextSelection(tr.doc.resolve(Math.max(newPos - 1, 0))))
|
||||
|
||||
return true
|
||||
}
|
||||
44
node_modules/@tiptap/core/src/commands/deleteCurrentNode.ts
generated
vendored
Normal file
44
node_modules/@tiptap/core/src/commands/deleteCurrentNode.ts
generated
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
deleteCurrentNode: {
|
||||
/**
|
||||
* Delete the node that currently has the selection anchor.
|
||||
* @example editor.commands.deleteCurrentNode()
|
||||
*/
|
||||
deleteCurrentNode: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteCurrentNode: RawCommands['deleteCurrentNode'] =
|
||||
() =>
|
||||
({ tr, dispatch }) => {
|
||||
const { selection } = tr
|
||||
const currentNode = selection.$anchor.node()
|
||||
|
||||
// if there is content inside the current node, break out of this command
|
||||
if (currentNode.content.size > 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
const $pos = tr.selection.$anchor
|
||||
|
||||
for (let depth = $pos.depth; depth > 0; depth -= 1) {
|
||||
const node = $pos.node(depth)
|
||||
|
||||
if (node.type === currentNode.type) {
|
||||
if (dispatch) {
|
||||
const from = $pos.before(depth)
|
||||
const to = $pos.after(depth)
|
||||
|
||||
tr.delete(from, to).scrollIntoView()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
41
node_modules/@tiptap/core/src/commands/deleteNode.ts
generated
vendored
Normal file
41
node_modules/@tiptap/core/src/commands/deleteNode.ts
generated
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { NodeType } from '@tiptap/pm/model'
|
||||
|
||||
import { getNodeType } from '../helpers/getNodeType.js'
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
deleteNode: {
|
||||
/**
|
||||
* Delete a node with a given type or name.
|
||||
* @param typeOrName The type or name of the node.
|
||||
* @example editor.commands.deleteNode('paragraph')
|
||||
*/
|
||||
deleteNode: (typeOrName: string | NodeType) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteNode: RawCommands['deleteNode'] =
|
||||
typeOrName =>
|
||||
({ tr, state, dispatch }) => {
|
||||
const type = getNodeType(typeOrName, state.schema)
|
||||
const $pos = tr.selection.$anchor
|
||||
|
||||
for (let depth = $pos.depth; depth > 0; depth -= 1) {
|
||||
const node = $pos.node(depth)
|
||||
|
||||
if (node.type === type) {
|
||||
if (dispatch) {
|
||||
const from = $pos.before(depth)
|
||||
const to = $pos.after(depth)
|
||||
|
||||
tr.delete(from, to).scrollIntoView()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
26
node_modules/@tiptap/core/src/commands/deleteRange.ts
generated
vendored
Normal file
26
node_modules/@tiptap/core/src/commands/deleteRange.ts
generated
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { Range, RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
deleteRange: {
|
||||
/**
|
||||
* Delete a given range.
|
||||
* @param range The range to delete.
|
||||
* @example editor.commands.deleteRange({ from: 1, to: 3 })
|
||||
*/
|
||||
deleteRange: (range: Range) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteRange: RawCommands['deleteRange'] =
|
||||
range =>
|
||||
({ tr, dispatch }) => {
|
||||
const { from, to } = range
|
||||
|
||||
if (dispatch) {
|
||||
tr.delete(from, to)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
21
node_modules/@tiptap/core/src/commands/deleteSelection.ts
generated
vendored
Normal file
21
node_modules/@tiptap/core/src/commands/deleteSelection.ts
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
import { deleteSelection as originalDeleteSelection } from '@tiptap/pm/commands'
|
||||
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
deleteSelection: {
|
||||
/**
|
||||
* Delete the selection, if there is one.
|
||||
* @example editor.commands.deleteSelection()
|
||||
*/
|
||||
deleteSelection: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteSelection: RawCommands['deleteSelection'] =
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
return originalDeleteSelection(state, dispatch)
|
||||
}
|
||||
19
node_modules/@tiptap/core/src/commands/enter.ts
generated
vendored
Normal file
19
node_modules/@tiptap/core/src/commands/enter.ts
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
enter: {
|
||||
/**
|
||||
* Trigger enter.
|
||||
* @example editor.commands.enter()
|
||||
*/
|
||||
enter: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const enter: RawCommands['enter'] =
|
||||
() =>
|
||||
({ commands }) => {
|
||||
return commands.keyboardShortcut('Enter')
|
||||
}
|
||||
21
node_modules/@tiptap/core/src/commands/exitCode.ts
generated
vendored
Normal file
21
node_modules/@tiptap/core/src/commands/exitCode.ts
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
import { exitCode as originalExitCode } from '@tiptap/pm/commands'
|
||||
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
exitCode: {
|
||||
/**
|
||||
* Exit from a code block.
|
||||
* @example editor.commands.exitCode()
|
||||
*/
|
||||
exitCode: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const exitCode: RawCommands['exitCode'] =
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
return originalExitCode(state, dispatch)
|
||||
}
|
||||
51
node_modules/@tiptap/core/src/commands/extendMarkRange.ts
generated
vendored
Normal file
51
node_modules/@tiptap/core/src/commands/extendMarkRange.ts
generated
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { MarkType } from '@tiptap/pm/model'
|
||||
import { TextSelection } from '@tiptap/pm/state'
|
||||
|
||||
import { getMarkRange } from '../helpers/getMarkRange.js'
|
||||
import { getMarkType } from '../helpers/getMarkType.js'
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
extendMarkRange: {
|
||||
/**
|
||||
* Extends the text selection to the current mark by type or name.
|
||||
* @param typeOrName The type or name of the mark.
|
||||
* @param attributes The attributes of the mark.
|
||||
* @example editor.commands.extendMarkRange('bold')
|
||||
* @example editor.commands.extendMarkRange('mention', { userId: "1" })
|
||||
*/
|
||||
extendMarkRange: (
|
||||
/**
|
||||
* The type or name of the mark.
|
||||
*/
|
||||
typeOrName: string | MarkType,
|
||||
|
||||
/**
|
||||
* The attributes of the mark.
|
||||
*/
|
||||
attributes?: Record<string, any>,
|
||||
) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const extendMarkRange: RawCommands['extendMarkRange'] =
|
||||
(typeOrName, attributes = {}) =>
|
||||
({ tr, state, dispatch }) => {
|
||||
const type = getMarkType(typeOrName, state.schema)
|
||||
const { doc, selection } = tr
|
||||
const { $from, from, to } = selection
|
||||
|
||||
if (dispatch) {
|
||||
const range = getMarkRange($from, type, attributes)
|
||||
|
||||
if (range && range.from <= from && range.to >= to) {
|
||||
const newSelection = TextSelection.create(doc, range.from, range.to)
|
||||
|
||||
tr.setSelection(newSelection)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
26
node_modules/@tiptap/core/src/commands/first.ts
generated
vendored
Normal file
26
node_modules/@tiptap/core/src/commands/first.ts
generated
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { Command, CommandProps, RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
first: {
|
||||
/**
|
||||
* Runs one command after the other and stops at the first which returns true.
|
||||
* @param commands The commands to run.
|
||||
* @example editor.commands.first([command1, command2])
|
||||
*/
|
||||
first: (commands: Command[] | ((props: CommandProps) => Command[])) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const first: RawCommands['first'] = commands => props => {
|
||||
const items = typeof commands === 'function' ? commands(props) : commands
|
||||
|
||||
for (let i = 0; i < items.length; i += 1) {
|
||||
if (items[i](props)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
108
node_modules/@tiptap/core/src/commands/focus.ts
generated
vendored
Normal file
108
node_modules/@tiptap/core/src/commands/focus.ts
generated
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
import { isTextSelection } from '../helpers/isTextSelection.js'
|
||||
import { resolveFocusPosition } from '../helpers/resolveFocusPosition.js'
|
||||
import type { FocusPosition, RawCommands } from '../types.js'
|
||||
import { isAndroid } from '../utilities/isAndroid.js'
|
||||
import { isiOS } from '../utilities/isiOS.js'
|
||||
import { isSafari } from '../utilities/isSafari.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
focus: {
|
||||
/**
|
||||
* Focus the editor at the given position.
|
||||
* @param position The position to focus at.
|
||||
* @param options.scrollIntoView Scroll the focused position into view after focusing
|
||||
* @example editor.commands.focus()
|
||||
* @example editor.commands.focus(32, { scrollIntoView: false })
|
||||
*/
|
||||
focus: (
|
||||
/**
|
||||
* The position to focus at.
|
||||
*/
|
||||
position?: FocusPosition,
|
||||
|
||||
/**
|
||||
* Optional options
|
||||
* @default { scrollIntoView: true }
|
||||
*/
|
||||
options?: {
|
||||
scrollIntoView?: boolean
|
||||
},
|
||||
) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const focus: RawCommands['focus'] =
|
||||
(position = null, options = {}) =>
|
||||
({ editor, view, tr, dispatch }) => {
|
||||
options = {
|
||||
scrollIntoView: true,
|
||||
...options,
|
||||
}
|
||||
|
||||
const delayedFocus = () => {
|
||||
// focus within `requestAnimationFrame` breaks focus on iOS and Android
|
||||
// so we have to call this
|
||||
if (isiOS() || isAndroid()) {
|
||||
;(view.dom as HTMLElement).focus()
|
||||
}
|
||||
|
||||
// Safari requires preventScroll to avoid the browser scrolling to the
|
||||
// top of the editor when focus is called before the selection is set.
|
||||
// We exclude iOS and Android since they are already handled above.
|
||||
// see: https://github.com/ueberdosis/tiptap/issues/7318
|
||||
if (isSafari() && !isiOS() && !isAndroid()) {
|
||||
;(view.dom as HTMLElement).focus({ preventScroll: true })
|
||||
}
|
||||
|
||||
// For React we have to focus asynchronously. Otherwise wild things happen.
|
||||
// see: https://github.com/ueberdosis/tiptap/issues/1520
|
||||
requestAnimationFrame(() => {
|
||||
if (!editor.isDestroyed) {
|
||||
view.focus()
|
||||
|
||||
if (options?.scrollIntoView) {
|
||||
editor.commands.scrollIntoView()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
if ((view.hasFocus() && position === null) || position === false) {
|
||||
return true
|
||||
}
|
||||
} catch {
|
||||
// if view.hasFocus fails (view not mounted yet)
|
||||
// we will return false because there's nothing to focus
|
||||
return false
|
||||
}
|
||||
|
||||
// we don’t try to resolve a NodeSelection or CellSelection
|
||||
if (dispatch && position === null && !isTextSelection(editor.state.selection)) {
|
||||
delayedFocus()
|
||||
return true
|
||||
}
|
||||
|
||||
// pass through tr.doc instead of editor.state.doc
|
||||
// since transactions could change the editors state before this command has been run
|
||||
const selection = resolveFocusPosition(tr.doc, position) || editor.state.selection
|
||||
const isSameSelection = editor.state.selection.eq(selection)
|
||||
|
||||
if (dispatch) {
|
||||
if (!isSameSelection) {
|
||||
tr.setSelection(selection)
|
||||
}
|
||||
|
||||
// `tr.setSelection` resets the stored marks
|
||||
// so we’ll restore them if the selection is the same as before
|
||||
if (isSameSelection && tr.storedMarks) {
|
||||
tr.setStoredMarks(tr.storedMarks)
|
||||
}
|
||||
|
||||
delayedFocus()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
24
node_modules/@tiptap/core/src/commands/forEach.ts
generated
vendored
Normal file
24
node_modules/@tiptap/core/src/commands/forEach.ts
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { CommandProps, RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
forEach: {
|
||||
/**
|
||||
* Loop through an array of items.
|
||||
*/
|
||||
forEach: <T>(
|
||||
items: T[],
|
||||
fn: (
|
||||
item: T,
|
||||
props: CommandProps & {
|
||||
index: number
|
||||
},
|
||||
) => boolean,
|
||||
) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const forEach: RawCommands['forEach'] = (items, fn) => props => {
|
||||
return items.every((item, index) => fn(item, { ...props, index }))
|
||||
}
|
||||
57
node_modules/@tiptap/core/src/commands/index.ts
generated
vendored
Normal file
57
node_modules/@tiptap/core/src/commands/index.ts
generated
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
export * from './blur.js'
|
||||
export * from './clearContent.js'
|
||||
export * from './clearNodes.js'
|
||||
export * from './command.js'
|
||||
export * from './createParagraphNear.js'
|
||||
export * from './cut.js'
|
||||
export * from './deleteCurrentNode.js'
|
||||
export * from './deleteNode.js'
|
||||
export * from './deleteRange.js'
|
||||
export * from './deleteSelection.js'
|
||||
export * from './enter.js'
|
||||
export * from './exitCode.js'
|
||||
export * from './extendMarkRange.js'
|
||||
export * from './first.js'
|
||||
export * from './focus.js'
|
||||
export * from './forEach.js'
|
||||
export * from './insertContent.js'
|
||||
export * from './insertContentAt.js'
|
||||
export * from './join.js'
|
||||
export * from './joinItemBackward.js'
|
||||
export * from './joinItemForward.js'
|
||||
export * from './joinTextblockBackward.js'
|
||||
export * from './joinTextblockForward.js'
|
||||
export * from './keyboardShortcut.js'
|
||||
export * from './lift.js'
|
||||
export * from './liftEmptyBlock.js'
|
||||
export * from './liftListItem.js'
|
||||
export * from './newlineInCode.js'
|
||||
export * from './resetAttributes.js'
|
||||
export * from './scrollIntoView.js'
|
||||
export * from './selectAll.js'
|
||||
export * from './selectNodeBackward.js'
|
||||
export * from './selectNodeForward.js'
|
||||
export * from './selectParentNode.js'
|
||||
export * from './selectTextblockEnd.js'
|
||||
export * from './selectTextblockStart.js'
|
||||
export * from './setContent.js'
|
||||
export * from './setMark.js'
|
||||
export * from './setMeta.js'
|
||||
export * from './setNode.js'
|
||||
export * from './setNodeSelection.js'
|
||||
export * from './setTextDirection.js'
|
||||
export * from './setTextSelection.js'
|
||||
export * from './sinkListItem.js'
|
||||
export * from './splitBlock.js'
|
||||
export * from './splitListItem.js'
|
||||
export * from './toggleList.js'
|
||||
export * from './toggleMark.js'
|
||||
export * from './toggleNode.js'
|
||||
export * from './toggleWrap.js'
|
||||
export * from './undoInputRule.js'
|
||||
export * from './unsetAllMarks.js'
|
||||
export * from './unsetMark.js'
|
||||
export * from './unsetTextDirection.js'
|
||||
export * from './updateAttributes.js'
|
||||
export * from './wrapIn.js'
|
||||
export * from './wrapInList.js'
|
||||
46
node_modules/@tiptap/core/src/commands/insertContent.ts
generated
vendored
Normal file
46
node_modules/@tiptap/core/src/commands/insertContent.ts
generated
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { Fragment, Node as ProseMirrorNode, ParseOptions } from '@tiptap/pm/model'
|
||||
|
||||
import type { Content, RawCommands } from '../types.js'
|
||||
|
||||
export interface InsertContentOptions {
|
||||
/**
|
||||
* Options for parsing the content.
|
||||
*/
|
||||
parseOptions?: ParseOptions
|
||||
|
||||
/**
|
||||
* Whether to update the selection after inserting the content.
|
||||
*/
|
||||
updateSelection?: boolean
|
||||
applyInputRules?: boolean
|
||||
applyPasteRules?: boolean
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
insertContent: {
|
||||
/**
|
||||
* Insert a node or string of HTML at the current position.
|
||||
* @example editor.commands.insertContent('<h1>Example</h1>')
|
||||
* @example editor.commands.insertContent('<h1>Example</h1>', { updateSelection: false })
|
||||
*/
|
||||
insertContent: (
|
||||
/**
|
||||
* The ProseMirror content to insert.
|
||||
*/
|
||||
value: Content | ProseMirrorNode | Fragment,
|
||||
|
||||
/**
|
||||
* Optional options
|
||||
*/
|
||||
options?: InsertContentOptions,
|
||||
) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const insertContent: RawCommands['insertContent'] =
|
||||
(value, options) =>
|
||||
({ tr, commands }) => {
|
||||
return commands.insertContentAt({ from: tr.selection.from, to: tr.selection.to }, value, options)
|
||||
}
|
||||
212
node_modules/@tiptap/core/src/commands/insertContentAt.ts
generated
vendored
Normal file
212
node_modules/@tiptap/core/src/commands/insertContentAt.ts
generated
vendored
Normal file
@@ -0,0 +1,212 @@
|
||||
import type { Node as ProseMirrorNode, ParseOptions } from '@tiptap/pm/model'
|
||||
import { Fragment } from '@tiptap/pm/model'
|
||||
|
||||
import { createNodeFromContent } from '../helpers/createNodeFromContent.js'
|
||||
import { selectionToInsertionEnd } from '../helpers/selectionToInsertionEnd.js'
|
||||
import type { Content, Range, RawCommands } from '../types.js'
|
||||
|
||||
export interface InsertContentAtOptions {
|
||||
/**
|
||||
* Options for parsing the content.
|
||||
*/
|
||||
parseOptions?: ParseOptions
|
||||
|
||||
/**
|
||||
* Whether to update the selection after inserting the content.
|
||||
*/
|
||||
updateSelection?: boolean
|
||||
|
||||
/**
|
||||
* Whether to apply input rules after inserting the content.
|
||||
*/
|
||||
applyInputRules?: boolean
|
||||
|
||||
/**
|
||||
* Whether to apply paste rules after inserting the content.
|
||||
*/
|
||||
applyPasteRules?: boolean
|
||||
|
||||
/**
|
||||
* Whether to throw an error if the content is invalid.
|
||||
*/
|
||||
errorOnInvalidContent?: boolean
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
insertContentAt: {
|
||||
/**
|
||||
* Insert a node or string of HTML at a specific position.
|
||||
* @example editor.commands.insertContentAt(0, '<h1>Example</h1>')
|
||||
*/
|
||||
insertContentAt: (
|
||||
/**
|
||||
* The position to insert the content at.
|
||||
*/
|
||||
position: number | Range,
|
||||
|
||||
/**
|
||||
* The ProseMirror content to insert.
|
||||
*/
|
||||
value: Content | ProseMirrorNode | Fragment,
|
||||
|
||||
/**
|
||||
* Optional options
|
||||
*/
|
||||
options?: InsertContentAtOptions,
|
||||
) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isFragment = (nodeOrFragment: ProseMirrorNode | Fragment): nodeOrFragment is Fragment => {
|
||||
return !('type' in nodeOrFragment)
|
||||
}
|
||||
|
||||
export const insertContentAt: RawCommands['insertContentAt'] =
|
||||
(position, value, options) =>
|
||||
({ tr, dispatch, editor }) => {
|
||||
if (dispatch) {
|
||||
options = {
|
||||
parseOptions: editor.options.parseOptions,
|
||||
updateSelection: true,
|
||||
applyInputRules: false,
|
||||
applyPasteRules: false,
|
||||
...options,
|
||||
}
|
||||
|
||||
let content: Fragment | ProseMirrorNode
|
||||
|
||||
const emitContentError = (error: Error) => {
|
||||
editor.emit('contentError', {
|
||||
editor,
|
||||
error,
|
||||
disableCollaboration: () => {
|
||||
if (
|
||||
'collaboration' in editor.storage &&
|
||||
typeof editor.storage.collaboration === 'object' &&
|
||||
editor.storage.collaboration
|
||||
) {
|
||||
;(editor.storage.collaboration as any).isDisabled = true
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const parseOptions: ParseOptions = {
|
||||
preserveWhitespace: 'full',
|
||||
...options.parseOptions,
|
||||
}
|
||||
|
||||
// If `emitContentError` is enabled, we want to check the content for errors
|
||||
// but ignore them (do not remove the invalid content from the document)
|
||||
if (!options.errorOnInvalidContent && !editor.options.enableContentCheck && editor.options.emitContentError) {
|
||||
try {
|
||||
createNodeFromContent(value, editor.schema, {
|
||||
parseOptions,
|
||||
errorOnInvalidContent: true,
|
||||
})
|
||||
} catch (e) {
|
||||
emitContentError(e as Error)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
content = createNodeFromContent(value, editor.schema, {
|
||||
parseOptions,
|
||||
errorOnInvalidContent: options.errorOnInvalidContent ?? editor.options.enableContentCheck,
|
||||
})
|
||||
} catch (e) {
|
||||
emitContentError(e as Error)
|
||||
return false
|
||||
}
|
||||
|
||||
let { from, to } =
|
||||
typeof position === 'number' ? { from: position, to: position } : { from: position.from, to: position.to }
|
||||
|
||||
let isOnlyTextContent = true
|
||||
let isOnlyBlockContent = true
|
||||
const nodes = isFragment(content) ? content : [content]
|
||||
|
||||
nodes.forEach(node => {
|
||||
// check if added node is valid
|
||||
node.check()
|
||||
|
||||
isOnlyTextContent = isOnlyTextContent ? node.isText && node.marks.length === 0 : false
|
||||
|
||||
isOnlyBlockContent = isOnlyBlockContent ? node.isBlock : false
|
||||
})
|
||||
|
||||
// check if we can replace the wrapping node by
|
||||
// the newly inserted content
|
||||
// example:
|
||||
// replace an empty paragraph by an inserted image
|
||||
// instead of inserting the image below the paragraph
|
||||
if (from === to && isOnlyBlockContent) {
|
||||
const { parent } = tr.doc.resolve(from)
|
||||
const isEmptyTextBlock = parent.isTextblock && !parent.type.spec.code && !parent.childCount
|
||||
|
||||
if (isEmptyTextBlock) {
|
||||
from -= 1
|
||||
to += 1
|
||||
}
|
||||
}
|
||||
|
||||
let newContent
|
||||
|
||||
// if there is only plain text we have to use `insertText`
|
||||
// because this will keep the current marks
|
||||
if (isOnlyTextContent) {
|
||||
// if value is string, we can use it directly
|
||||
// otherwise if it is an array, we have to join it
|
||||
if (Array.isArray(value)) {
|
||||
newContent = value.map(v => v.text || '').join('')
|
||||
} else if (value instanceof Fragment) {
|
||||
let text = ''
|
||||
|
||||
value.forEach(node => {
|
||||
if (node.text) {
|
||||
text += node.text
|
||||
}
|
||||
})
|
||||
|
||||
newContent = text
|
||||
} else if (typeof value === 'object' && !!value && !!value.text) {
|
||||
newContent = value.text
|
||||
} else {
|
||||
newContent = value as string
|
||||
}
|
||||
|
||||
tr.insertText(newContent, from, to)
|
||||
} else {
|
||||
newContent = content
|
||||
|
||||
const $from = tr.doc.resolve(from)
|
||||
const $fromNode = $from.node()
|
||||
const fromSelectionAtStart = $from.parentOffset === 0
|
||||
const isTextSelection = $fromNode.isText || $fromNode.isTextblock
|
||||
const hasContent = $fromNode.content.size > 0
|
||||
|
||||
if (fromSelectionAtStart && isTextSelection && hasContent) {
|
||||
from = Math.max(0, from - 1)
|
||||
}
|
||||
|
||||
tr.replaceWith(from, to, newContent)
|
||||
}
|
||||
|
||||
// set cursor at end of inserted content
|
||||
if (options.updateSelection) {
|
||||
selectionToInsertionEnd(tr, tr.steps.length - 1, -1)
|
||||
}
|
||||
|
||||
if (options.applyInputRules) {
|
||||
tr.setMeta('applyInputRules', { from, text: newContent })
|
||||
}
|
||||
|
||||
if (options.applyPasteRules) {
|
||||
tr.setMeta('applyPasteRules', { from, text: newContent })
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
69
node_modules/@tiptap/core/src/commands/join.ts
generated
vendored
Normal file
69
node_modules/@tiptap/core/src/commands/join.ts
generated
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
joinBackward as originalJoinBackward,
|
||||
joinDown as originalJoinDown,
|
||||
joinForward as originalJoinForward,
|
||||
joinUp as originalJoinUp,
|
||||
} from '@tiptap/pm/commands'
|
||||
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
joinUp: {
|
||||
/**
|
||||
* Join the selected block or, if there is a text selection, the closest ancestor block of the selection that can be joined, with the sibling above it.
|
||||
* @example editor.commands.joinUp()
|
||||
*/
|
||||
joinUp: () => ReturnType
|
||||
}
|
||||
joinDown: {
|
||||
/**
|
||||
* Join the selected block, or the closest ancestor of the selection that can be joined, with the sibling after it.
|
||||
* @example editor.commands.joinDown()
|
||||
*/
|
||||
joinDown: () => ReturnType
|
||||
}
|
||||
joinBackward: {
|
||||
/**
|
||||
* If the selection is empty and at the start of a textblock, try to reduce the distance between that block and the one before it—if there's a block directly before it that can be joined, join them.
|
||||
* If not, try to move the selected block closer to the next one in the document structure by lifting it out of its
|
||||
* parent or moving it into a parent of the previous block. Will use the view for accurate (bidi-aware) start-of-textblock detection if given.
|
||||
* @example editor.commands.joinBackward()
|
||||
*/
|
||||
joinBackward: () => ReturnType
|
||||
}
|
||||
joinForward: {
|
||||
/**
|
||||
* If the selection is empty and the cursor is at the end of a textblock, try to reduce or remove the boundary between that block and the one after it,
|
||||
* either by joining them or by moving the other block closer to this one in the tree structure.
|
||||
* Will use the view for accurate start-of-textblock detection if given.
|
||||
* @example editor.commands.joinForward()
|
||||
*/
|
||||
joinForward: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const joinUp: RawCommands['joinUp'] =
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
return originalJoinUp(state, dispatch)
|
||||
}
|
||||
|
||||
export const joinDown: RawCommands['joinDown'] =
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
return originalJoinDown(state, dispatch)
|
||||
}
|
||||
|
||||
export const joinBackward: RawCommands['joinBackward'] =
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
return originalJoinBackward(state, dispatch)
|
||||
}
|
||||
|
||||
export const joinForward: RawCommands['joinForward'] =
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
return originalJoinForward(state, dispatch)
|
||||
}
|
||||
37
node_modules/@tiptap/core/src/commands/joinItemBackward.ts
generated
vendored
Normal file
37
node_modules/@tiptap/core/src/commands/joinItemBackward.ts
generated
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
import { joinPoint } from '@tiptap/pm/transform'
|
||||
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
joinItemBackward: {
|
||||
/**
|
||||
* Join two items backward.
|
||||
* @example editor.commands.joinItemBackward()
|
||||
*/
|
||||
joinItemBackward: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const joinItemBackward: RawCommands['joinItemBackward'] =
|
||||
() =>
|
||||
({ state, dispatch, tr }) => {
|
||||
try {
|
||||
const point = joinPoint(state.doc, state.selection.$from.pos, -1)
|
||||
|
||||
if (point === null || point === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
tr.join(point, 2)
|
||||
|
||||
if (dispatch) {
|
||||
dispatch(tr)
|
||||
}
|
||||
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
37
node_modules/@tiptap/core/src/commands/joinItemForward.ts
generated
vendored
Normal file
37
node_modules/@tiptap/core/src/commands/joinItemForward.ts
generated
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
import { joinPoint } from '@tiptap/pm/transform'
|
||||
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
joinItemForward: {
|
||||
/**
|
||||
* Join two items Forwards.
|
||||
* @example editor.commands.joinItemForward()
|
||||
*/
|
||||
joinItemForward: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const joinItemForward: RawCommands['joinItemForward'] =
|
||||
() =>
|
||||
({ state, dispatch, tr }) => {
|
||||
try {
|
||||
const point = joinPoint(state.doc, state.selection.$from.pos, +1)
|
||||
|
||||
if (point === null || point === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
tr.join(point, 2)
|
||||
|
||||
if (dispatch) {
|
||||
dispatch(tr)
|
||||
}
|
||||
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
20
node_modules/@tiptap/core/src/commands/joinTextblockBackward.ts
generated
vendored
Normal file
20
node_modules/@tiptap/core/src/commands/joinTextblockBackward.ts
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
import { joinTextblockBackward as originalCommand } from '@tiptap/pm/commands'
|
||||
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
joinTextblockBackward: {
|
||||
/**
|
||||
* A more limited form of joinBackward that only tries to join the current textblock to the one before it, if the cursor is at the start of a textblock.
|
||||
*/
|
||||
joinTextblockBackward: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const joinTextblockBackward: RawCommands['joinTextblockBackward'] =
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
return originalCommand(state, dispatch)
|
||||
}
|
||||
20
node_modules/@tiptap/core/src/commands/joinTextblockForward.ts
generated
vendored
Normal file
20
node_modules/@tiptap/core/src/commands/joinTextblockForward.ts
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
import { joinTextblockForward as originalCommand } from '@tiptap/pm/commands'
|
||||
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
joinTextblockForward: {
|
||||
/**
|
||||
* A more limited form of joinForward that only tries to join the current textblock to the one after it, if the cursor is at the end of a textblock.
|
||||
*/
|
||||
joinTextblockForward: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const joinTextblockForward: RawCommands['joinTextblockForward'] =
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
return originalCommand(state, dispatch)
|
||||
}
|
||||
100
node_modules/@tiptap/core/src/commands/keyboardShortcut.ts
generated
vendored
Normal file
100
node_modules/@tiptap/core/src/commands/keyboardShortcut.ts
generated
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
import type { RawCommands } from '../types.js'
|
||||
import { isiOS } from '../utilities/isiOS.js'
|
||||
import { isMacOS } from '../utilities/isMacOS.js'
|
||||
|
||||
function normalizeKeyName(name: string) {
|
||||
const parts = name.split(/-(?!$)/)
|
||||
let result = parts[parts.length - 1]
|
||||
|
||||
if (result === 'Space') {
|
||||
result = ' '
|
||||
}
|
||||
|
||||
let alt
|
||||
let ctrl
|
||||
let shift
|
||||
let meta
|
||||
|
||||
for (let i = 0; i < parts.length - 1; i += 1) {
|
||||
const mod = parts[i]
|
||||
|
||||
if (/^(cmd|meta|m)$/i.test(mod)) {
|
||||
meta = true
|
||||
} else if (/^a(lt)?$/i.test(mod)) {
|
||||
alt = true
|
||||
} else if (/^(c|ctrl|control)$/i.test(mod)) {
|
||||
ctrl = true
|
||||
} else if (/^s(hift)?$/i.test(mod)) {
|
||||
shift = true
|
||||
} else if (/^mod$/i.test(mod)) {
|
||||
if (isiOS() || isMacOS()) {
|
||||
meta = true
|
||||
} else {
|
||||
ctrl = true
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unrecognized modifier name: ${mod}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (alt) {
|
||||
result = `Alt-${result}`
|
||||
}
|
||||
|
||||
if (ctrl) {
|
||||
result = `Ctrl-${result}`
|
||||
}
|
||||
|
||||
if (meta) {
|
||||
result = `Meta-${result}`
|
||||
}
|
||||
|
||||
if (shift) {
|
||||
result = `Shift-${result}`
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
keyboardShortcut: {
|
||||
/**
|
||||
* Trigger a keyboard shortcut.
|
||||
* @param name The name of the keyboard shortcut.
|
||||
* @example editor.commands.keyboardShortcut('Mod-b')
|
||||
*/
|
||||
keyboardShortcut: (name: string) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const keyboardShortcut: RawCommands['keyboardShortcut'] =
|
||||
name =>
|
||||
({ editor, view, tr, dispatch }) => {
|
||||
const keys = normalizeKeyName(name).split(/-(?!$)/)
|
||||
const key = keys.find(item => !['Alt', 'Ctrl', 'Meta', 'Shift'].includes(item))
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: key === 'Space' ? ' ' : key,
|
||||
altKey: keys.includes('Alt'),
|
||||
ctrlKey: keys.includes('Ctrl'),
|
||||
metaKey: keys.includes('Meta'),
|
||||
shiftKey: keys.includes('Shift'),
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
|
||||
const capturedTransaction = editor.captureTransaction(() => {
|
||||
view.someProp('handleKeyDown', f => f(view, event))
|
||||
})
|
||||
|
||||
capturedTransaction?.steps.forEach(step => {
|
||||
const newStep = step.map(tr.mapping)
|
||||
|
||||
if (newStep && dispatch) {
|
||||
tr.maybeStep(newStep)
|
||||
}
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
34
node_modules/@tiptap/core/src/commands/lift.ts
generated
vendored
Normal file
34
node_modules/@tiptap/core/src/commands/lift.ts
generated
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
import { lift as originalLift } from '@tiptap/pm/commands'
|
||||
import type { NodeType } from '@tiptap/pm/model'
|
||||
|
||||
import { getNodeType } from '../helpers/getNodeType.js'
|
||||
import { isNodeActive } from '../helpers/isNodeActive.js'
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
lift: {
|
||||
/**
|
||||
* Removes an existing wrap if possible lifting the node out of it
|
||||
* @param typeOrName The type or name of the node.
|
||||
* @param attributes The attributes of the node.
|
||||
* @example editor.commands.lift('paragraph')
|
||||
* @example editor.commands.lift('heading', { level: 1 })
|
||||
*/
|
||||
lift: (typeOrName: string | NodeType, attributes?: Record<string, any>) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const lift: RawCommands['lift'] =
|
||||
(typeOrName, attributes = {}) =>
|
||||
({ state, dispatch }) => {
|
||||
const type = getNodeType(typeOrName, state.schema)
|
||||
const isActive = isNodeActive(state, type, attributes)
|
||||
|
||||
if (!isActive) {
|
||||
return false
|
||||
}
|
||||
|
||||
return originalLift(state, dispatch)
|
||||
}
|
||||
21
node_modules/@tiptap/core/src/commands/liftEmptyBlock.ts
generated
vendored
Normal file
21
node_modules/@tiptap/core/src/commands/liftEmptyBlock.ts
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
import { liftEmptyBlock as originalLiftEmptyBlock } from '@tiptap/pm/commands'
|
||||
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
liftEmptyBlock: {
|
||||
/**
|
||||
* If the cursor is in an empty textblock that can be lifted, lift the block.
|
||||
* @example editor.commands.liftEmptyBlock()
|
||||
*/
|
||||
liftEmptyBlock: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const liftEmptyBlock: RawCommands['liftEmptyBlock'] =
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
return originalLiftEmptyBlock(state, dispatch)
|
||||
}
|
||||
26
node_modules/@tiptap/core/src/commands/liftListItem.ts
generated
vendored
Normal file
26
node_modules/@tiptap/core/src/commands/liftListItem.ts
generated
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { NodeType } from '@tiptap/pm/model'
|
||||
import { liftListItem as originalLiftListItem } from '@tiptap/pm/schema-list'
|
||||
|
||||
import { getNodeType } from '../helpers/getNodeType.js'
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
liftListItem: {
|
||||
/**
|
||||
* Create a command to lift the list item around the selection up into a wrapping list.
|
||||
* @param typeOrName The type or name of the node.
|
||||
* @example editor.commands.liftListItem('listItem')
|
||||
*/
|
||||
liftListItem: (typeOrName: string | NodeType) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const liftListItem: RawCommands['liftListItem'] =
|
||||
typeOrName =>
|
||||
({ state, dispatch }) => {
|
||||
const type = getNodeType(typeOrName, state.schema)
|
||||
|
||||
return originalLiftListItem(type)(state, dispatch)
|
||||
}
|
||||
21
node_modules/@tiptap/core/src/commands/newlineInCode.ts
generated
vendored
Normal file
21
node_modules/@tiptap/core/src/commands/newlineInCode.ts
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
import { newlineInCode as originalNewlineInCode } from '@tiptap/pm/commands'
|
||||
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
newlineInCode: {
|
||||
/**
|
||||
* Add a newline character in code.
|
||||
* @example editor.commands.newlineInCode()
|
||||
*/
|
||||
newlineInCode: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const newlineInCode: RawCommands['newlineInCode'] =
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
return originalNewlineInCode(state, dispatch)
|
||||
}
|
||||
73
node_modules/@tiptap/core/src/commands/resetAttributes.ts
generated
vendored
Normal file
73
node_modules/@tiptap/core/src/commands/resetAttributes.ts
generated
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
import type { MarkType, NodeType } from '@tiptap/pm/model'
|
||||
|
||||
import { getMarkType } from '../helpers/getMarkType.js'
|
||||
import { getNodeType } from '../helpers/getNodeType.js'
|
||||
import { getSchemaTypeNameByName } from '../helpers/getSchemaTypeNameByName.js'
|
||||
import type { RawCommands } from '../types.js'
|
||||
import { deleteProps } from '../utilities/deleteProps.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
resetAttributes: {
|
||||
/**
|
||||
* Resets some node attributes to the default value.
|
||||
* @param typeOrName The type or name of the node.
|
||||
* @param attributes The attributes of the node to reset.
|
||||
* @example editor.commands.resetAttributes('heading', 'level')
|
||||
*/
|
||||
resetAttributes: (typeOrName: string | NodeType | MarkType, attributes: string | string[]) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const resetAttributes: RawCommands['resetAttributes'] =
|
||||
(typeOrName, attributes) =>
|
||||
({ tr, state, dispatch }) => {
|
||||
let nodeType: NodeType | null = null
|
||||
let markType: MarkType | null = null
|
||||
|
||||
const schemaType = getSchemaTypeNameByName(
|
||||
typeof typeOrName === 'string' ? typeOrName : typeOrName.name,
|
||||
state.schema,
|
||||
)
|
||||
|
||||
if (!schemaType) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (schemaType === 'node') {
|
||||
nodeType = getNodeType(typeOrName as NodeType, state.schema)
|
||||
}
|
||||
|
||||
if (schemaType === 'mark') {
|
||||
markType = getMarkType(typeOrName as MarkType, state.schema)
|
||||
}
|
||||
|
||||
let canReset = false
|
||||
|
||||
tr.selection.ranges.forEach(range => {
|
||||
state.doc.nodesBetween(range.$from.pos, range.$to.pos, (node, pos) => {
|
||||
if (nodeType && nodeType === node.type) {
|
||||
canReset = true
|
||||
|
||||
if (dispatch) {
|
||||
tr.setNodeMarkup(pos, undefined, deleteProps(node.attrs, attributes))
|
||||
}
|
||||
}
|
||||
|
||||
if (markType && node.marks.length) {
|
||||
node.marks.forEach(mark => {
|
||||
if (markType === mark.type) {
|
||||
canReset = true
|
||||
|
||||
if (dispatch) {
|
||||
tr.addMark(pos, pos + node.nodeSize, markType.create(deleteProps(mark.attrs, attributes)))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return canReset
|
||||
}
|
||||
23
node_modules/@tiptap/core/src/commands/scrollIntoView.ts
generated
vendored
Normal file
23
node_modules/@tiptap/core/src/commands/scrollIntoView.ts
generated
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
scrollIntoView: {
|
||||
/**
|
||||
* Scroll the selection into view.
|
||||
* @example editor.commands.scrollIntoView()
|
||||
*/
|
||||
scrollIntoView: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const scrollIntoView: RawCommands['scrollIntoView'] =
|
||||
() =>
|
||||
({ tr, dispatch }) => {
|
||||
if (dispatch) {
|
||||
tr.scrollIntoView()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
27
node_modules/@tiptap/core/src/commands/selectAll.ts
generated
vendored
Normal file
27
node_modules/@tiptap/core/src/commands/selectAll.ts
generated
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
import { AllSelection } from '@tiptap/pm/state'
|
||||
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
selectAll: {
|
||||
/**
|
||||
* Select the whole document.
|
||||
* @example editor.commands.selectAll()
|
||||
*/
|
||||
selectAll: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const selectAll: RawCommands['selectAll'] =
|
||||
() =>
|
||||
({ tr, dispatch }) => {
|
||||
if (dispatch) {
|
||||
const selection = new AllSelection(tr.doc)
|
||||
|
||||
tr.setSelection(selection)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
21
node_modules/@tiptap/core/src/commands/selectNodeBackward.ts
generated
vendored
Normal file
21
node_modules/@tiptap/core/src/commands/selectNodeBackward.ts
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
import { selectNodeBackward as originalSelectNodeBackward } from '@tiptap/pm/commands'
|
||||
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
selectNodeBackward: {
|
||||
/**
|
||||
* Select a node backward.
|
||||
* @example editor.commands.selectNodeBackward()
|
||||
*/
|
||||
selectNodeBackward: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const selectNodeBackward: RawCommands['selectNodeBackward'] =
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
return originalSelectNodeBackward(state, dispatch)
|
||||
}
|
||||
21
node_modules/@tiptap/core/src/commands/selectNodeForward.ts
generated
vendored
Normal file
21
node_modules/@tiptap/core/src/commands/selectNodeForward.ts
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
import { selectNodeForward as originalSelectNodeForward } from '@tiptap/pm/commands'
|
||||
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
selectNodeForward: {
|
||||
/**
|
||||
* Select a node forward.
|
||||
* @example editor.commands.selectNodeForward()
|
||||
*/
|
||||
selectNodeForward: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const selectNodeForward: RawCommands['selectNodeForward'] =
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
return originalSelectNodeForward(state, dispatch)
|
||||
}
|
||||
21
node_modules/@tiptap/core/src/commands/selectParentNode.ts
generated
vendored
Normal file
21
node_modules/@tiptap/core/src/commands/selectParentNode.ts
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
import { selectParentNode as originalSelectParentNode } from '@tiptap/pm/commands'
|
||||
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
selectParentNode: {
|
||||
/**
|
||||
* Select the parent node.
|
||||
* @example editor.commands.selectParentNode()
|
||||
*/
|
||||
selectParentNode: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const selectParentNode: RawCommands['selectParentNode'] =
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
return originalSelectParentNode(state, dispatch)
|
||||
}
|
||||
23
node_modules/@tiptap/core/src/commands/selectTextblockEnd.ts
generated
vendored
Normal file
23
node_modules/@tiptap/core/src/commands/selectTextblockEnd.ts
generated
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
// @ts-ignore
|
||||
// TODO: add types to @types/prosemirror-commands
|
||||
import { selectTextblockEnd as originalSelectTextblockEnd } from '@tiptap/pm/commands'
|
||||
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
selectTextblockEnd: {
|
||||
/**
|
||||
* Moves the cursor to the end of current text block.
|
||||
* @example editor.commands.selectTextblockEnd()
|
||||
*/
|
||||
selectTextblockEnd: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const selectTextblockEnd: RawCommands['selectTextblockEnd'] =
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
return originalSelectTextblockEnd(state, dispatch)
|
||||
}
|
||||
23
node_modules/@tiptap/core/src/commands/selectTextblockStart.ts
generated
vendored
Normal file
23
node_modules/@tiptap/core/src/commands/selectTextblockStart.ts
generated
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
// @ts-ignore
|
||||
// TODO: add types to @types/prosemirror-commands
|
||||
import { selectTextblockStart as originalSelectTextblockStart } from '@tiptap/pm/commands'
|
||||
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
selectTextblockStart: {
|
||||
/**
|
||||
* Moves the cursor to the start of current text block.
|
||||
* @example editor.commands.selectTextblockStart()
|
||||
*/
|
||||
selectTextblockStart: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const selectTextblockStart: RawCommands['selectTextblockStart'] =
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
return originalSelectTextblockStart(state, dispatch)
|
||||
}
|
||||
76
node_modules/@tiptap/core/src/commands/setContent.ts
generated
vendored
Normal file
76
node_modules/@tiptap/core/src/commands/setContent.ts
generated
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
import type { Fragment, Node as ProseMirrorNode, ParseOptions } from '@tiptap/pm/model'
|
||||
|
||||
import { createDocument } from '../helpers/createDocument.js'
|
||||
import type { Content, RawCommands } from '../types.js'
|
||||
|
||||
export interface SetContentOptions {
|
||||
/**
|
||||
* Options for parsing the content.
|
||||
* @default {}
|
||||
*/
|
||||
parseOptions?: ParseOptions
|
||||
|
||||
/**
|
||||
* Whether to throw an error if the content is invalid.
|
||||
*/
|
||||
errorOnInvalidContent?: boolean
|
||||
|
||||
/**
|
||||
* Whether to emit an update event.
|
||||
* @default true
|
||||
*/
|
||||
emitUpdate?: boolean
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
setContent: {
|
||||
/**
|
||||
* Replace the whole document with new content.
|
||||
* @param content The new content.
|
||||
* @param emitUpdate Whether to emit an update event.
|
||||
* @param parseOptions Options for parsing the content.
|
||||
* @example editor.commands.setContent('<p>Example text</p>')
|
||||
*/
|
||||
setContent: (
|
||||
/**
|
||||
* The new content.
|
||||
*/
|
||||
content: Content | Fragment | ProseMirrorNode,
|
||||
|
||||
/**
|
||||
* Options for `setContent`.
|
||||
*/
|
||||
options?: SetContentOptions,
|
||||
) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const setContent: RawCommands['setContent'] =
|
||||
(content, { errorOnInvalidContent, emitUpdate = true, parseOptions = {} } = {}) =>
|
||||
({ editor, tr, dispatch, commands }) => {
|
||||
const { doc } = tr
|
||||
|
||||
// This is to keep backward compatibility with the previous behavior
|
||||
// TODO remove this in the next major version
|
||||
if (parseOptions.preserveWhitespace !== 'full') {
|
||||
const document = createDocument(content, editor.schema, parseOptions, {
|
||||
errorOnInvalidContent: errorOnInvalidContent ?? editor.options.enableContentCheck,
|
||||
})
|
||||
|
||||
if (dispatch) {
|
||||
tr.replaceWith(0, doc.content.size, document).setMeta('preventUpdate', !emitUpdate)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if (dispatch) {
|
||||
tr.setMeta('preventUpdate', !emitUpdate)
|
||||
}
|
||||
|
||||
return commands.insertContentAt({ from: 0, to: doc.content.size }, content, {
|
||||
parseOptions,
|
||||
errorOnInvalidContent: errorOnInvalidContent ?? editor.options.enableContentCheck,
|
||||
})
|
||||
}
|
||||
118
node_modules/@tiptap/core/src/commands/setMark.ts
generated
vendored
Normal file
118
node_modules/@tiptap/core/src/commands/setMark.ts
generated
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
import type { MarkType, ResolvedPos } from '@tiptap/pm/model'
|
||||
import type { EditorState, Transaction } from '@tiptap/pm/state'
|
||||
|
||||
import { getMarkAttributes } from '../helpers/getMarkAttributes.js'
|
||||
import { getMarkType } from '../helpers/getMarkType.js'
|
||||
import { isTextSelection } from '../helpers/index.js'
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
setMark: {
|
||||
/**
|
||||
* Add a mark with new attributes.
|
||||
* @param typeOrName The mark type or name.
|
||||
* @example editor.commands.setMark('bold', { level: 1 })
|
||||
*/
|
||||
setMark: (typeOrName: string | MarkType, attributes?: Record<string, any>) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function canSetMark(state: EditorState, tr: Transaction, newMarkType: MarkType) {
|
||||
const { selection } = tr
|
||||
let cursor: ResolvedPos | null = null
|
||||
|
||||
if (isTextSelection(selection)) {
|
||||
cursor = selection.$cursor
|
||||
}
|
||||
|
||||
if (cursor) {
|
||||
const currentMarks = state.storedMarks ?? cursor.marks()
|
||||
const parentAllowsMarkType = cursor.parent.type.allowsMarkType(newMarkType)
|
||||
|
||||
// There can be no current marks that exclude the new mark, and the parent must allow this mark type
|
||||
return (
|
||||
parentAllowsMarkType &&
|
||||
(!!newMarkType.isInSet(currentMarks) || !currentMarks.some(mark => mark.type.excludes(newMarkType)))
|
||||
)
|
||||
}
|
||||
|
||||
const { ranges } = selection
|
||||
|
||||
return ranges.some(({ $from, $to }) => {
|
||||
let someNodeSupportsMark =
|
||||
$from.depth === 0 ? state.doc.inlineContent && state.doc.type.allowsMarkType(newMarkType) : false
|
||||
|
||||
state.doc.nodesBetween($from.pos, $to.pos, (node, _pos, parent) => {
|
||||
// If we already found a mark that we can enable, return false to bypass the remaining search
|
||||
if (someNodeSupportsMark) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (node.isInline) {
|
||||
const parentAllowsMarkType = !parent || parent.type.allowsMarkType(newMarkType)
|
||||
const currentMarksAllowMarkType =
|
||||
!!newMarkType.isInSet(node.marks) || !node.marks.some(otherMark => otherMark.type.excludes(newMarkType))
|
||||
|
||||
someNodeSupportsMark = parentAllowsMarkType && currentMarksAllowMarkType
|
||||
}
|
||||
return !someNodeSupportsMark
|
||||
})
|
||||
|
||||
return someNodeSupportsMark
|
||||
})
|
||||
}
|
||||
export const setMark: RawCommands['setMark'] =
|
||||
(typeOrName, attributes = {}) =>
|
||||
({ tr, state, dispatch }) => {
|
||||
const { selection } = tr
|
||||
const { empty, ranges } = selection
|
||||
const type = getMarkType(typeOrName, state.schema)
|
||||
|
||||
if (dispatch) {
|
||||
if (empty) {
|
||||
const oldAttributes = getMarkAttributes(state, type)
|
||||
|
||||
tr.addStoredMark(
|
||||
type.create({
|
||||
...oldAttributes,
|
||||
...attributes,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
ranges.forEach(range => {
|
||||
const from = range.$from.pos
|
||||
const to = range.$to.pos
|
||||
|
||||
state.doc.nodesBetween(from, to, (node, pos) => {
|
||||
const trimmedFrom = Math.max(pos, from)
|
||||
const trimmedTo = Math.min(pos + node.nodeSize, to)
|
||||
const someHasMark = node.marks.find(mark => mark.type === type)
|
||||
|
||||
// if there is already a mark of this type
|
||||
// we know that we have to merge its attributes
|
||||
// otherwise we add a fresh new mark
|
||||
if (someHasMark) {
|
||||
node.marks.forEach(mark => {
|
||||
if (type === mark.type) {
|
||||
tr.addMark(
|
||||
trimmedFrom,
|
||||
trimmedTo,
|
||||
type.create({
|
||||
...mark.attrs,
|
||||
...attributes,
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
tr.addMark(trimmedFrom, trimmedTo, type.create(attributes))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return canSetMark(state, tr, type)
|
||||
}
|
||||
25
node_modules/@tiptap/core/src/commands/setMeta.ts
generated
vendored
Normal file
25
node_modules/@tiptap/core/src/commands/setMeta.ts
generated
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
setMeta: {
|
||||
/**
|
||||
* Store a metadata property in the current transaction.
|
||||
* @param key The key of the metadata property.
|
||||
* @param value The value to store.
|
||||
* @example editor.commands.setMeta('foo', 'bar')
|
||||
*/
|
||||
setMeta: (key: string | Plugin | PluginKey, value: any) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const setMeta: RawCommands['setMeta'] =
|
||||
(key, value) =>
|
||||
({ tr }) => {
|
||||
tr.setMeta(key, value)
|
||||
|
||||
return true
|
||||
}
|
||||
57
node_modules/@tiptap/core/src/commands/setNode.ts
generated
vendored
Normal file
57
node_modules/@tiptap/core/src/commands/setNode.ts
generated
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
import { setBlockType } from '@tiptap/pm/commands'
|
||||
import type { NodeType } from '@tiptap/pm/model'
|
||||
|
||||
import { getNodeType } from '../helpers/getNodeType.js'
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
setNode: {
|
||||
/**
|
||||
* Replace a given range with a node.
|
||||
* @param typeOrName The type or name of the node
|
||||
* @param attributes The attributes of the node
|
||||
* @example editor.commands.setNode('paragraph')
|
||||
*/
|
||||
setNode: (typeOrName: string | NodeType, attributes?: Record<string, any>) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const setNode: RawCommands['setNode'] =
|
||||
(typeOrName, attributes = {}) =>
|
||||
({ state, dispatch, chain }) => {
|
||||
const type = getNodeType(typeOrName, state.schema)
|
||||
|
||||
let attributesToCopy: Record<string, any> | undefined
|
||||
|
||||
if (state.selection.$anchor.sameParent(state.selection.$head)) {
|
||||
// only copy attributes if the selection is pointing to a node of the same type
|
||||
attributesToCopy = state.selection.$anchor.parent.attrs
|
||||
}
|
||||
|
||||
// TODO: use a fallback like insertContent?
|
||||
if (!type.isTextblock) {
|
||||
console.warn('[tiptap warn]: Currently "setNode()" only supports text block nodes.')
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
chain()
|
||||
// try to convert node to default node if needed
|
||||
.command(({ commands }) => {
|
||||
const canSetBlock = setBlockType(type, { ...attributesToCopy, ...attributes })(state)
|
||||
|
||||
if (canSetBlock) {
|
||||
return true
|
||||
}
|
||||
|
||||
return commands.clearNodes()
|
||||
})
|
||||
.command(({ state: updatedState }) => {
|
||||
return setBlockType(type, { ...attributesToCopy, ...attributes })(updatedState, dispatch)
|
||||
})
|
||||
.run()
|
||||
)
|
||||
}
|
||||
31
node_modules/@tiptap/core/src/commands/setNodeSelection.ts
generated
vendored
Normal file
31
node_modules/@tiptap/core/src/commands/setNodeSelection.ts
generated
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
import { NodeSelection } from '@tiptap/pm/state'
|
||||
|
||||
import type { RawCommands } from '../types.js'
|
||||
import { minMax } from '../utilities/minMax.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
setNodeSelection: {
|
||||
/**
|
||||
* Creates a NodeSelection.
|
||||
* @param position - Position of the node.
|
||||
* @example editor.commands.setNodeSelection(10)
|
||||
*/
|
||||
setNodeSelection: (position: number) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const setNodeSelection: RawCommands['setNodeSelection'] =
|
||||
position =>
|
||||
({ tr, dispatch }) => {
|
||||
if (dispatch) {
|
||||
const { doc } = tr
|
||||
const from = minMax(position, 0, doc.content.size)
|
||||
const selection = NodeSelection.create(doc, from)
|
||||
|
||||
tr.setSelection(selection)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
51
node_modules/@tiptap/core/src/commands/setTextDirection.ts
generated
vendored
Normal file
51
node_modules/@tiptap/core/src/commands/setTextDirection.ts
generated
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { Range, RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
setTextDirection: {
|
||||
/**
|
||||
* Set the text direction for nodes.
|
||||
* If no position is provided, it will use the current selection.
|
||||
* @param direction The text direction to set ('ltr', 'rtl', or 'auto')
|
||||
* @param position Optional position or range to apply the direction to
|
||||
* @example editor.commands.setTextDirection('rtl')
|
||||
* @example editor.commands.setTextDirection('ltr', { from: 0, to: 10 })
|
||||
*/
|
||||
setTextDirection: (direction: 'ltr' | 'rtl' | 'auto', position?: number | Range) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const setTextDirection: RawCommands['setTextDirection'] =
|
||||
(direction, position) =>
|
||||
({ tr, state, dispatch }) => {
|
||||
const { selection } = state
|
||||
let from: number
|
||||
let to: number
|
||||
|
||||
if (typeof position === 'number') {
|
||||
from = position
|
||||
to = position
|
||||
} else if (position && 'from' in position && 'to' in position) {
|
||||
from = position.from
|
||||
to = position.to
|
||||
} else {
|
||||
from = selection.from
|
||||
to = selection.to
|
||||
}
|
||||
|
||||
if (dispatch) {
|
||||
tr.doc.nodesBetween(from, to, (node, pos) => {
|
||||
if (node.isText) {
|
||||
return
|
||||
}
|
||||
|
||||
tr.setNodeMarkup(pos, undefined, {
|
||||
...node.attrs,
|
||||
dir: direction,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
35
node_modules/@tiptap/core/src/commands/setTextSelection.ts
generated
vendored
Normal file
35
node_modules/@tiptap/core/src/commands/setTextSelection.ts
generated
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
import { TextSelection } from '@tiptap/pm/state'
|
||||
|
||||
import type { Range, RawCommands } from '../types.js'
|
||||
import { minMax } from '../utilities/minMax.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
setTextSelection: {
|
||||
/**
|
||||
* Creates a TextSelection.
|
||||
* @param position The position of the selection.
|
||||
* @example editor.commands.setTextSelection(10)
|
||||
*/
|
||||
setTextSelection: (position: number | Range) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const setTextSelection: RawCommands['setTextSelection'] =
|
||||
position =>
|
||||
({ tr, dispatch }) => {
|
||||
if (dispatch) {
|
||||
const { doc } = tr
|
||||
const { from, to } = typeof position === 'number' ? { from: position, to: position } : position
|
||||
const minPos = TextSelection.atStart(doc).from
|
||||
const maxPos = TextSelection.atEnd(doc).to
|
||||
const resolvedFrom = minMax(from, minPos, maxPos)
|
||||
const resolvedEnd = minMax(to, minPos, maxPos)
|
||||
const selection = TextSelection.create(doc, resolvedFrom, resolvedEnd)
|
||||
|
||||
tr.setSelection(selection)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
26
node_modules/@tiptap/core/src/commands/sinkListItem.ts
generated
vendored
Normal file
26
node_modules/@tiptap/core/src/commands/sinkListItem.ts
generated
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { NodeType } from '@tiptap/pm/model'
|
||||
import { sinkListItem as originalSinkListItem } from '@tiptap/pm/schema-list'
|
||||
|
||||
import { getNodeType } from '../helpers/getNodeType.js'
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
sinkListItem: {
|
||||
/**
|
||||
* Sink the list item down into an inner list.
|
||||
* @param typeOrName The type or name of the node.
|
||||
* @example editor.commands.sinkListItem('listItem')
|
||||
*/
|
||||
sinkListItem: (typeOrName: string | NodeType) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const sinkListItem: RawCommands['sinkListItem'] =
|
||||
typeOrName =>
|
||||
({ state, dispatch }) => {
|
||||
const type = getNodeType(typeOrName, state.schema)
|
||||
|
||||
return originalSinkListItem(type)(state, dispatch)
|
||||
}
|
||||
115
node_modules/@tiptap/core/src/commands/splitBlock.ts
generated
vendored
Normal file
115
node_modules/@tiptap/core/src/commands/splitBlock.ts
generated
vendored
Normal file
@@ -0,0 +1,115 @@
|
||||
import type { EditorState } from '@tiptap/pm/state'
|
||||
import { NodeSelection, TextSelection } from '@tiptap/pm/state'
|
||||
import { canSplit } from '@tiptap/pm/transform'
|
||||
|
||||
import { defaultBlockAt } from '../helpers/defaultBlockAt.js'
|
||||
import { getSplittedAttributes } from '../helpers/getSplittedAttributes.js'
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
function ensureMarks(state: EditorState, splittableMarks?: string[]) {
|
||||
const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks())
|
||||
|
||||
if (marks) {
|
||||
const filteredMarks = marks.filter(mark => splittableMarks?.includes(mark.type.name))
|
||||
|
||||
state.tr.ensureMarks(filteredMarks)
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
splitBlock: {
|
||||
/**
|
||||
* Forks a new node from an existing node.
|
||||
* @param options.keepMarks Keep marks from the previous node.
|
||||
* @example editor.commands.splitBlock()
|
||||
* @example editor.commands.splitBlock({ keepMarks: true })
|
||||
*/
|
||||
splitBlock: (options?: { keepMarks?: boolean }) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const splitBlock: RawCommands['splitBlock'] =
|
||||
({ keepMarks = true } = {}) =>
|
||||
({ tr, state, dispatch, editor }) => {
|
||||
const { selection, doc } = tr
|
||||
const { $from, $to } = selection
|
||||
const extensionAttributes = editor.extensionManager.attributes
|
||||
const newAttributes = getSplittedAttributes(extensionAttributes, $from.node().type.name, $from.node().attrs)
|
||||
|
||||
if (selection instanceof NodeSelection && selection.node.isBlock) {
|
||||
if (!$from.parentOffset || !canSplit(doc, $from.pos)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (dispatch) {
|
||||
if (keepMarks) {
|
||||
ensureMarks(state, editor.extensionManager.splittableMarks)
|
||||
}
|
||||
|
||||
tr.split($from.pos).scrollIntoView()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if (!$from.parent.isBlock) {
|
||||
return false
|
||||
}
|
||||
|
||||
const atEnd = $to.parentOffset === $to.parent.content.size
|
||||
|
||||
const deflt = $from.depth === 0 ? undefined : defaultBlockAt($from.node(-1).contentMatchAt($from.indexAfter(-1)))
|
||||
|
||||
let types =
|
||||
atEnd && deflt
|
||||
? [
|
||||
{
|
||||
type: deflt,
|
||||
attrs: newAttributes,
|
||||
},
|
||||
]
|
||||
: undefined
|
||||
|
||||
let can = canSplit(tr.doc, tr.mapping.map($from.pos), 1, types)
|
||||
|
||||
if (!types && !can && canSplit(tr.doc, tr.mapping.map($from.pos), 1, deflt ? [{ type: deflt }] : undefined)) {
|
||||
can = true
|
||||
types = deflt
|
||||
? [
|
||||
{
|
||||
type: deflt,
|
||||
attrs: newAttributes,
|
||||
},
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
|
||||
if (dispatch) {
|
||||
if (can) {
|
||||
if (selection instanceof TextSelection) {
|
||||
tr.deleteSelection()
|
||||
}
|
||||
|
||||
tr.split(tr.mapping.map($from.pos), 1, types)
|
||||
|
||||
if (deflt && !atEnd && !$from.parentOffset && $from.parent.type !== deflt) {
|
||||
const first = tr.mapping.map($from.before())
|
||||
const $first = tr.doc.resolve(first)
|
||||
|
||||
if ($from.node(-1).canReplaceWith($first.index(), $first.index() + 1, deflt)) {
|
||||
tr.setNodeMarkup(tr.mapping.map($from.before()), deflt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (keepMarks) {
|
||||
ensureMarks(state, editor.extensionManager.splittableMarks)
|
||||
}
|
||||
|
||||
tr.scrollIntoView()
|
||||
}
|
||||
|
||||
return can
|
||||
}
|
||||
149
node_modules/@tiptap/core/src/commands/splitListItem.ts
generated
vendored
Normal file
149
node_modules/@tiptap/core/src/commands/splitListItem.ts
generated
vendored
Normal file
@@ -0,0 +1,149 @@
|
||||
import type { Node as ProseMirrorNode, NodeType } from '@tiptap/pm/model'
|
||||
import { Fragment, Slice } from '@tiptap/pm/model'
|
||||
import { TextSelection } from '@tiptap/pm/state'
|
||||
import { canSplit } from '@tiptap/pm/transform'
|
||||
|
||||
import { getNodeType } from '../helpers/getNodeType.js'
|
||||
import { getSplittedAttributes } from '../helpers/getSplittedAttributes.js'
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
splitListItem: {
|
||||
/**
|
||||
* Splits one list item into two list items.
|
||||
* @param typeOrName The type or name of the node.
|
||||
* @param overrideAttrs The attributes to ensure on the new node.
|
||||
* @example editor.commands.splitListItem('listItem')
|
||||
*/
|
||||
splitListItem: (typeOrName: string | NodeType, overrideAttrs?: Record<string, any>) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const splitListItem: RawCommands['splitListItem'] =
|
||||
(typeOrName, overrideAttrs = {}) =>
|
||||
({ tr, state, dispatch, editor }) => {
|
||||
const type = getNodeType(typeOrName, state.schema)
|
||||
const { $from, $to } = state.selection
|
||||
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line
|
||||
const node: ProseMirrorNode = state.selection.node
|
||||
|
||||
if ((node && node.isBlock) || $from.depth < 2 || !$from.sameParent($to)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const grandParent = $from.node(-1)
|
||||
|
||||
if (grandParent.type !== type) {
|
||||
return false
|
||||
}
|
||||
|
||||
const extensionAttributes = editor.extensionManager.attributes
|
||||
|
||||
if ($from.parent.content.size === 0 && $from.node(-1).childCount === $from.indexAfter(-1)) {
|
||||
// In an empty block. If this is a nested list, the wrapping
|
||||
// list item should be split. Otherwise, bail out and let next
|
||||
// command handle lifting.
|
||||
if ($from.depth === 2 || $from.node(-3).type !== type || $from.index(-2) !== $from.node(-2).childCount - 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (dispatch) {
|
||||
let wrap = Fragment.empty
|
||||
// eslint-disable-next-line
|
||||
const depthBefore = $from.index(-1) ? 1 : $from.index(-2) ? 2 : 3
|
||||
|
||||
// Build a fragment containing empty versions of the structure
|
||||
// from the outer list item to the parent node of the cursor
|
||||
for (let d = $from.depth - depthBefore; d >= $from.depth - 3; d -= 1) {
|
||||
wrap = Fragment.from($from.node(d).copy(wrap))
|
||||
}
|
||||
|
||||
const depthAfter =
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
$from.indexAfter(-1) < $from.node(-2).childCount
|
||||
? 1
|
||||
: $from.indexAfter(-2) < $from.node(-3).childCount
|
||||
? 2
|
||||
: 3
|
||||
|
||||
// Add a second list item with an empty default start node
|
||||
const newNextTypeAttributes = {
|
||||
...getSplittedAttributes(extensionAttributes, $from.node().type.name, $from.node().attrs),
|
||||
...overrideAttrs,
|
||||
}
|
||||
const nextType = type.contentMatch.defaultType?.createAndFill(newNextTypeAttributes) || undefined
|
||||
|
||||
wrap = wrap.append(Fragment.from(type.createAndFill(null, nextType) || undefined))
|
||||
|
||||
const start = $from.before($from.depth - (depthBefore - 1))
|
||||
|
||||
tr.replace(start, $from.after(-depthAfter), new Slice(wrap, 4 - depthBefore, 0))
|
||||
|
||||
let sel = -1
|
||||
|
||||
tr.doc.nodesBetween(start, tr.doc.content.size, (n, pos) => {
|
||||
if (sel > -1) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (n.isTextblock && n.content.size === 0) {
|
||||
sel = pos + 1
|
||||
}
|
||||
})
|
||||
|
||||
if (sel > -1) {
|
||||
tr.setSelection(TextSelection.near(tr.doc.resolve(sel)))
|
||||
}
|
||||
|
||||
tr.scrollIntoView()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const nextType = $to.pos === $from.end() ? grandParent.contentMatchAt(0).defaultType : null
|
||||
|
||||
const newTypeAttributes = {
|
||||
...getSplittedAttributes(extensionAttributes, grandParent.type.name, grandParent.attrs),
|
||||
...overrideAttrs,
|
||||
}
|
||||
const newNextTypeAttributes = {
|
||||
...getSplittedAttributes(extensionAttributes, $from.node().type.name, $from.node().attrs),
|
||||
...overrideAttrs,
|
||||
}
|
||||
|
||||
tr.delete($from.pos, $to.pos)
|
||||
|
||||
const types = nextType
|
||||
? [
|
||||
{ type, attrs: newTypeAttributes },
|
||||
{ type: nextType, attrs: newNextTypeAttributes },
|
||||
]
|
||||
: [{ type, attrs: newTypeAttributes }]
|
||||
|
||||
if (!canSplit(tr.doc, $from.pos, 2)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (dispatch) {
|
||||
const { selection, storedMarks } = state
|
||||
const { splittableMarks } = editor.extensionManager
|
||||
const marks = storedMarks || (selection.$to.parentOffset && selection.$from.marks())
|
||||
|
||||
tr.split($from.pos, 2, types).scrollIntoView()
|
||||
|
||||
if (!marks || !dispatch) {
|
||||
return true
|
||||
}
|
||||
|
||||
const filteredMarks = marks.filter(mark => splittableMarks.includes(mark.type.name))
|
||||
|
||||
tr.ensureMarks(filteredMarks)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
159
node_modules/@tiptap/core/src/commands/toggleList.ts
generated
vendored
Normal file
159
node_modules/@tiptap/core/src/commands/toggleList.ts
generated
vendored
Normal file
@@ -0,0 +1,159 @@
|
||||
import type { NodeType } from '@tiptap/pm/model'
|
||||
import type { Transaction } from '@tiptap/pm/state'
|
||||
import { canJoin } from '@tiptap/pm/transform'
|
||||
|
||||
import { findParentNode } from '../helpers/findParentNode.js'
|
||||
import { getNodeType } from '../helpers/getNodeType.js'
|
||||
import { isList } from '../helpers/isList.js'
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
const joinListBackwards = (tr: Transaction, listType: NodeType): boolean => {
|
||||
const list = findParentNode(node => node.type === listType)(tr.selection)
|
||||
|
||||
if (!list) {
|
||||
return true
|
||||
}
|
||||
|
||||
const before = tr.doc.resolve(Math.max(0, list.pos - 1)).before(list.depth)
|
||||
|
||||
if (before === undefined) {
|
||||
return true
|
||||
}
|
||||
|
||||
const nodeBefore = tr.doc.nodeAt(before)
|
||||
const canJoinBackwards = list.node.type === nodeBefore?.type && canJoin(tr.doc, list.pos)
|
||||
|
||||
if (!canJoinBackwards) {
|
||||
return true
|
||||
}
|
||||
|
||||
tr.join(list.pos)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const joinListForwards = (tr: Transaction, listType: NodeType): boolean => {
|
||||
const list = findParentNode(node => node.type === listType)(tr.selection)
|
||||
|
||||
if (!list) {
|
||||
return true
|
||||
}
|
||||
|
||||
const after = tr.doc.resolve(list.start).after(list.depth)
|
||||
|
||||
if (after === undefined) {
|
||||
return true
|
||||
}
|
||||
|
||||
const nodeAfter = tr.doc.nodeAt(after)
|
||||
const canJoinForwards = list.node.type === nodeAfter?.type && canJoin(tr.doc, after)
|
||||
|
||||
if (!canJoinForwards) {
|
||||
return true
|
||||
}
|
||||
|
||||
tr.join(after)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
toggleList: {
|
||||
/**
|
||||
* Toggle between different list types.
|
||||
* @param listTypeOrName The type or name of the list.
|
||||
* @param itemTypeOrName The type or name of the list item.
|
||||
* @param keepMarks Keep marks when toggling.
|
||||
* @param attributes Attributes for the new list.
|
||||
* @example editor.commands.toggleList('bulletList', 'listItem')
|
||||
*/
|
||||
toggleList: (
|
||||
listTypeOrName: string | NodeType,
|
||||
itemTypeOrName: string | NodeType,
|
||||
keepMarks?: boolean,
|
||||
attributes?: Record<string, any>,
|
||||
) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const toggleList: RawCommands['toggleList'] =
|
||||
(listTypeOrName, itemTypeOrName, keepMarks, attributes = {}) =>
|
||||
({ editor, tr, state, dispatch, chain, commands, can }) => {
|
||||
const { extensions, splittableMarks } = editor.extensionManager
|
||||
const listType = getNodeType(listTypeOrName, state.schema)
|
||||
const itemType = getNodeType(itemTypeOrName, state.schema)
|
||||
const { selection, storedMarks } = state
|
||||
const { $from, $to } = selection
|
||||
const range = $from.blockRange($to)
|
||||
|
||||
const marks = storedMarks || (selection.$to.parentOffset && selection.$from.marks())
|
||||
|
||||
if (!range) {
|
||||
return false
|
||||
}
|
||||
|
||||
const parentList = findParentNode(node => isList(node.type.name, extensions))(selection)
|
||||
|
||||
if (range.depth >= 1 && parentList && range.depth - parentList.depth <= 1) {
|
||||
// remove list
|
||||
if (parentList.node.type === listType) {
|
||||
return commands.liftListItem(itemType)
|
||||
}
|
||||
|
||||
// change list type
|
||||
if (isList(parentList.node.type.name, extensions) && listType.validContent(parentList.node.content) && dispatch) {
|
||||
return chain()
|
||||
.command(() => {
|
||||
tr.setNodeMarkup(parentList.pos, listType)
|
||||
|
||||
return true
|
||||
})
|
||||
.command(() => joinListBackwards(tr, listType))
|
||||
.command(() => joinListForwards(tr, listType))
|
||||
.run()
|
||||
}
|
||||
}
|
||||
if (!keepMarks || !marks || !dispatch) {
|
||||
return (
|
||||
chain()
|
||||
// try to convert node to default node if needed
|
||||
.command(() => {
|
||||
const canWrapInList = can().wrapInList(listType, attributes)
|
||||
|
||||
if (canWrapInList) {
|
||||
return true
|
||||
}
|
||||
|
||||
return commands.clearNodes()
|
||||
})
|
||||
.wrapInList(listType, attributes)
|
||||
.command(() => joinListBackwards(tr, listType))
|
||||
.command(() => joinListForwards(tr, listType))
|
||||
.run()
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
chain()
|
||||
// try to convert node to default node if needed
|
||||
.command(() => {
|
||||
const canWrapInList = can().wrapInList(listType, attributes)
|
||||
|
||||
const filteredMarks = marks.filter(mark => splittableMarks.includes(mark.type.name))
|
||||
|
||||
tr.ensureMarks(filteredMarks)
|
||||
|
||||
if (canWrapInList) {
|
||||
return true
|
||||
}
|
||||
|
||||
return commands.clearNodes()
|
||||
})
|
||||
.wrapInList(listType, attributes)
|
||||
.command(() => joinListBackwards(tr, listType))
|
||||
.command(() => joinListForwards(tr, listType))
|
||||
.run()
|
||||
)
|
||||
}
|
||||
51
node_modules/@tiptap/core/src/commands/toggleMark.ts
generated
vendored
Normal file
51
node_modules/@tiptap/core/src/commands/toggleMark.ts
generated
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { MarkType } from '@tiptap/pm/model'
|
||||
|
||||
import { getMarkType } from '../helpers/getMarkType.js'
|
||||
import { isMarkActive } from '../helpers/isMarkActive.js'
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
toggleMark: {
|
||||
/**
|
||||
* Toggle a mark on and off.
|
||||
* @param typeOrName The mark type or name.
|
||||
* @param attributes The attributes of the mark.
|
||||
* @param options.extendEmptyMarkRange Removes the mark even across the current selection. Defaults to `false`.
|
||||
* @example editor.commands.toggleMark('bold')
|
||||
*/
|
||||
toggleMark: (
|
||||
/**
|
||||
* The mark type or name.
|
||||
*/
|
||||
typeOrName: string | MarkType,
|
||||
|
||||
/**
|
||||
* The attributes of the mark.
|
||||
*/
|
||||
attributes?: Record<string, any>,
|
||||
|
||||
options?: {
|
||||
/**
|
||||
* Removes the mark even across the current selection. Defaults to `false`.
|
||||
*/
|
||||
extendEmptyMarkRange?: boolean
|
||||
},
|
||||
) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const toggleMark: RawCommands['toggleMark'] =
|
||||
(typeOrName, attributes = {}, options = {}) =>
|
||||
({ state, commands }) => {
|
||||
const { extendEmptyMarkRange = false } = options
|
||||
const type = getMarkType(typeOrName, state.schema)
|
||||
const isActive = isMarkActive(state, type, attributes)
|
||||
|
||||
if (isActive) {
|
||||
return commands.unsetMark(type, { extendEmptyMarkRange })
|
||||
}
|
||||
|
||||
return commands.setMark(type, attributes)
|
||||
}
|
||||
47
node_modules/@tiptap/core/src/commands/toggleNode.ts
generated
vendored
Normal file
47
node_modules/@tiptap/core/src/commands/toggleNode.ts
generated
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { NodeType } from '@tiptap/pm/model'
|
||||
|
||||
import { getNodeType } from '../helpers/getNodeType.js'
|
||||
import { isNodeActive } from '../helpers/isNodeActive.js'
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
toggleNode: {
|
||||
/**
|
||||
* Toggle a node with another node.
|
||||
* @param typeOrName The type or name of the node.
|
||||
* @param toggleTypeOrName The type or name of the node to toggle.
|
||||
* @param attributes The attributes of the node.
|
||||
* @example editor.commands.toggleNode('heading', 'paragraph')
|
||||
*/
|
||||
toggleNode: (
|
||||
typeOrName: string | NodeType,
|
||||
toggleTypeOrName: string | NodeType,
|
||||
attributes?: Record<string, any>,
|
||||
) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const toggleNode: RawCommands['toggleNode'] =
|
||||
(typeOrName, toggleTypeOrName, attributes = {}) =>
|
||||
({ state, commands }) => {
|
||||
const type = getNodeType(typeOrName, state.schema)
|
||||
const toggleType = getNodeType(toggleTypeOrName, state.schema)
|
||||
const isActive = isNodeActive(state, type, attributes)
|
||||
|
||||
let attributesToCopy: Record<string, any> | undefined
|
||||
|
||||
if (state.selection.$anchor.sameParent(state.selection.$head)) {
|
||||
// only copy attributes if the selection is pointing to a node of the same type
|
||||
attributesToCopy = state.selection.$anchor.parent.attrs
|
||||
}
|
||||
|
||||
if (isActive) {
|
||||
return commands.setNode(toggleType, attributesToCopy)
|
||||
}
|
||||
|
||||
// If the node is not active, we want to set the new node type with the given attributes
|
||||
// Copying over the attributes from the current node if the selection is pointing to a node of the same type
|
||||
return commands.setNode(type, { ...attributesToCopy, ...attributes })
|
||||
}
|
||||
32
node_modules/@tiptap/core/src/commands/toggleWrap.ts
generated
vendored
Normal file
32
node_modules/@tiptap/core/src/commands/toggleWrap.ts
generated
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { NodeType } from '@tiptap/pm/model'
|
||||
|
||||
import { getNodeType } from '../helpers/getNodeType.js'
|
||||
import { isNodeActive } from '../helpers/isNodeActive.js'
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
toggleWrap: {
|
||||
/**
|
||||
* Wraps nodes in another node, or removes an existing wrap.
|
||||
* @param typeOrName The type or name of the node.
|
||||
* @param attributes The attributes of the node.
|
||||
* @example editor.commands.toggleWrap('blockquote')
|
||||
*/
|
||||
toggleWrap: (typeOrName: string | NodeType, attributes?: Record<string, any>) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const toggleWrap: RawCommands['toggleWrap'] =
|
||||
(typeOrName, attributes = {}) =>
|
||||
({ state, commands }) => {
|
||||
const type = getNodeType(typeOrName, state.schema)
|
||||
const isActive = isNodeActive(state, type, attributes)
|
||||
|
||||
if (isActive) {
|
||||
return commands.lift(type)
|
||||
}
|
||||
|
||||
return commands.wrapIn(type, attributes)
|
||||
}
|
||||
49
node_modules/@tiptap/core/src/commands/undoInputRule.ts
generated
vendored
Normal file
49
node_modules/@tiptap/core/src/commands/undoInputRule.ts
generated
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
undoInputRule: {
|
||||
/**
|
||||
* Undo an input rule.
|
||||
* @example editor.commands.undoInputRule()
|
||||
*/
|
||||
undoInputRule: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const undoInputRule: RawCommands['undoInputRule'] =
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
const plugins = state.plugins
|
||||
|
||||
for (let i = 0; i < plugins.length; i += 1) {
|
||||
const plugin = plugins[i]
|
||||
let undoable
|
||||
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line
|
||||
if (plugin.spec.isInputRules && (undoable = plugin.getState(state))) {
|
||||
if (dispatch) {
|
||||
const tr = state.tr
|
||||
const toUndo = undoable.transform
|
||||
|
||||
for (let j = toUndo.steps.length - 1; j >= 0; j -= 1) {
|
||||
tr.step(toUndo.steps[j].invert(toUndo.docs[j]))
|
||||
}
|
||||
|
||||
if (undoable.text) {
|
||||
const marks = tr.doc.resolve(undoable.from).marks()
|
||||
|
||||
tr.replaceWith(undoable.from, undoable.to, state.schema.text(undoable.text, marks))
|
||||
} else {
|
||||
tr.delete(undoable.from, undoable.to)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
32
node_modules/@tiptap/core/src/commands/unsetAllMarks.ts
generated
vendored
Normal file
32
node_modules/@tiptap/core/src/commands/unsetAllMarks.ts
generated
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
unsetAllMarks: {
|
||||
/**
|
||||
* Remove all marks in the current selection.
|
||||
* @example editor.commands.unsetAllMarks()
|
||||
*/
|
||||
unsetAllMarks: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const unsetAllMarks: RawCommands['unsetAllMarks'] =
|
||||
() =>
|
||||
({ tr, dispatch }) => {
|
||||
const { selection } = tr
|
||||
const { empty, ranges } = selection
|
||||
|
||||
if (empty) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (dispatch) {
|
||||
ranges.forEach(range => {
|
||||
tr.removeMark(range.$from.pos, range.$to.pos)
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
65
node_modules/@tiptap/core/src/commands/unsetMark.ts
generated
vendored
Normal file
65
node_modules/@tiptap/core/src/commands/unsetMark.ts
generated
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
import type { MarkType } from '@tiptap/pm/model'
|
||||
|
||||
import { getMarkRange } from '../helpers/getMarkRange.js'
|
||||
import { getMarkType } from '../helpers/getMarkType.js'
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
unsetMark: {
|
||||
/**
|
||||
* Remove all marks in the current selection.
|
||||
* @param typeOrName The mark type or name.
|
||||
* @param options.extendEmptyMarkRange Removes the mark even across the current selection. Defaults to `false`.
|
||||
* @example editor.commands.unsetMark('bold')
|
||||
*/
|
||||
unsetMark: (
|
||||
/**
|
||||
* The mark type or name.
|
||||
*/
|
||||
typeOrName: string | MarkType,
|
||||
|
||||
options?: {
|
||||
/**
|
||||
* Removes the mark even across the current selection. Defaults to `false`.
|
||||
*/
|
||||
extendEmptyMarkRange?: boolean
|
||||
},
|
||||
) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const unsetMark: RawCommands['unsetMark'] =
|
||||
(typeOrName, options = {}) =>
|
||||
({ tr, state, dispatch }) => {
|
||||
const { extendEmptyMarkRange = false } = options
|
||||
const { selection } = tr
|
||||
const type = getMarkType(typeOrName, state.schema)
|
||||
const { $from, empty, ranges } = selection
|
||||
|
||||
if (!dispatch) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (empty && extendEmptyMarkRange) {
|
||||
let { from, to } = selection
|
||||
const attrs = $from.marks().find(mark => mark.type === type)?.attrs
|
||||
const range = getMarkRange($from, type, attrs)
|
||||
|
||||
if (range) {
|
||||
from = range.from
|
||||
to = range.to
|
||||
}
|
||||
|
||||
tr.removeMark(from, to, type)
|
||||
} else {
|
||||
ranges.forEach(range => {
|
||||
tr.removeMark(range.$from.pos, range.$to.pos, type)
|
||||
})
|
||||
}
|
||||
|
||||
tr.removeStoredMark(type)
|
||||
|
||||
return true
|
||||
}
|
||||
51
node_modules/@tiptap/core/src/commands/unsetTextDirection.ts
generated
vendored
Normal file
51
node_modules/@tiptap/core/src/commands/unsetTextDirection.ts
generated
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { Range, RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
unsetTextDirection: {
|
||||
/**
|
||||
* Remove the text direction attribute from nodes.
|
||||
* If no position is provided, it will use the current selection.
|
||||
* @param position Optional position or range to remove the direction from
|
||||
* @example editor.commands.unsetTextDirection()
|
||||
* @example editor.commands.unsetTextDirection({ from: 0, to: 10 })
|
||||
*/
|
||||
unsetTextDirection: (position?: number | Range) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const unsetTextDirection: RawCommands['unsetTextDirection'] =
|
||||
position =>
|
||||
({ tr, state, dispatch }) => {
|
||||
const { selection } = state
|
||||
let from: number
|
||||
let to: number
|
||||
|
||||
if (typeof position === 'number') {
|
||||
from = position
|
||||
to = position
|
||||
} else if (position && 'from' in position && 'to' in position) {
|
||||
from = position.from
|
||||
to = position.to
|
||||
} else {
|
||||
from = selection.from
|
||||
to = selection.to
|
||||
}
|
||||
|
||||
if (dispatch) {
|
||||
tr.doc.nodesBetween(from, to, (node, pos) => {
|
||||
if (node.isText) {
|
||||
return
|
||||
}
|
||||
|
||||
const newAttrs = { ...node.attrs }
|
||||
|
||||
delete newAttrs.dir
|
||||
|
||||
tr.setNodeMarkup(pos, undefined, newAttrs)
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
150
node_modules/@tiptap/core/src/commands/updateAttributes.ts
generated
vendored
Normal file
150
node_modules/@tiptap/core/src/commands/updateAttributes.ts
generated
vendored
Normal file
@@ -0,0 +1,150 @@
|
||||
import type { Mark, MarkType, Node, NodeType } from '@tiptap/pm/model'
|
||||
import type { SelectionRange } from '@tiptap/pm/state'
|
||||
|
||||
import { getMarkType } from '../helpers/getMarkType.js'
|
||||
import { getNodeType } from '../helpers/getNodeType.js'
|
||||
import { getSchemaTypeNameByName } from '../helpers/getSchemaTypeNameByName.js'
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
updateAttributes: {
|
||||
/**
|
||||
* Update attributes of a node or mark.
|
||||
* @param typeOrName The type or name of the node or mark.
|
||||
* @param attributes The attributes of the node or mark.
|
||||
* @example editor.commands.updateAttributes('mention', { userId: "2" })
|
||||
*/
|
||||
updateAttributes: (
|
||||
/**
|
||||
* The type or name of the node or mark.
|
||||
*/
|
||||
typeOrName: string | NodeType | MarkType,
|
||||
|
||||
/**
|
||||
* The attributes of the node or mark.
|
||||
*/
|
||||
attributes: Record<string, any>,
|
||||
) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const updateAttributes: RawCommands['updateAttributes'] =
|
||||
(typeOrName, attributes = {}) =>
|
||||
({ tr, state, dispatch }) => {
|
||||
let nodeType: NodeType | null = null
|
||||
let markType: MarkType | null = null
|
||||
|
||||
const schemaType = getSchemaTypeNameByName(
|
||||
typeof typeOrName === 'string' ? typeOrName : typeOrName.name,
|
||||
state.schema,
|
||||
)
|
||||
|
||||
if (!schemaType) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (schemaType === 'node') {
|
||||
nodeType = getNodeType(typeOrName as NodeType, state.schema)
|
||||
}
|
||||
|
||||
if (schemaType === 'mark') {
|
||||
markType = getMarkType(typeOrName as MarkType, state.schema)
|
||||
}
|
||||
|
||||
let canUpdate = false
|
||||
|
||||
tr.selection.ranges.forEach((range: SelectionRange) => {
|
||||
const from = range.$from.pos
|
||||
const to = range.$to.pos
|
||||
|
||||
let lastPos: number | undefined
|
||||
let lastNode: Node | undefined
|
||||
let trimmedFrom: number
|
||||
let trimmedTo: number
|
||||
|
||||
if (tr.selection.empty) {
|
||||
state.doc.nodesBetween(from, to, (node: Node, pos: number) => {
|
||||
if (nodeType && nodeType === node.type) {
|
||||
canUpdate = true
|
||||
trimmedFrom = Math.max(pos, from)
|
||||
trimmedTo = Math.min(pos + node.nodeSize, to)
|
||||
lastPos = pos
|
||||
lastNode = node
|
||||
}
|
||||
})
|
||||
} else {
|
||||
state.doc.nodesBetween(from, to, (node: Node, pos: number) => {
|
||||
if (pos < from && nodeType && nodeType === node.type) {
|
||||
canUpdate = true
|
||||
trimmedFrom = Math.max(pos, from)
|
||||
trimmedTo = Math.min(pos + node.nodeSize, to)
|
||||
lastPos = pos
|
||||
lastNode = node
|
||||
}
|
||||
|
||||
if (pos >= from && pos <= to) {
|
||||
if (nodeType && nodeType === node.type) {
|
||||
canUpdate = true
|
||||
|
||||
if (dispatch) {
|
||||
tr.setNodeMarkup(pos, undefined, {
|
||||
...node.attrs,
|
||||
...attributes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (markType && node.marks.length) {
|
||||
node.marks.forEach((mark: Mark) => {
|
||||
if (markType === mark.type) {
|
||||
canUpdate = true
|
||||
|
||||
if (dispatch) {
|
||||
const trimmedFrom2 = Math.max(pos, from)
|
||||
const trimmedTo2 = Math.min(pos + node.nodeSize, to)
|
||||
|
||||
tr.addMark(
|
||||
trimmedFrom2,
|
||||
trimmedTo2,
|
||||
markType.create({
|
||||
...mark.attrs,
|
||||
...attributes,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (lastNode) {
|
||||
if (lastPos !== undefined && dispatch) {
|
||||
tr.setNodeMarkup(lastPos, undefined, {
|
||||
...lastNode.attrs,
|
||||
...attributes,
|
||||
})
|
||||
}
|
||||
|
||||
if (markType && lastNode.marks.length) {
|
||||
lastNode.marks.forEach((mark: Mark) => {
|
||||
if (markType === mark.type && dispatch) {
|
||||
tr.addMark(
|
||||
trimmedFrom,
|
||||
trimmedTo,
|
||||
markType.create({
|
||||
...mark.attrs,
|
||||
...attributes,
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return canUpdate
|
||||
}
|
||||
27
node_modules/@tiptap/core/src/commands/wrapIn.ts
generated
vendored
Normal file
27
node_modules/@tiptap/core/src/commands/wrapIn.ts
generated
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
import { wrapIn as originalWrapIn } from '@tiptap/pm/commands'
|
||||
import type { NodeType } from '@tiptap/pm/model'
|
||||
|
||||
import { getNodeType } from '../helpers/getNodeType.js'
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
wrapIn: {
|
||||
/**
|
||||
* Wraps nodes in another node.
|
||||
* @param typeOrName The type or name of the node.
|
||||
* @param attributes The attributes of the node.
|
||||
* @example editor.commands.wrapIn('blockquote')
|
||||
*/
|
||||
wrapIn: (typeOrName: string | NodeType, attributes?: Record<string, any>) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const wrapIn: RawCommands['wrapIn'] =
|
||||
(typeOrName, attributes = {}) =>
|
||||
({ state, dispatch }) => {
|
||||
const type = getNodeType(typeOrName, state.schema)
|
||||
|
||||
return originalWrapIn(type, attributes)(state, dispatch)
|
||||
}
|
||||
27
node_modules/@tiptap/core/src/commands/wrapInList.ts
generated
vendored
Normal file
27
node_modules/@tiptap/core/src/commands/wrapInList.ts
generated
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { NodeType } from '@tiptap/pm/model'
|
||||
import { wrapInList as originalWrapInList } from '@tiptap/pm/schema-list'
|
||||
|
||||
import { getNodeType } from '../helpers/getNodeType.js'
|
||||
import type { RawCommands } from '../types.js'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
wrapInList: {
|
||||
/**
|
||||
* Wrap a node in a list.
|
||||
* @param typeOrName The type or name of the node.
|
||||
* @param attributes The attributes of the node.
|
||||
* @example editor.commands.wrapInList('bulletList')
|
||||
*/
|
||||
wrapInList: (typeOrName: string | NodeType, attributes?: Record<string, any>) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const wrapInList: RawCommands['wrapInList'] =
|
||||
(typeOrName, attributes = {}) =>
|
||||
({ state, dispatch }) => {
|
||||
const type = getNodeType(typeOrName, state.schema)
|
||||
|
||||
return originalWrapInList(type, attributes)(state, dispatch)
|
||||
}
|
||||
44
node_modules/@tiptap/core/src/extensions/clipboardTextSerializer.ts
generated
vendored
Normal file
44
node_modules/@tiptap/core/src/extensions/clipboardTextSerializer.ts
generated
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
|
||||
import { Extension } from '../Extension.js'
|
||||
import { getTextBetween } from '../helpers/getTextBetween.js'
|
||||
import { getTextSerializersFromSchema } from '../helpers/getTextSerializersFromSchema.js'
|
||||
|
||||
export type ClipboardTextSerializerOptions = {
|
||||
blockSeparator?: string
|
||||
}
|
||||
|
||||
export const ClipboardTextSerializer = Extension.create<ClipboardTextSerializerOptions>({
|
||||
name: 'clipboardTextSerializer',
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
blockSeparator: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey('clipboardTextSerializer'),
|
||||
props: {
|
||||
clipboardTextSerializer: () => {
|
||||
const { editor } = this
|
||||
const { state, schema } = editor
|
||||
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 }
|
||||
|
||||
return getTextBetween(doc, range, {
|
||||
...(this.options.blockSeparator !== undefined ? { blockSeparator: this.options.blockSeparator } : {}),
|
||||
textSerializers,
|
||||
})
|
||||
},
|
||||
},
|
||||
}),
|
||||
]
|
||||
},
|
||||
})
|
||||
14
node_modules/@tiptap/core/src/extensions/commands.ts
generated
vendored
Normal file
14
node_modules/@tiptap/core/src/extensions/commands.ts
generated
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
import * as commands from '../commands/index.js'
|
||||
import { Extension } from '../Extension.js'
|
||||
|
||||
export * from '../commands/index.js'
|
||||
|
||||
export const Commands = Extension.create({
|
||||
name: 'commands',
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
...commands,
|
||||
}
|
||||
},
|
||||
})
|
||||
89
node_modules/@tiptap/core/src/extensions/delete.ts
generated
vendored
Normal file
89
node_modules/@tiptap/core/src/extensions/delete.ts
generated
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
import { RemoveMarkStep } from '@tiptap/pm/transform'
|
||||
|
||||
import { Extension } from '../Extension.js'
|
||||
import { combineTransactionSteps, getChangedRanges } from '../helpers/index.js'
|
||||
|
||||
/**
|
||||
* This extension allows you to be notified when the user deletes content you are interested in.
|
||||
*/
|
||||
export const Delete = Extension.create({
|
||||
name: 'delete',
|
||||
|
||||
onUpdate({ transaction, appendedTransactions }) {
|
||||
const callback = () => {
|
||||
if (
|
||||
this.editor.options.coreExtensionOptions?.delete?.filterTransaction?.(transaction) ??
|
||||
transaction.getMeta('y-sync$')
|
||||
) {
|
||||
return
|
||||
}
|
||||
const nextTransaction = combineTransactionSteps(transaction.before, [transaction, ...appendedTransactions])
|
||||
const changes = getChangedRanges(nextTransaction)
|
||||
|
||||
changes.forEach(change => {
|
||||
if (
|
||||
nextTransaction.mapping.mapResult(change.oldRange.from).deletedAfter &&
|
||||
nextTransaction.mapping.mapResult(change.oldRange.to).deletedBefore
|
||||
) {
|
||||
nextTransaction.before.nodesBetween(change.oldRange.from, change.oldRange.to, (node, from) => {
|
||||
const to = from + node.nodeSize - 2
|
||||
const isFullyWithinRange = change.oldRange.from <= from && to <= change.oldRange.to
|
||||
|
||||
this.editor.emit('delete', {
|
||||
type: 'node',
|
||||
node,
|
||||
from,
|
||||
to,
|
||||
newFrom: nextTransaction.mapping.map(from),
|
||||
newTo: nextTransaction.mapping.map(to),
|
||||
deletedRange: change.oldRange,
|
||||
newRange: change.newRange,
|
||||
partial: !isFullyWithinRange,
|
||||
editor: this.editor,
|
||||
transaction,
|
||||
combinedTransform: nextTransaction,
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const mapping = nextTransaction.mapping
|
||||
nextTransaction.steps.forEach((step, index) => {
|
||||
if (step instanceof RemoveMarkStep) {
|
||||
const newStart = mapping.slice(index).map(step.from, -1)
|
||||
const newEnd = mapping.slice(index).map(step.to)
|
||||
const oldStart = mapping.invert().map(newStart, -1)
|
||||
const oldEnd = mapping.invert().map(newEnd)
|
||||
|
||||
const foundBeforeMark = nextTransaction.doc.nodeAt(newStart - 1)?.marks.some(mark => mark.eq(step.mark))
|
||||
const foundAfterMark = nextTransaction.doc.nodeAt(newEnd)?.marks.some(mark => mark.eq(step.mark))
|
||||
|
||||
this.editor.emit('delete', {
|
||||
type: 'mark',
|
||||
mark: step.mark,
|
||||
from: step.from,
|
||||
to: step.to,
|
||||
deletedRange: {
|
||||
from: oldStart,
|
||||
to: oldEnd,
|
||||
},
|
||||
newRange: {
|
||||
from: newStart,
|
||||
to: newEnd,
|
||||
},
|
||||
partial: Boolean(foundAfterMark || foundBeforeMark),
|
||||
editor: this.editor,
|
||||
transaction,
|
||||
combinedTransform: nextTransaction,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (this.editor.options.coreExtensionOptions?.delete?.async ?? true) {
|
||||
setTimeout(callback, 0)
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
})
|
||||
26
node_modules/@tiptap/core/src/extensions/drop.ts
generated
vendored
Normal file
26
node_modules/@tiptap/core/src/extensions/drop.ts
generated
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
|
||||
import { Extension } from '../Extension.js'
|
||||
|
||||
export const Drop = Extension.create({
|
||||
name: 'drop',
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey('tiptapDrop'),
|
||||
|
||||
props: {
|
||||
handleDrop: (_, e, slice, moved) => {
|
||||
this.editor.emit('drop', {
|
||||
editor: this.editor,
|
||||
event: e,
|
||||
slice,
|
||||
moved,
|
||||
})
|
||||
},
|
||||
},
|
||||
}),
|
||||
]
|
||||
},
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user