2024-01-09 03:21:23 +08:00
|
|
|
|
import {
|
2024-06-04 15:32:54 +08:00
|
|
|
|
MarkType,
|
|
|
|
|
Node as ProseMirrorNode,
|
|
|
|
|
NodeType,
|
|
|
|
|
Schema,
|
2024-01-09 03:21:23 +08:00
|
|
|
|
} from '@tiptap/pm/model'
|
2021-03-30 20:07:18 +08:00
|
|
|
|
import {
|
2023-02-03 00:37:33 +08:00
|
|
|
|
EditorState, Plugin, PluginKey, Transaction,
|
|
|
|
|
} from '@tiptap/pm/state'
|
|
|
|
|
import { EditorView } from '@tiptap/pm/view'
|
2022-06-08 20:10:25 +08:00
|
|
|
|
|
2023-07-01 03:03:49 +08:00
|
|
|
|
import { CommandManager } from './CommandManager.js'
|
|
|
|
|
import { EventEmitter } from './EventEmitter.js'
|
|
|
|
|
import { ExtensionManager } from './ExtensionManager.js'
|
2024-04-08 19:12:40 +08:00
|
|
|
|
import {
|
|
|
|
|
ClipboardTextSerializer, Commands, Editable, FocusEvents, Keymap, Tabindex,
|
|
|
|
|
} from './extensions/index.js'
|
2023-07-01 03:03:49 +08:00
|
|
|
|
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 { resolveFocusPosition } from './helpers/resolveFocusPosition.js'
|
2024-01-09 03:21:23 +08:00
|
|
|
|
import { NodePos } from './NodePos.js'
|
2024-08-22 00:46:49 +08:00
|
|
|
|
import { DropPlugin } from './plugins/DropPlugin.js'
|
|
|
|
|
import { PastePlugin } from './plugins/PastePlugin.js'
|
2023-07-01 03:03:49 +08:00
|
|
|
|
import { style } from './style.js'
|
2021-01-28 16:04:55 +08:00
|
|
|
|
import {
|
|
|
|
|
CanCommands,
|
|
|
|
|
ChainedCommands,
|
2022-06-08 20:10:25 +08:00
|
|
|
|
EditorEvents,
|
|
|
|
|
EditorOptions,
|
2021-11-10 07:24:18 +08:00
|
|
|
|
JSONContent,
|
2021-01-28 16:04:55 +08:00
|
|
|
|
SingleCommands,
|
2021-09-30 15:34:45 +08:00
|
|
|
|
TextSerializer,
|
2023-07-01 03:03:49 +08:00
|
|
|
|
} from './types.js'
|
|
|
|
|
import { createStyleTag } from './utilities/createStyleTag.js'
|
|
|
|
|
import { isFunction } from './utilities/isFunction.js'
|
2020-08-21 05:25:55 +08:00
|
|
|
|
|
2024-04-08 19:12:40 +08:00
|
|
|
|
export * as extensions from './extensions/index.js'
|
2020-11-16 23:58:30 +08:00
|
|
|
|
|
2024-07-25 21:40:07 +08:00
|
|
|
|
// @ts-ignore
|
2024-07-19 20:30:55 +08:00
|
|
|
|
export interface TiptapEditorHTMLElement extends HTMLElement {
|
|
|
|
|
editor?: Editor
|
2020-09-12 00:06:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
2021-09-30 15:25:40 +08:00
|
|
|
|
export class Editor extends EventEmitter<EditorEvents> {
|
2020-09-23 03:25:32 +08:00
|
|
|
|
private commandManager!: CommandManager
|
2020-09-24 06:29:05 +08:00
|
|
|
|
|
2021-01-20 03:27:51 +08:00
|
|
|
|
public extensionManager!: ExtensionManager
|
2020-09-24 06:29:05 +08:00
|
|
|
|
|
2020-08-18 15:36:37 +08:00
|
|
|
|
private css!: HTMLStyleElement
|
2020-09-24 06:29:05 +08:00
|
|
|
|
|
2020-08-18 15:36:37 +08:00
|
|
|
|
public schema!: Schema
|
2020-09-24 06:29:05 +08:00
|
|
|
|
|
2020-08-18 15:36:37 +08:00
|
|
|
|
public view!: EditorView
|
2020-09-24 06:29:05 +08:00
|
|
|
|
|
2020-08-22 04:08:54 +08:00
|
|
|
|
public isFocused = false
|
2020-09-24 06:29:05 +08:00
|
|
|
|
|
2024-08-09 13:56:19 +08:00
|
|
|
|
/**
|
|
|
|
|
* The editor is considered initialized after the `create` event has been emitted.
|
|
|
|
|
*/
|
|
|
|
|
public isInitialized = false
|
|
|
|
|
|
2021-10-22 14:52:54 +08:00
|
|
|
|
public extensionStorage: Record<string, any> = {}
|
|
|
|
|
|
2020-08-18 15:41:31 +08:00
|
|
|
|
public options: EditorOptions = {
|
2020-04-11 20:33:58 +08:00
|
|
|
|
element: document.createElement('div'),
|
2020-03-05 05:40:08 +08:00
|
|
|
|
content: '',
|
|
|
|
|
injectCSS: true,
|
2022-03-07 23:35:06 +08:00
|
|
|
|
injectNonce: undefined,
|
2020-03-06 03:30:58 +08:00
|
|
|
|
extensions: [],
|
2020-11-17 22:47:39 +08:00
|
|
|
|
autofocus: false,
|
2020-08-22 03:53:45 +08:00
|
|
|
|
editable: true,
|
2020-11-18 04:21:19 +08:00
|
|
|
|
editorProps: {},
|
2020-11-18 04:15:10 +08:00
|
|
|
|
parseOptions: {},
|
2024-04-08 19:12:40 +08:00
|
|
|
|
coreExtensionOptions: {},
|
2020-11-21 04:30:12 +08:00
|
|
|
|
enableInputRules: true,
|
|
|
|
|
enablePasteRules: true,
|
2021-09-22 01:20:38 +08:00
|
|
|
|
enableCoreExtensions: true,
|
2024-06-04 15:32:54 +08:00
|
|
|
|
enableContentCheck: false,
|
2021-04-02 06:07:40 +08:00
|
|
|
|
onBeforeCreate: () => null,
|
2020-11-30 20:56:42 +08:00
|
|
|
|
onCreate: () => null,
|
2020-11-17 22:27:00 +08:00
|
|
|
|
onUpdate: () => null,
|
2021-03-09 16:50:03 +08:00
|
|
|
|
onSelectionUpdate: () => null,
|
2020-11-17 22:27:00 +08:00
|
|
|
|
onTransaction: () => null,
|
|
|
|
|
onFocus: () => null,
|
|
|
|
|
onBlur: () => null,
|
2020-11-30 20:56:42 +08:00
|
|
|
|
onDestroy: () => null,
|
2024-06-04 15:32:54 +08:00
|
|
|
|
onContentError: ({ error }) => { throw error },
|
2024-08-22 00:46:49 +08:00
|
|
|
|
onPaste: () => null,
|
|
|
|
|
onDrop: () => null,
|
2020-03-05 05:40:08 +08:00
|
|
|
|
}
|
2020-08-11 22:57:11 +08:00
|
|
|
|
|
2020-04-14 16:13:27 +08:00
|
|
|
|
constructor(options: Partial<EditorOptions> = {}) {
|
2020-03-06 06:59:48 +08:00
|
|
|
|
super()
|
2021-03-05 07:02:28 +08:00
|
|
|
|
this.setOptions(options)
|
2020-03-06 04:49:53 +08:00
|
|
|
|
this.createExtensionManager()
|
2021-02-10 21:52:08 +08:00
|
|
|
|
this.createCommandManager()
|
2020-03-06 04:49:53 +08:00
|
|
|
|
this.createSchema()
|
2021-04-28 03:52:22 +08:00
|
|
|
|
this.on('beforeCreate', this.options.onBeforeCreate)
|
2021-04-02 06:07:40 +08:00
|
|
|
|
this.emit('beforeCreate', { editor: this })
|
2024-06-04 15:32:54 +08:00
|
|
|
|
this.on('contentError', this.options.onContentError)
|
2020-03-06 04:49:53 +08:00
|
|
|
|
this.createView()
|
2020-09-30 23:12:17 +08:00
|
|
|
|
this.injectCSS()
|
2020-11-30 20:56:42 +08:00
|
|
|
|
this.on('create', this.options.onCreate)
|
2020-11-17 22:27:00 +08:00
|
|
|
|
this.on('update', this.options.onUpdate)
|
2021-03-09 16:50:03 +08:00
|
|
|
|
this.on('selectionUpdate', this.options.onSelectionUpdate)
|
2020-11-17 22:27:00 +08:00
|
|
|
|
this.on('transaction', this.options.onTransaction)
|
|
|
|
|
this.on('focus', this.options.onFocus)
|
|
|
|
|
this.on('blur', this.options.onBlur)
|
2020-11-30 20:56:42 +08:00
|
|
|
|
this.on('destroy', this.options.onDestroy)
|
2020-11-17 22:27:00 +08:00
|
|
|
|
|
2024-08-22 00:46:49 +08:00
|
|
|
|
if (this.options.onPaste) {
|
|
|
|
|
this.registerPlugin(PastePlugin(this.options.onPaste))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.options.onDrop) {
|
|
|
|
|
this.registerPlugin(DropPlugin(this.options.onDrop))
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-17 22:27:00 +08:00
|
|
|
|
window.setTimeout(() => {
|
2021-05-28 18:14:12 +08:00
|
|
|
|
if (this.isDestroyed) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-17 22:47:39 +08:00
|
|
|
|
this.commands.focus(this.options.autofocus)
|
2021-03-09 16:50:03 +08:00
|
|
|
|
this.emit('create', { editor: this })
|
2024-08-09 13:56:19 +08:00
|
|
|
|
this.isInitialized = true
|
2020-11-17 22:27:00 +08:00
|
|
|
|
}, 0)
|
2019-12-08 07:16:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
2021-10-22 14:52:54 +08:00
|
|
|
|
/**
|
|
|
|
|
* Returns the editor storage.
|
|
|
|
|
*/
|
|
|
|
|
public get storage(): Record<string, any> {
|
|
|
|
|
return this.extensionStorage
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-13 18:42:04 +08:00
|
|
|
|
/**
|
|
|
|
|
* An object of all registered commands.
|
|
|
|
|
*/
|
2021-01-28 16:04:55 +08:00
|
|
|
|
public get commands(): SingleCommands {
|
2021-10-14 17:56:40 +08:00
|
|
|
|
return this.commandManager.commands
|
2020-11-13 16:58:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
2020-09-23 03:25:32 +08:00
|
|
|
|
/**
|
|
|
|
|
* Create a command chain to call multiple commands at once.
|
|
|
|
|
*/
|
2021-01-28 16:04:55 +08:00
|
|
|
|
public chain(): ChainedCommands {
|
2021-10-14 17:56:40 +08:00
|
|
|
|
return this.commandManager.chain()
|
2020-09-22 00:40:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
2020-11-03 00:18:12 +08:00
|
|
|
|
/**
|
|
|
|
|
* Check if a command or a command chain can be executed. Without executing it.
|
|
|
|
|
*/
|
2021-01-28 16:04:55 +08:00
|
|
|
|
public can(): CanCommands {
|
2021-10-14 17:56:40 +08:00
|
|
|
|
return this.commandManager.can()
|
2020-11-03 00:18:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
2020-09-30 23:12:17 +08:00
|
|
|
|
/**
|
|
|
|
|
* Inject CSS styles.
|
|
|
|
|
*/
|
2021-01-28 16:04:55 +08:00
|
|
|
|
private injectCSS(): void {
|
2020-09-30 23:12:17 +08:00
|
|
|
|
if (this.options.injectCSS && document) {
|
2022-03-07 23:35:06 +08:00
|
|
|
|
this.css = createStyleTag(style, this.options.injectNonce)
|
2020-09-30 23:12:17 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-22 05:35:15 +08:00
|
|
|
|
/**
|
|
|
|
|
* Update editor options.
|
2020-09-03 21:11:55 +08:00
|
|
|
|
*
|
2020-08-22 05:35:15 +08:00
|
|
|
|
* @param options A list of options
|
|
|
|
|
*/
|
2021-01-28 16:04:55 +08:00
|
|
|
|
public setOptions(options: Partial<EditorOptions> = {}): void {
|
2021-08-09 23:24:18 +08:00
|
|
|
|
this.options = {
|
|
|
|
|
...this.options,
|
|
|
|
|
...options,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!this.view || !this.state || this.isDestroyed) {
|
|
|
|
|
return
|
|
|
|
|
}
|
2021-08-09 23:19:50 +08:00
|
|
|
|
|
2021-08-09 23:24:18 +08:00
|
|
|
|
if (this.options.editorProps) {
|
|
|
|
|
this.view.setProps(this.options.editorProps)
|
2021-08-09 23:19:50 +08:00
|
|
|
|
}
|
2021-08-09 23:24:18 +08:00
|
|
|
|
|
|
|
|
|
this.view.updateState(this.state)
|
2021-03-05 07:02:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
2021-03-05 19:15:50 +08:00
|
|
|
|
/**
|
|
|
|
|
* Update editable state of the editor.
|
|
|
|
|
*/
|
2023-01-20 16:55:28 +08:00
|
|
|
|
public setEditable(editable: boolean, emitUpdate = true): void {
|
2021-03-05 07:02:28 +08:00
|
|
|
|
this.setOptions({ editable })
|
2023-01-20 16:55:28 +08:00
|
|
|
|
|
|
|
|
|
if (emitUpdate) {
|
|
|
|
|
this.emit('update', { editor: this, transaction: this.state.tr })
|
|
|
|
|
}
|
2020-08-22 03:53:45 +08:00
|
|
|
|
}
|
2020-09-03 21:11:55 +08:00
|
|
|
|
|
2020-08-22 05:35:15 +08:00
|
|
|
|
/**
|
|
|
|
|
* Returns whether the editor is editable.
|
|
|
|
|
*/
|
2021-01-28 16:04:55 +08:00
|
|
|
|
public get isEditable(): boolean {
|
2021-08-09 23:40:45 +08:00
|
|
|
|
// 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`
|
2023-02-03 00:37:33 +08:00
|
|
|
|
return this.options.editable && this.view && this.view.editable
|
2020-08-22 04:08:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
2020-08-22 05:35:15 +08:00
|
|
|
|
/**
|
|
|
|
|
* Returns the editor state.
|
|
|
|
|
*/
|
2021-01-28 16:04:55 +08:00
|
|
|
|
public get state(): EditorState {
|
2020-03-29 07:21:28 +08:00
|
|
|
|
return this.view.state
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-22 05:35:15 +08:00
|
|
|
|
/**
|
|
|
|
|
* Register a ProseMirror plugin.
|
2020-09-03 21:11:55 +08:00
|
|
|
|
*
|
2020-08-22 05:35:15 +08:00
|
|
|
|
* @param plugin A ProseMirror plugin
|
|
|
|
|
* @param handlePlugins Control how to merge the plugin into the existing plugins.
|
|
|
|
|
*/
|
2023-02-03 00:37:33 +08:00
|
|
|
|
public registerPlugin(
|
|
|
|
|
plugin: Plugin,
|
|
|
|
|
handlePlugins?: (newPlugin: Plugin, plugins: Plugin[]) => Plugin[],
|
|
|
|
|
): void {
|
2021-10-08 21:02:09 +08:00
|
|
|
|
const plugins = isFunction(handlePlugins)
|
2022-06-20 17:45:37 +08:00
|
|
|
|
? handlePlugins(plugin, [...this.state.plugins])
|
2021-01-29 00:39:57 +08:00
|
|
|
|
: [...this.state.plugins, plugin]
|
2020-04-11 03:43:23 +08:00
|
|
|
|
|
2020-04-13 02:17:56 +08:00
|
|
|
|
const state = this.state.reconfigure({ plugins })
|
2020-04-11 03:43:23 +08:00
|
|
|
|
|
|
|
|
|
this.view.updateState(state)
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-22 05:35:15 +08:00
|
|
|
|
/**
|
|
|
|
|
* Unregister a ProseMirror plugin.
|
2020-09-03 21:11:55 +08:00
|
|
|
|
*
|
2021-09-30 15:25:40 +08:00
|
|
|
|
* @param nameOrPluginKey The plugins name
|
2020-08-22 05:35:15 +08:00
|
|
|
|
*/
|
2021-03-30 20:07:18 +08:00
|
|
|
|
public unregisterPlugin(nameOrPluginKey: string | PluginKey): void {
|
|
|
|
|
if (this.isDestroyed) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-03 00:37:33 +08:00
|
|
|
|
// @ts-ignore
|
|
|
|
|
const name = typeof nameOrPluginKey === 'string' ? `${nameOrPluginKey}$` : nameOrPluginKey.key
|
2021-03-30 20:07:18 +08:00
|
|
|
|
|
2020-04-11 04:55:14 +08:00
|
|
|
|
const state = this.state.reconfigure({
|
|
|
|
|
// @ts-ignore
|
2021-03-30 20:07:18 +08:00
|
|
|
|
plugins: this.state.plugins.filter(plugin => !plugin.key.startsWith(name)),
|
2020-04-11 04:55:14 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
this.view.updateState(state)
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-22 05:35:15 +08:00
|
|
|
|
/**
|
|
|
|
|
* Creates an extension manager.
|
|
|
|
|
*/
|
2021-01-28 16:04:55 +08:00
|
|
|
|
private createExtensionManager(): void {
|
2024-04-08 19:12:40 +08:00
|
|
|
|
|
|
|
|
|
const coreExtensions = this.options.enableCoreExtensions ? [
|
|
|
|
|
Editable,
|
|
|
|
|
ClipboardTextSerializer.configure({
|
|
|
|
|
blockSeparator: this.options.coreExtensionOptions?.clipboardTextSerializer?.blockSeparator,
|
|
|
|
|
}),
|
|
|
|
|
Commands,
|
|
|
|
|
FocusEvents,
|
|
|
|
|
Keymap,
|
|
|
|
|
Tabindex,
|
2024-08-20 22:32:44 +08:00
|
|
|
|
].filter(ext => {
|
|
|
|
|
if (typeof this.options.enableCoreExtensions === 'object') {
|
|
|
|
|
return this.options.enableCoreExtensions[ext.name as keyof typeof this.options.enableCoreExtensions] !== false
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}) : []
|
2021-01-29 00:39:57 +08:00
|
|
|
|
const allExtensions = [...coreExtensions, ...this.options.extensions].filter(extension => {
|
2020-12-22 00:43:29 +08:00
|
|
|
|
return ['extension', 'node', 'mark'].includes(extension?.type)
|
|
|
|
|
})
|
2020-10-23 16:44:30 +08:00
|
|
|
|
|
2021-02-17 01:54:44 +08:00
|
|
|
|
this.extensionManager = new ExtensionManager(allExtensions, this)
|
2019-12-08 07:16:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
2020-09-23 03:25:32 +08:00
|
|
|
|
/**
|
|
|
|
|
* Creates an command manager.
|
|
|
|
|
*/
|
2021-01-28 16:04:55 +08:00
|
|
|
|
private createCommandManager(): void {
|
2021-10-14 17:56:40 +08:00
|
|
|
|
this.commandManager = new CommandManager({
|
|
|
|
|
editor: this,
|
|
|
|
|
})
|
2020-09-23 03:25:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
2020-08-22 05:35:15 +08:00
|
|
|
|
/**
|
|
|
|
|
* Creates a ProseMirror schema.
|
|
|
|
|
*/
|
2021-01-28 16:04:55 +08:00
|
|
|
|
private createSchema(): void {
|
2020-09-10 06:09:05 +08:00
|
|
|
|
this.schema = this.extensionManager.schema
|
2020-03-06 04:05:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
2020-08-22 05:35:15 +08:00
|
|
|
|
/**
|
|
|
|
|
* Creates a ProseMirror view.
|
|
|
|
|
*/
|
2021-01-28 16:04:55 +08:00
|
|
|
|
private createView(): void {
|
2024-06-04 15:32:54 +08:00
|
|
|
|
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: () => {
|
|
|
|
|
// 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 },
|
|
|
|
|
)
|
|
|
|
|
}
|
2021-12-03 17:35:17 +08:00
|
|
|
|
const selection = resolveFocusPosition(doc, this.options.autofocus)
|
2021-12-03 17:36:51 +08:00
|
|
|
|
|
2020-04-11 20:33:58 +08:00
|
|
|
|
this.view = new EditorView(this.options.element, {
|
2020-11-18 04:21:19 +08:00
|
|
|
|
...this.options.editorProps,
|
2020-11-04 22:31:42 +08:00
|
|
|
|
dispatchTransaction: this.dispatchTransaction.bind(this),
|
2020-03-06 04:49:53 +08:00
|
|
|
|
state: EditorState.create({
|
2021-12-03 17:35:17 +08:00
|
|
|
|
doc,
|
2022-06-20 17:45:37 +08:00
|
|
|
|
selection: selection || undefined,
|
2020-03-06 04:49:53 +08:00
|
|
|
|
}),
|
2020-10-30 18:08:23 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// `editor.view` is not yet available at this time.
|
2020-11-04 22:31:42 +08:00
|
|
|
|
// Therefore we will add all plugins and node views directly afterwards.
|
|
|
|
|
const newState = this.state.reconfigure({
|
2021-04-08 04:07:36 +08:00
|
|
|
|
plugins: this.extensionManager.plugins,
|
2020-11-04 22:31:42 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
this.view.updateState(newState)
|
|
|
|
|
|
2020-12-14 19:05:46 +08:00
|
|
|
|
this.createNodeViews()
|
2023-05-25 19:45:06 +08:00
|
|
|
|
this.prependClass()
|
2020-09-12 00:06:13 +08:00
|
|
|
|
|
2020-10-30 18:08:23 +08:00
|
|
|
|
// Let’s store the editor instance in the DOM element.
|
|
|
|
|
// So we’ll have access to it for tests.
|
2024-07-25 21:40:07 +08:00
|
|
|
|
// @ts-ignore
|
2024-07-19 20:30:55 +08:00
|
|
|
|
const dom = this.view.dom as TiptapEditorHTMLElement
|
2021-12-03 07:03:39 +08:00
|
|
|
|
|
2021-02-17 01:54:44 +08:00
|
|
|
|
dom.editor = this
|
2019-12-08 07:16:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
2020-12-14 19:05:46 +08:00
|
|
|
|
/**
|
|
|
|
|
* Creates all node views.
|
|
|
|
|
*/
|
2021-01-28 16:04:55 +08:00
|
|
|
|
public createNodeViews(): void {
|
2024-07-22 20:38:25 +08:00
|
|
|
|
if (this.view.isDestroyed) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-14 19:05:46 +08:00
|
|
|
|
this.view.setProps({
|
|
|
|
|
nodeViews: this.extensionManager.nodeViews,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-25 19:45:06 +08:00
|
|
|
|
/**
|
|
|
|
|
* Prepend class name to element.
|
|
|
|
|
*/
|
|
|
|
|
public prependClass(): void {
|
|
|
|
|
this.view.dom.className = `tiptap ${this.view.dom.className}`
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-09 17:06:13 +08:00
|
|
|
|
public isCapturingTransaction = false
|
|
|
|
|
|
|
|
|
|
private capturedTransaction: Transaction | null = null
|
|
|
|
|
|
|
|
|
|
public captureTransaction(fn: Function) {
|
|
|
|
|
this.isCapturingTransaction = true
|
|
|
|
|
fn()
|
|
|
|
|
this.isCapturingTransaction = false
|
|
|
|
|
|
|
|
|
|
const tr = this.capturedTransaction
|
|
|
|
|
|
|
|
|
|
this.capturedTransaction = null
|
|
|
|
|
|
|
|
|
|
return tr
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-22 05:35:15 +08:00
|
|
|
|
/**
|
|
|
|
|
* The callback over which to send transactions (state updates) produced by the view.
|
2020-09-03 21:11:55 +08:00
|
|
|
|
*
|
2020-08-22 05:35:15 +08:00
|
|
|
|
* @param transaction An editor state transaction
|
|
|
|
|
*/
|
2021-01-28 16:04:55 +08:00
|
|
|
|
private dispatchTransaction(transaction: Transaction): void {
|
2023-02-28 17:50:43 +08:00
|
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-09 17:06:13 +08:00
|
|
|
|
if (this.isCapturingTransaction) {
|
|
|
|
|
if (!this.capturedTransaction) {
|
|
|
|
|
this.capturedTransaction = transaction
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
transaction.steps.forEach(step => this.capturedTransaction?.step(step))
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-05 05:40:08 +08:00
|
|
|
|
const state = this.state.apply(transaction)
|
2020-11-27 21:52:19 +08:00
|
|
|
|
const selectionHasChanged = !this.state.selection.eq(state.selection)
|
2020-11-17 22:27:00 +08:00
|
|
|
|
|
2024-07-22 19:18:03 +08:00
|
|
|
|
this.emit('beforeTransaction', {
|
|
|
|
|
editor: this,
|
|
|
|
|
transaction,
|
|
|
|
|
nextState: state,
|
|
|
|
|
})
|
2020-03-05 05:40:08 +08:00
|
|
|
|
this.view.updateState(state)
|
2021-03-09 16:50:03 +08:00
|
|
|
|
this.emit('transaction', {
|
|
|
|
|
editor: this,
|
|
|
|
|
transaction,
|
|
|
|
|
})
|
2020-08-11 22:57:11 +08:00
|
|
|
|
|
2020-11-27 21:52:19 +08:00
|
|
|
|
if (selectionHasChanged) {
|
2021-03-09 16:50:03 +08:00
|
|
|
|
this.emit('selectionUpdate', {
|
|
|
|
|
editor: this,
|
2021-08-13 20:00:50 +08:00
|
|
|
|
transaction,
|
2021-03-09 16:50:03 +08:00
|
|
|
|
})
|
2020-11-27 21:52:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
2020-11-17 22:27:00 +08:00
|
|
|
|
const focus = transaction.getMeta('focus')
|
|
|
|
|
const blur = transaction.getMeta('blur')
|
|
|
|
|
|
|
|
|
|
if (focus) {
|
2021-03-09 16:50:03 +08:00
|
|
|
|
this.emit('focus', {
|
|
|
|
|
editor: this,
|
|
|
|
|
event: focus.event,
|
2021-08-13 20:00:50 +08:00
|
|
|
|
transaction,
|
2021-03-09 16:50:03 +08:00
|
|
|
|
})
|
2020-11-17 22:27:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (blur) {
|
2021-03-09 16:50:03 +08:00
|
|
|
|
this.emit('blur', {
|
|
|
|
|
editor: this,
|
|
|
|
|
event: blur.event,
|
2021-08-13 20:00:50 +08:00
|
|
|
|
transaction,
|
2021-03-09 16:50:03 +08:00
|
|
|
|
})
|
2020-11-17 22:27:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
2019-12-08 07:16:44 +08:00
|
|
|
|
if (!transaction.docChanged || transaction.getMeta('preventUpdate')) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-09 16:50:03 +08:00
|
|
|
|
this.emit('update', {
|
|
|
|
|
editor: this,
|
|
|
|
|
transaction,
|
|
|
|
|
})
|
2019-12-08 07:16:44 +08:00
|
|
|
|
}
|
2019-12-17 06:51:18 +08:00
|
|
|
|
|
2021-05-07 17:10:18 +08:00
|
|
|
|
/**
|
|
|
|
|
* Get attributes of the currently selected node or mark.
|
|
|
|
|
*/
|
|
|
|
|
public getAttributes(nameOrType: string | NodeType | MarkType): Record<string, any> {
|
|
|
|
|
return getAttributes(this.state, nameOrType)
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-22 05:35:15 +08:00
|
|
|
|
/**
|
|
|
|
|
* Returns if the currently selected node or mark is active.
|
2020-09-03 21:11:55 +08:00
|
|
|
|
*
|
2020-08-22 05:35:15 +08:00
|
|
|
|
* @param name Name of the node or mark
|
2020-11-30 07:04:30 +08:00
|
|
|
|
* @param attributes Attributes of the node or mark
|
2020-08-22 05:35:15 +08:00
|
|
|
|
*/
|
2023-02-03 00:37:33 +08:00
|
|
|
|
public isActive(name: string, attributes?: {}): boolean
|
|
|
|
|
public isActive(attributes: {}): boolean
|
2020-11-30 07:04:30 +08:00
|
|
|
|
public isActive(nameOrAttributes: string, attributesOrUndefined?: {}): boolean {
|
2023-02-03 00:37:33 +08:00
|
|
|
|
const name = typeof nameOrAttributes === 'string' ? nameOrAttributes : null
|
2020-11-30 07:04:30 +08:00
|
|
|
|
|
2023-02-03 00:37:33 +08:00
|
|
|
|
const attributes = typeof nameOrAttributes === 'string' ? attributesOrUndefined : nameOrAttributes
|
2020-11-30 07:04:30 +08:00
|
|
|
|
|
|
|
|
|
return isActive(this.state, name, attributes)
|
2020-03-06 04:49:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
2020-08-22 05:35:15 +08:00
|
|
|
|
/**
|
|
|
|
|
* Get the document as JSON.
|
|
|
|
|
*/
|
2021-11-10 07:24:18 +08:00
|
|
|
|
public getJSON(): JSONContent {
|
2020-03-04 17:21:48 +08:00
|
|
|
|
return this.state.doc.toJSON()
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-22 05:35:15 +08:00
|
|
|
|
/**
|
|
|
|
|
* Get the document as HTML.
|
|
|
|
|
*/
|
2021-01-28 16:04:55 +08:00
|
|
|
|
public getHTML(): string {
|
2021-09-29 03:34:57 +08:00
|
|
|
|
return getHTMLFromFragment(this.state.doc.content, this.schema)
|
2020-03-04 17:21:48 +08:00
|
|
|
|
}
|
2020-03-05 04:45:49 +08:00
|
|
|
|
|
2021-09-10 05:51:05 +08:00
|
|
|
|
/**
|
|
|
|
|
* Get the document as text.
|
|
|
|
|
*/
|
|
|
|
|
public getText(options?: {
|
2023-02-03 00:37:33 +08:00
|
|
|
|
blockSeparator?: string
|
|
|
|
|
textSerializers?: Record<string, TextSerializer>
|
2021-09-10 05:51:05 +08:00
|
|
|
|
}): string {
|
2023-02-03 00:37:33 +08:00
|
|
|
|
const { blockSeparator = '\n\n', textSerializers = {} } = options || {}
|
2021-09-10 05:51:05 +08:00
|
|
|
|
|
|
|
|
|
return getText(this.state.doc, {
|
|
|
|
|
blockSeparator,
|
|
|
|
|
textSerializers: {
|
2022-05-03 04:01:07 +08:00
|
|
|
|
...getTextSerializersFromSchema(this.schema),
|
2023-02-19 01:26:49 +08:00
|
|
|
|
...textSerializers,
|
2021-09-10 05:51:05 +08:00
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-23 20:28:25 +08:00
|
|
|
|
/**
|
|
|
|
|
* Check if there is no content.
|
|
|
|
|
*/
|
2021-03-25 05:23:08 +08:00
|
|
|
|
public get isEmpty(): boolean {
|
|
|
|
|
return isNodeEmpty(this.state.doc)
|
2020-10-23 20:28:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
2021-01-27 18:40:49 +08:00
|
|
|
|
/**
|
|
|
|
|
* Get the number of characters for the current document.
|
2021-12-09 04:26:30 +08:00
|
|
|
|
*
|
|
|
|
|
* @deprecated
|
2021-01-27 18:40:49 +08:00
|
|
|
|
*/
|
2021-01-28 16:04:55 +08:00
|
|
|
|
public getCharacterCount(): number {
|
2023-02-03 00:37:33 +08:00
|
|
|
|
console.warn(
|
|
|
|
|
'[tiptap warn]: "editor.getCharacterCount()" is deprecated. Please use "editor.storage.characterCount.characters()" instead.',
|
|
|
|
|
)
|
2021-12-09 04:26:30 +08:00
|
|
|
|
|
2021-01-27 18:40:49 +08:00
|
|
|
|
return this.state.doc.content.size - 2
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-22 05:35:15 +08:00
|
|
|
|
/**
|
|
|
|
|
* Destroy the editor.
|
|
|
|
|
*/
|
2021-01-28 16:04:55 +08:00
|
|
|
|
public destroy(): void {
|
2020-11-30 20:50:06 +08:00
|
|
|
|
this.emit('destroy')
|
|
|
|
|
|
2020-03-31 19:07:57 +08:00
|
|
|
|
if (this.view) {
|
|
|
|
|
this.view.destroy()
|
2020-03-05 04:45:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
2020-03-06 06:59:48 +08:00
|
|
|
|
this.removeAllListeners()
|
2020-03-05 04:45:49 +08:00
|
|
|
|
}
|
2020-09-24 06:29:05 +08:00
|
|
|
|
|
2020-10-01 04:43:58 +08:00
|
|
|
|
/**
|
|
|
|
|
* Check if the editor is already destroyed.
|
|
|
|
|
*/
|
2021-03-05 07:02:28 +08:00
|
|
|
|
public get isDestroyed(): boolean {
|
2020-10-01 04:43:58 +08:00
|
|
|
|
// @ts-ignore
|
|
|
|
|
return !this.view?.docView
|
|
|
|
|
}
|
2024-01-09 03:21:23 +08:00
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
2019-12-08 07:16:44 +08:00
|
|
|
|
}
|