From cb6e0dd7e972f8c3779b57d75758bd85c9dbb73d Mon Sep 17 00:00:00 2001 From: yanmao <55792257+yanmao-cc@users.noreply.github.com> Date: Thu, 30 Dec 2021 17:49:32 +0800 Subject: [PATCH] update --- docs/plugin/plugin-mention.md | 4 +- docs/plugin/plugin-mention.zh-CN.md | 4 +- packages/engine/src/card/entry.ts | 10 +- packages/engine/src/card/index.ts | 4 +- packages/engine/src/card/maximize/index.ts | 9 +- packages/engine/src/card/resize/index.ts | 2 +- packages/engine/src/card/toolbar/index.ts | 8 +- packages/engine/src/card/typing/down.ts | 24 +- packages/engine/src/card/typing/left.ts | 11 + packages/engine/src/card/typing/right.ts | 11 + packages/engine/src/card/typing/up.ts | 24 +- packages/engine/src/clipboard.ts | 3 +- packages/engine/src/command.ts | 4 +- packages/engine/src/editor.ts | 206 ++++++++ packages/engine/src/engine/index.ts | 189 +------- packages/engine/src/index.ts | 2 + packages/engine/src/inline/index.ts | 11 +- packages/engine/src/node/event.ts | 8 + packages/engine/src/parser/index.ts | 2 +- packages/engine/src/plugin/base.ts | 5 +- packages/engine/src/plugin/block.ts | 7 +- packages/engine/src/plugin/element.ts | 8 +- packages/engine/src/plugin/index.ts | 46 +- packages/engine/src/plugin/inline.ts | 7 +- packages/engine/src/plugin/list/index.ts | 6 +- packages/engine/src/plugin/mark.ts | 7 +- packages/engine/src/range.ts | 2 +- packages/engine/src/types/block.ts | 5 +- packages/engine/src/types/card.ts | 29 +- packages/engine/src/types/editor.ts | 433 +++++++++++++++++ packages/engine/src/types/engine.ts | 444 +----------------- packages/engine/src/types/index.ts | 1 + packages/engine/src/types/inline.ts | 9 +- packages/engine/src/types/list.ts | 4 +- packages/engine/src/types/mark.ts | 9 +- packages/engine/src/types/node.ts | 4 + packages/engine/src/types/plugin.ts | 31 +- packages/engine/src/types/range.ts | 2 +- packages/engine/src/types/typing.ts | 13 +- packages/engine/src/types/view.ts | 44 +- .../engine/src/typing/keydown/backspace.ts | 33 +- packages/engine/src/typing/keydown/default.ts | 14 +- packages/engine/src/typing/keydown/delete.ts | 32 +- packages/engine/src/typing/keydown/enter.ts | 32 +- packages/engine/src/typing/keydown/left.ts | 33 +- packages/engine/src/typing/keydown/right.ts | 41 +- .../engine/src/typing/keydown/shift-enter.ts | 31 +- packages/engine/src/typing/keydown/tab.ts | 28 +- packages/engine/src/typing/keyup/backspace.ts | 31 +- packages/engine/src/utils/index.ts | 9 +- packages/engine/src/view.ts | 168 +------ .../toolbar-vue/src/plugin/component/popup.ts | 4 +- packages/toolbar-vue/src/plugin/index.ts | 21 +- .../toolbar/src/plugin/component/popup.tsx | 11 +- packages/toolbar/src/plugin/index.ts | 22 +- plugins/alignment/src/index.ts | 10 +- plugins/mark-range/src/index.ts | 66 ++- plugins/mention/README.md | 4 +- plugins/mention/src/component/collapse.ts | 11 +- plugins/mention/src/component/index.ts | 14 +- plugins/status/src/components/index.ts | 8 +- plugins/table/src/component/helper.ts | 20 +- plugins/table/src/component/index.ts | 49 +- plugins/table/src/index.ts | 15 +- plugins/table/src/types.ts | 17 +- 65 files changed, 1143 insertions(+), 1233 deletions(-) create mode 100644 packages/engine/src/editor.ts create mode 100644 packages/engine/src/types/editor.ts diff --git a/docs/plugin/plugin-mention.md b/docs/plugin/plugin-mention.md index 6f66a8ef..532cdbfd 100644 --- a/docs/plugin/plugin-mention.md +++ b/docs/plugin/plugin-mention.md @@ -195,9 +195,7 @@ this.engine.on('mention:render-item', (data, root) => { `mention:loading`: custom rendering loading status ```ts -this.engine.on('mention:loading', (data, root) => { - root.html(`
${data}
`); - // or +this.engine.on('mention:loading', (root) => { ReactDOM.render(
Loading...
, root.get()!, diff --git a/docs/plugin/plugin-mention.zh-CN.md b/docs/plugin/plugin-mention.zh-CN.md index a81ec0f4..99e8d9ae 100644 --- a/docs/plugin/plugin-mention.zh-CN.md +++ b/docs/plugin/plugin-mention.zh-CN.md @@ -195,9 +195,7 @@ this.engine.on('mention:render-item', (data, root) => { `mention:loading`: 自定渲染加载状态 ```ts -this.engine.on('mention:loading', (data, root) => { - root.html(`
${data}
`); - // or +this.engine.on('mention:loading', (root) => { ReactDOM.render(
Loading...
, root.get()!, diff --git a/packages/engine/src/card/entry.ts b/packages/engine/src/card/entry.ts index e7e441b1..79c2d28d 100644 --- a/packages/engine/src/card/entry.ts +++ b/packages/engine/src/card/entry.ts @@ -18,7 +18,7 @@ import { ResizeInterface, CardValue, } from '../types/card'; -import { EditorInterface } from '../types/engine'; +import { EditorInterface } from '../types/editor'; import { NodeInterface } from '../types/node'; import { RangeInterface } from '../types/range'; import { ToolbarItemOptions } from '../types/toolbar'; @@ -31,7 +31,9 @@ import { $ } from '../node'; import { CardType, SelectStyleType } from './enum'; import { DATA_ELEMENT, UI } from '../constants'; -abstract class CardEntry implements CardInterface { +abstract class CardEntry + implements CardInterface +{ protected readonly editor: EditorInterface; readonly root: NodeInterface; toolbarModel?: CardToolbarInterface; @@ -323,6 +325,10 @@ abstract class CardEntry implements CardInterface { else this.root.removeClass(className); return center; } + onSelectLeft?(event: KeyboardEvent): boolean | void; + onSelectRight?(event: KeyboardEvent): boolean | void; + onSelectUp?(event: KeyboardEvent): boolean | void; + onSelectDown?(event: KeyboardEvent): boolean | void; onActivate(activated: boolean) { if (!this.resize) return; if (activated) this.resizeModel?.show(); diff --git a/packages/engine/src/card/index.ts b/packages/engine/src/card/index.ts index 35110846..0abfd9ba 100644 --- a/packages/engine/src/card/index.ts +++ b/packages/engine/src/card/index.ts @@ -21,7 +21,7 @@ import { } from '../types/card'; import { NodeInterface } from '../types/node'; import { RangeInterface } from '../types/range'; -import { EditorInterface } from '../types/engine'; +import { EditorInterface } from '../types/editor'; import { decodeCardValue, encodeCardValue, @@ -104,7 +104,7 @@ class CardModel implements CardModelInterface { cards.forEach((card) => { this.classes[card.cardName] = card; }); - + if (!this.lazyRender) return; window.addEventListener('resize', this.renderAsyncComponents); this.editor.scrollNode ?.get() diff --git a/packages/engine/src/card/maximize/index.ts b/packages/engine/src/card/maximize/index.ts index 14656c79..b0fa5f09 100644 --- a/packages/engine/src/card/maximize/index.ts +++ b/packages/engine/src/card/maximize/index.ts @@ -1,6 +1,9 @@ -import { NodeInterface } from '../../types/node'; -import { CardInterface, MaximizeInterface } from '../../types/card'; -import { EditorInterface } from '../../types/engine'; +import { + CardInterface, + MaximizeInterface, + EditorInterface, + NodeInterface, +} from '../../types'; import { $ } from '../../node'; import { DATA_ELEMENT, DATA_TRANSIENT_ELEMENT, UI } from '../../constants'; import { isEngine } from '../../utils'; diff --git a/packages/engine/src/card/resize/index.ts b/packages/engine/src/card/resize/index.ts index 5847aa0e..51c2e96e 100644 --- a/packages/engine/src/card/resize/index.ts +++ b/packages/engine/src/card/resize/index.ts @@ -63,7 +63,7 @@ class Resize implements ResizeInterface { if (start) { this.card.setValue({ height: container.height(), - }); + } as any); start = false; } }, diff --git a/packages/engine/src/card/toolbar/index.ts b/packages/engine/src/card/toolbar/index.ts index 131a187e..6b147607 100644 --- a/packages/engine/src/card/toolbar/index.ts +++ b/packages/engine/src/card/toolbar/index.ts @@ -1,15 +1,13 @@ import Toolbar, { Tooltip } from '../../toolbar'; -import { +import type { + EditorInterface, CardEntry, CardInterface, CardToolbarInterface, CardToolbarItemOptions, -} from '../../types/card'; -import { ToolbarItemOptions, ToolbarInterface as ToolbarBaseInterface, -} from '../../types/toolbar'; -import { EditorInterface } from '../../types/engine'; +} from '../../types'; import { DATA_ELEMENT, TRIGGER_CARD_ID, UI } from '../../constants'; import { $ } from '../../node'; import { isEngine, isMobile } from '../../utils'; diff --git a/packages/engine/src/card/typing/down.ts b/packages/engine/src/card/typing/down.ts index df6fbd0c..0c23e85c 100644 --- a/packages/engine/src/card/typing/down.ts +++ b/packages/engine/src/card/typing/down.ts @@ -28,16 +28,28 @@ class Down { } trigger(event: KeyboardEvent) { - const { change } = this.engine; + const { change, block, card } = this.engine; const range = change.range.get(); - const card = this.engine.card.getSingleCard(range); - if (!card) return true; + const singleCard = card.getSingleCard(range); + if (!singleCard) { + if (range.collapsed) { + const closetBlock = block.closest(range.startNode); + const next = closetBlock.next(); + if (next?.isCard()) { + const cardComponent = card.find(next); + if (cardComponent && cardComponent.onSelectDown) { + return cardComponent.onSelectDown(event); + } + } + } + return true; + } if (isHotkey('shift+down', event)) { return true; } - return card.type === CardType.INLINE - ? this.inline(card, event) - : this.block(card, event); + return singleCard.type === CardType.INLINE + ? this.inline(singleCard, event) + : this.block(singleCard, event); } } export default Down; diff --git a/packages/engine/src/card/typing/left.ts b/packages/engine/src/card/typing/left.ts index 284a58e1..590c313c 100644 --- a/packages/engine/src/card/typing/left.ts +++ b/packages/engine/src/card/typing/left.ts @@ -37,6 +37,11 @@ class Left { event.preventDefault(); if (isCenter) { card.select(false); + } else if (range.collapsed) { + const cardComponent = this.engine.card.find(range.startNode); + if (cardComponent && cardComponent.onSelectLeft) { + return cardComponent.onSelectLeft(event); + } } if (!isCenter && singleSelectable !== false) { this.engine.card.select(card); @@ -63,6 +68,12 @@ class Left { // 右侧光标 const cardRight = range.commonAncestorNode.closest(CARD_RIGHT_SELECTOR); if (cardRight.length > 0) { + if (range.collapsed) { + const cardComponent = card.find(range.startNode); + if (cardComponent && cardComponent.onSelectLeft) { + return cardComponent.onSelectLeft(event); + } + } event.preventDefault(); card.select(component); return false; diff --git a/packages/engine/src/card/typing/right.ts b/packages/engine/src/card/typing/right.ts index 527375d7..3e2b347d 100644 --- a/packages/engine/src/card/typing/right.ts +++ b/packages/engine/src/card/typing/right.ts @@ -21,6 +21,11 @@ class Right { event.preventDefault(); if (isCenter) { card.select(false); + } else if (range.collapsed) { + const cardComponent = this.engine.card.find(range.startNode); + if (cardComponent && cardComponent.onSelectRight) { + return cardComponent.onSelectRight(event); + } } if (!isCenter && singleSelectable !== false) { this.engine.card.select(card); @@ -51,6 +56,12 @@ class Right { // 左侧光标 const cardLeft = range.commonAncestorNode.closest(CARD_LEFT_SELECTOR); if (cardLeft.length > 0) { + if (range.collapsed) { + const cardComponent = this.engine.card.find(range.startNode); + if (cardComponent && cardComponent.onSelectRight) { + return cardComponent.onSelectRight(event); + } + } event.preventDefault(); card.select(component); return false; diff --git a/packages/engine/src/card/typing/up.ts b/packages/engine/src/card/typing/up.ts index 0bd2845b..e67a38c5 100644 --- a/packages/engine/src/card/typing/up.ts +++ b/packages/engine/src/card/typing/up.ts @@ -27,16 +27,28 @@ class Up { } trigger(event: KeyboardEvent) { - const { change } = this.engine; + const { change, card, block } = this.engine; const range = change.range.get(); - const card = this.engine.card.getSingleCard(range); - if (!card) return true; + const singleCard = card.getSingleCard(range); + if (!singleCard) { + if (range.collapsed) { + const closetBlock = block.closest(range.startNode); + const prev = closetBlock.prev(); + if (prev?.isCard()) { + const cardComponent = card.find(prev); + if (cardComponent && cardComponent.onSelectUp) { + return cardComponent.onSelectUp(event); + } + } + } + return true; + } if (isHotkey('shift+up', event)) { return; } - return card.type === CardType.INLINE - ? this.inline(card, event) - : this.block(card, event); + return singleCard.type === CardType.INLINE + ? this.inline(singleCard, event) + : this.block(singleCard, event); } } export default Up; diff --git a/packages/engine/src/clipboard.ts b/packages/engine/src/clipboard.ts index b50ef642..8102b10a 100644 --- a/packages/engine/src/clipboard.ts +++ b/packages/engine/src/clipboard.ts @@ -1,7 +1,6 @@ import copyTo from 'copy-to-clipboard'; import Parser from './parser'; -import { ClipboardInterface } from './types/clipboard'; -import { EditorInterface, EngineInterface } from './types/engine'; +import { EditorInterface, EngineInterface, ClipboardInterface } from './types'; import { RangeInterface } from './types/range'; import { isEngine, isSafari } from './utils'; import { $ } from './node'; diff --git a/packages/engine/src/command.ts b/packages/engine/src/command.ts index e49e7263..550b3813 100644 --- a/packages/engine/src/command.ts +++ b/packages/engine/src/command.ts @@ -1,7 +1,5 @@ import { isMarkPlugin } from './plugin'; -import { ChangeInterface } from './types'; -import { CommandInterface } from './types/command'; -import { EditorInterface } from './types/engine'; +import { ChangeInterface, EditorInterface, CommandInterface } from './types'; import { isEngine } from './utils'; /** diff --git a/packages/engine/src/editor.ts b/packages/engine/src/editor.ts new file mode 100644 index 00000000..d6c0454f --- /dev/null +++ b/packages/engine/src/editor.ts @@ -0,0 +1,206 @@ +import { merge } from 'lodash'; +import Language from './language'; +import { + BlockModelInterface, + CardEntry, + CardInterface, + CardModelInterface, + CardValue, + ClipboardInterface, + CommandInterface, + ConversionInterface, + EditorInterface, + EditorOptions, + EventInterface, + EventListener, + InlineModelInterface, + LanguageInterface, + MarkModelInterface, + NodeIdInterface, + NodeInterface, + NodeModelInterface, + PluginEntry, + PluginModelInterface, + RequestInterface, + SchemaInterface, + Selector, +} from './types'; +import { ListModelInterface } from './types/list'; +import language from './locales'; +import NodeModel, { $, Event } from './node'; +import Command from './command'; +import Plugin from './plugin'; +import Schema from './schema'; +import schemaDefaultData from './constants/schema'; +import Conversion from './parser/conversion'; +import conversionDefault from './constants/conversion'; +import CardModel from './card'; +import NodeId from './node/id'; +import Clipboard from './clipboard'; +import Request from './request'; +import List from './list'; +import Mark from './mark'; +import Inline from './inline'; +import Block from './block'; + +class Editor + implements EditorInterface +{ + readonly kind: 'editor' | 'engine' | 'view' = 'editor'; + options: T = { + lang: 'zh-CN', + locale: {}, + plugins: [] as PluginEntry[], + cards: [] as CardEntry[], + config: {}, + } as T; + readonly container: NodeInterface; + + language: LanguageInterface; + root: NodeInterface; + card: CardModelInterface; + plugin: PluginModelInterface; + node: NodeModelInterface; + nodeId: NodeIdInterface; + list: ListModelInterface; + mark: MarkModelInterface; + inline: InlineModelInterface; + block: BlockModelInterface; + event: EventInterface; + schema: SchemaInterface; + conversion: ConversionInterface; + command: CommandInterface; + clipboard: ClipboardInterface; + request: RequestInterface; + #_scrollNode: NodeInterface | null = null; + + get scrollNode(): NodeInterface | null { + if (this.#_scrollNode) return this.#_scrollNode; + const { scrollNode } = this.options; + let sn = scrollNode + ? typeof scrollNode === 'function' + ? scrollNode() + : scrollNode + : null; + // 查找父级样式 overflow 或者 overflow-y 为 auto 或者 scroll 的节点 + const targetValues = ['auto', 'scroll']; + let parent = this.container.parent(); + while (!sn && parent && parent.length > 0 && parent.name !== 'body') { + if ( + targetValues.includes(parent.css('overflow')) || + targetValues.includes(parent.css('overflow-y')) + ) { + sn = parent.get(); + break; + } else { + parent = parent.parent(); + } + } + if (sn === null) sn = document.documentElement; + this.#_scrollNode = sn ? $(sn) : null; + return this.#_scrollNode; + } + + constructor(selector: Selector, options?: EditorOptions) { + this.options = { ...this.options, ...options }; + this.container = $(selector); + // 多语言 + this.language = new Language( + this.options.lang || 'zh-CN', + merge(language, options?.locale), + ); + // 事件管理 + this.event = new Event(); + // 命令 + this.command = new Command(this); + // 节点规则 + this.schema = new Schema(); + this.schema.add(schemaDefaultData); + // 节点转换规则 + this.conversion = new Conversion(this); + conversionDefault.forEach((rule) => + this.conversion.add(rule.from, rule.to), + ); + // 卡片 + this.card = new CardModel(this, this.options.lazyRender); + // 剪贴板 + this.clipboard = new Clipboard(this); + // http请求 + this.request = new Request(); + // 插件 + this.plugin = new Plugin(this); + // 节点管理 + this.node = new NodeModel(this); + this.nodeId = new NodeId(this); + // 列表 + this.list = new List(this); + // 样式标记 + this.mark = new Mark(this); + // 行内样式 + this.inline = new Inline(this); + // 块级节点 + this.block = new Block(this); + // 编辑器父节点 + this.root = $( + this.options.root || this.container.parent() || document.body, + ); + const rootPosition = this.root.css('position'); + if (!rootPosition || rootPosition === 'static') + this.root.css('position', 'relative'); + } + + init() { + // 实例化插件 + this.mark.init(); + this.inline.init(); + this.block.init(); + this.list.init(); + this.card.init(this.options.cards || []); + this.plugin.init(this.options.plugins || [], this.options.config || {}); + this.nodeId.init(); + } + + setScrollNode(node?: HTMLElement) { + this.#_scrollNode = node ? $(node) : null; + } + + on = EventListener>( + eventType: string, + listener: F, + rewrite?: boolean, + ) { + this.event.on(eventType, listener, rewrite); + return this; + } + + off(eventType: string, listener: EventListener) { + this.event.off(eventType, listener); + return this; + } + + trigger(eventType: string, ...args: any): R { + return this.event.trigger(eventType, ...args); + } + + messageSuccess(message: string) { + console.log(`success:${message}`); + } + + messageError(error: string) { + console.log(`error:${error}`); + } + + messageConfirm(message: string): Promise { + console.log(`confirm:${message}`); + return Promise.reject(false); + } + + destroy() { + this.event.destroy(); + this.plugin.destroy(); + this.card.destroy(); + this.container.empty(); + } +} + +export default Editor; diff --git a/packages/engine/src/engine/index.ts b/packages/engine/src/engine/index.ts index 591c0edb..d9b50a27 100644 --- a/packages/engine/src/engine/index.ts +++ b/packages/engine/src/engine/index.ts @@ -1,20 +1,7 @@ -import { merge } from 'lodash'; -import NodeModel, { Event, $ } from '../node'; -import language from '../locales'; import Change from '../change'; import { DATA_ELEMENT } from '../constants/root'; -import schemaDefaultData from '../constants/schema'; -import conversionDefault from '../constants/conversion'; -import Schema from '../schema'; import OT from '../ot'; -import { - Selector, - NodeInterface, - EventInterface, - EventListener, - NodeModelInterface, - NodeIdInterface, -} from '../types/node'; +import { Selector, NodeInterface } from '../types/node'; import { ChangeInterface } from '../types/change'; import { ContainerInterface, @@ -23,112 +10,42 @@ import { } from '../types/engine'; import { HistoryInterface } from '../types/history'; import { OTInterface } from '../types/ot'; -import { SchemaInterface } from '../types/schema'; -import { ConversionInterface } from '../types/conversion'; -import { CommandInterface } from '../types/command'; -import { PluginModelInterface } from '../types/plugin'; import { HotkeyInterface } from '../types/hotkey'; -import { CardInterface, CardModelInterface } from '../types/card'; -import { ClipboardInterface } from '../types/clipboard'; -import { LanguageInterface } from '../types/language'; -import { MarkModelInterface } from '../types/mark'; -import { ListModelInterface } from '../types/list'; -import { InlineModelInterface } from '../types/inline'; -import { BlockModelInterface } from '../types/block'; -import { RequestInterface } from '../types/request'; -import Conversion from '../parser/conversion'; +import { CardInterface } from '../types/card'; import History from '../history'; -import Command from '../command'; import Hotkey from '../hotkey'; -import Plugin from '../plugin'; -import CardModel from '../card'; import { getDocument } from '../utils'; import { ANCHOR, CURSOR, FOCUS } from '../constants/selection'; import { toJSON0, toDOM } from '../ot/utils'; -import Clipboard from '../clipboard'; import Parser from '../parser'; -import Language from '../language'; -import Mark from '../mark'; -import List from '../list'; import { TypingInterface } from '../types'; import Typing from '../typing'; import Container from './container'; -import Inline from '../inline'; -import Block from '../block'; import Selection from '../selection'; -import Request from '../request'; -import NodeId from '../node/id'; +import Editor from '../editor'; +import { $ } from '../node'; import './index.css'; -class Engine implements EngineInterface { +class Engine + extends Editor + implements EngineInterface +{ private _readonly: boolean = false; private _container: ContainerInterface; readonly kind = 'engine'; - options: EngineOptions = { - lang: 'zh-CN', - locale: {}, - plugins: [], - cards: [], - config: {}, - }; - language: LanguageInterface; - root: NodeInterface; - change: ChangeInterface; - card: CardModelInterface; - plugin: PluginModelInterface; - node: NodeModelInterface; - nodeId: NodeIdInterface; - list: ListModelInterface; - mark: MarkModelInterface; - inline: InlineModelInterface; - block: BlockModelInterface; - event: EventInterface; + typing: TypingInterface; ot: OTInterface; - schema: SchemaInterface; - conversion: ConversionInterface; + change: ChangeInterface; history: HistoryInterface; - command: CommandInterface; hotkey: HotkeyInterface; - clipboard: ClipboardInterface; - request: RequestInterface; - #_scrollNode: NodeInterface | null = null; - get container(): NodeInterface { - return this._container.getNode(); - } + readonly container: NodeInterface; get readonly(): boolean { return this._readonly; } - get scrollNode(): NodeInterface | null { - if (this.#_scrollNode) return this.#_scrollNode; - const { scrollNode } = this.options; - let sn = scrollNode - ? typeof scrollNode === 'function' - ? scrollNode() - : scrollNode - : null; - // 查找父级样式 overflow 或者 overflow-y 为 auto 或者 scroll 的节点 - const targetValues = ['auto', 'scroll']; - let parent = this.container.parent(); - while (!sn && parent && parent.length > 0 && parent.name !== 'body') { - if ( - targetValues.includes(parent.css('overflow')) || - targetValues.includes(parent.css('overflow-y')) - ) { - sn = parent.get(); - break; - } else { - parent = parent.parent(); - } - } - if (sn === null) sn = document.documentElement; - this.#_scrollNode = sn ? $(sn) : null; - return this.#_scrollNode; - } - set readonly(readonly: boolean) { if (this.readonly === readonly) return; if (readonly) { @@ -145,45 +62,10 @@ class Engine implements EngineInterface { } constructor(selector: Selector, options?: EngineOptions) { + super(selector, options); this.options = { ...this.options, ...options }; - // 多语言 - this.language = new Language( - this.options.lang || 'zh-CN', - merge(language, options?.locale), - ); - // 事件管理 - this.event = new Event(); - // 命令 - this.command = new Command(this); - // 节点规则 - this.schema = new Schema(); - this.schema.add(schemaDefaultData); - // 节点转换规则 - this.conversion = new Conversion(this); - conversionDefault.forEach((rule) => - this.conversion.add(rule.from, rule.to), - ); // 历史 this.history = new History(this); - // 卡片 - this.card = new CardModel(this, this.options.lazyRender); - // 剪贴板 - this.clipboard = new Clipboard(this); - // http请求 - this.request = new Request(); - // 插件 - this.plugin = new Plugin(this); - // 节点管理 - this.node = new NodeModel(this); - this.nodeId = new NodeId(this); - // 列表 - this.list = new List(this); - // 样式标记 - this.mark = new Mark(this); - // 行内样式 - this.inline = new Inline(this); - // 块级节点 - this.block = new Block(this); // 编辑器容器 this._container = new Container(selector, { engine: this, @@ -192,6 +74,7 @@ class Engine implements EngineInterface { tabIndex: this.options.tabIndex, placeholder: this.options.placeholder, }); + this.container = this._container.getNode(); // 编辑器父节点 this.root = $( this.options.root || this.container.parent() || getDocument().body, @@ -223,16 +106,9 @@ class Engine implements EngineInterface { this._readonly = this.options.readonly === undefined ? false : this.options.readonly; this._container.setReadonly(this._readonly); - // 实例化插件 - this.mark.init(); - this.inline.init(); - this.block.init(); - this.list.init(); // 快捷键 this.hotkey = new Hotkey(this); - this.card.init(this.options.cards || []); - this.plugin.init(this.options.plugins || [], this.options.config || {}); - this.nodeId.init(); + this.init(); // 协同 this.ot = new OT(this); @@ -242,10 +118,6 @@ class Engine implements EngineInterface { this.ot.initLocal(); } - setScrollNode(node?: HTMLElement) { - this.#_scrollNode = node ? $(node) : null; - } - isFocus() { return this._container.isFocus(); } @@ -262,24 +134,6 @@ class Engine implements EngineInterface { this.change.range.blur(); } - on = EventListener>( - eventType: string, - listener: F, - rewrite?: boolean, - ) { - this.event.on(eventType, listener, rewrite); - return this; - } - - off(eventType: string, listener: EventListener) { - this.event.off(eventType, listener); - return this; - } - - trigger(eventType: string, ...args: any): R { - return this.event.trigger(eventType, ...args); - } - getValue(ignoreCursor: boolean = false) { const value = this.change.getValue({}); return ignoreCursor ? Selection.removeTags(value) : value; @@ -420,19 +274,6 @@ class Engine implements EngineInterface { }); } - messageSuccess(message: string) { - console.log(`success:${message}`); - } - - messageError(error: string) { - console.log(`error:${error}`); - } - - messageConfirm(message: string): Promise { - console.log(`confirm:${message}`); - return Promise.reject(false); - } - showPlaceholder() { this._container.showPlaceholder(); } @@ -445,10 +286,10 @@ class Engine implements EngineInterface { this._container.destroy(); this.change.destroy(); this.hotkey.destroy(); - this.card.destroy(); if (this.ot) { this.ot.destroy(); } + super.destroy(); } } diff --git a/packages/engine/src/index.ts b/packages/engine/src/index.ts index 9550145b..aa9ad611 100644 --- a/packages/engine/src/index.ts +++ b/packages/engine/src/index.ts @@ -13,6 +13,7 @@ import { isMarkPlugin, } from './plugin'; import Card from './card/entry'; +import CardManage from './card'; import View from './view'; import Toolbar, { Tooltip } from './toolbar'; import Range, { isRangeInterface, isRange, isSelection } from './range'; @@ -71,4 +72,5 @@ export { isRange, isSelection, Resizer, + CardManage, }; diff --git a/packages/engine/src/inline/index.ts b/packages/engine/src/inline/index.ts index 2a850545..fb98972d 100644 --- a/packages/engine/src/inline/index.ts +++ b/packages/engine/src/inline/index.ts @@ -4,10 +4,13 @@ import { CARD_SELECTOR, CARD_TYPE_KEY, } from '../constants'; -import { EditorInterface, EngineInterface } from '../types/engine'; -import { InlineModelInterface } from '../types/inline'; -import { NodeInterface } from '../types/node'; -import { RangeInterface } from '../types/range'; +import { + EditorInterface, + EngineInterface, + InlineModelInterface, + NodeInterface, + RangeInterface, +} from '../types'; import { getDocument, isEngine } from '../utils'; import { Backspace, Left, Right } from './typing'; import { $ } from '../node'; diff --git a/packages/engine/src/node/event.ts b/packages/engine/src/node/event.ts index ded0928a..27e3a8dc 100644 --- a/packages/engine/src/node/event.ts +++ b/packages/engine/src/node/event.ts @@ -62,6 +62,14 @@ class Event implements EventInterface { } return undefined as any; } + + destroy() { + Object.keys(this.listeners).forEach((type) => { + this.listeners[type].forEach((listener) => { + this.off(type, listener); + }); + }); + } } export default Event; diff --git a/packages/engine/src/parser/index.ts b/packages/engine/src/parser/index.ts index af5af2e1..f1c28aec 100644 --- a/packages/engine/src/parser/index.ts +++ b/packages/engine/src/parser/index.ts @@ -1,6 +1,6 @@ import { NodeInterface } from '../types/node'; import { DATA_ELEMENT, DATA_ID, EDITABLE } from '../constants/root'; -import { EditorInterface } from '../types/engine'; +import { EditorInterface } from '../types/editor'; import { SchemaInterface, ParserInterface, diff --git a/packages/engine/src/plugin/base.ts b/packages/engine/src/plugin/base.ts index a0d11e30..de6ddb66 100644 --- a/packages/engine/src/plugin/base.ts +++ b/packages/engine/src/plugin/base.ts @@ -1,8 +1,8 @@ import { CardInterface } from '../types/card'; -import { EditorInterface } from '../types/engine'; +import { EditorInterface } from '../types/editor'; import { PluginOptions, PluginInterface } from '../types/plugin'; -abstract class PluginEntry +abstract class PluginEntry implements PluginInterface { protected readonly editor: EditorInterface; @@ -55,6 +55,7 @@ abstract class PluginEntry ...args: any ) => boolean | number | void, ): Promise; + destroy?(): void; } export default PluginEntry; diff --git a/packages/engine/src/plugin/block.ts b/packages/engine/src/plugin/block.ts index b61d040e..c22493ee 100644 --- a/packages/engine/src/plugin/block.ts +++ b/packages/engine/src/plugin/block.ts @@ -1,14 +1,15 @@ import ElementPluginEntry from './element'; -import { +import type { SchemaBlock, BlockInterface, NodeInterface, PluginInterface, + PluginOptions, } from '../types'; -abstract class BlockEntry +abstract class BlockEntry extends ElementPluginEntry - implements BlockInterface + implements BlockInterface { readonly kind: string = 'block'; /** diff --git a/packages/engine/src/plugin/element.ts b/packages/engine/src/plugin/element.ts index 238df7aa..a0046c77 100644 --- a/packages/engine/src/plugin/element.ts +++ b/packages/engine/src/plugin/element.ts @@ -1,11 +1,11 @@ -import { +import type { PluginOptions, ElementPluginInterface, NodeInterface, ConversionData, PluginInterface, } from '../types'; -import { +import type { SchemaAttributes, SchemaGlobal, SchemaRule, @@ -17,9 +17,9 @@ import { $ } from '../node'; import PluginEntry from './base'; import { isNode } from '../node/utils'; -abstract class ElementPluginEntry +abstract class ElementPluginEntry extends PluginEntry - implements ElementPluginInterface + implements ElementPluginInterface { readonly kind: string = 'element'; /** diff --git a/packages/engine/src/plugin/index.ts b/packages/engine/src/plugin/index.ts index 5ba148cb..752969eb 100644 --- a/packages/engine/src/plugin/index.ts +++ b/packages/engine/src/plugin/index.ts @@ -1,4 +1,4 @@ -import { EditorInterface } from '../types/engine'; +import { EditorInterface } from '../types/editor'; import { ElementPluginInterface, PluginEntry, @@ -45,37 +45,52 @@ class PluginModel implements PluginModelInterface { } } - findPlugin(pluginName: string) { + findPlugin( + pluginName: string, + ): PluginInterface | undefined { const plugin = this.components[pluginName]; - return plugin; + if (!plugin) return; + return plugin as PluginInterface; } - findElementPlugin(pluginName: string) { - const plugin = this.findPlugin(pluginName); + findElementPlugin( + pluginName: string, + ): ElementPluginInterface | undefined { + const plugin = this.findPlugin(pluginName); + if (!plugin) return; if (isElementPlugin(plugin)) { - return plugin as ElementPluginInterface; + return plugin as ElementPluginInterface; } return; } - findMarkPlugin(pluginName: string) { + findMarkPlugin( + pluginName: string, + ): MarkInterface | undefined { const plugin = this.findPlugin(pluginName); + if (!plugin) return; if (isMarkPlugin(plugin)) { - return plugin as MarkInterface; + return plugin as MarkInterface; } return; } - findInlinePlugin(pluginName: string) { + findInlinePlugin( + pluginName: string, + ): InlineInterface | undefined { const plugin = this.findPlugin(pluginName); + if (!plugin) return; if (isInlinePlugin(plugin)) { - return plugin as InlineInterface; + return plugin as InlineInterface; } return; } - findBlockPlugin(pluginName: string) { + findBlockPlugin( + pluginName: string, + ): BlockInterface | undefined { const plugin = this.findPlugin(pluginName); + if (!plugin) return; if (isBlockPlugin(plugin)) { - return plugin as BlockInterface; + return plugin as BlockInterface; } return; } @@ -92,6 +107,13 @@ class PluginModel implements PluginModelInterface { return; }); } + + destroy() { + Object.keys(this.components).forEach((pluginName) => { + const plugin = this.components[pluginName]; + if (plugin.destroy) plugin.destroy(); + }); + } } export default PluginModel; diff --git a/packages/engine/src/plugin/inline.ts b/packages/engine/src/plugin/inline.ts index 56967955..0aa78cab 100644 --- a/packages/engine/src/plugin/inline.ts +++ b/packages/engine/src/plugin/inline.ts @@ -1,17 +1,18 @@ import ElementPluginEntry from './element'; -import { +import type { InlineInterface, NodeInterface, PluginEntry as PluginEntryType, PluginInterface, + PluginOptions, } from '../types'; import { $ } from '../node'; import { isEngine } from '../utils'; -abstract class InlineEntry +abstract class InlineEntry extends ElementPluginEntry - implements InlineInterface + implements InlineInterface { readonly kind: string = 'inline'; /** diff --git a/packages/engine/src/plugin/list/index.ts b/packages/engine/src/plugin/list/index.ts index bb388993..c3704981 100644 --- a/packages/engine/src/plugin/list/index.ts +++ b/packages/engine/src/plugin/list/index.ts @@ -1,4 +1,4 @@ -import { NodeInterface } from '../../types'; +import { NodeInterface, PluginOptions } from '../../types'; import { CARD_KEY, READY_CARD_KEY } from '../../constants'; import { ListInterface } from '../../types/list'; import { PluginEntry as PluginEntryType } from '../../types/plugin'; @@ -7,9 +7,9 @@ import { $ } from '../../node'; import { isEngine } from '../../utils'; import './index.css'; -abstract class ListEntry +abstract class ListEntry extends BlockEntry - implements ListInterface + implements ListInterface { cardName?: string; private isPasteList: boolean = false; diff --git a/packages/engine/src/plugin/mark.ts b/packages/engine/src/plugin/mark.ts index ecc4351f..7d6b0092 100644 --- a/packages/engine/src/plugin/mark.ts +++ b/packages/engine/src/plugin/mark.ts @@ -1,17 +1,18 @@ import ElementPluginEntry from './element'; -import { +import type { MarkInterface, NodeInterface, SchemaMark, PluginEntry as PluginEntryType, PluginInterface, + PluginOptions, } from '../types'; import { $ } from '../node'; import { isEngine } from '../utils'; -abstract class MarkEntry +abstract class MarkEntry extends ElementPluginEntry - implements MarkInterface + implements MarkInterface { readonly kind: string = 'mark'; /** diff --git a/packages/engine/src/range.ts b/packages/engine/src/range.ts index 69c5f813..55613e2b 100644 --- a/packages/engine/src/range.ts +++ b/packages/engine/src/range.ts @@ -17,7 +17,7 @@ import { } from './constants/root'; import Selection from './selection'; import { SelectionInterface } from './types/selection'; -import { EditorInterface } from './types/engine'; +import { EditorInterface } from './types/editor'; import { Path } from 'sharedb'; import { $ } from './node'; import { CardEntry } from './types/card'; diff --git a/packages/engine/src/types/block.ts b/packages/engine/src/types/block.ts index d75783c4..f370ee9a 100644 --- a/packages/engine/src/types/block.ts +++ b/packages/engine/src/types/block.ts @@ -1,5 +1,5 @@ import { NodeInterface } from './node'; -import { ElementPluginInterface } from './plugin'; +import { ElementPluginInterface, PluginOptions } from './plugin'; import { RangeInterface } from './range'; import { SchemaBlock } from './schema'; /** @@ -161,7 +161,8 @@ export interface BlockModelInterface { /** * block 插件 */ -export interface BlockInterface extends ElementPluginInterface { +export interface BlockInterface + extends ElementPluginInterface { readonly kind: string; /** * 标签名称 diff --git a/packages/engine/src/types/card.ts b/packages/engine/src/types/card.ts index 2827ff35..9fe985de 100644 --- a/packages/engine/src/types/card.ts +++ b/packages/engine/src/types/card.ts @@ -1,4 +1,4 @@ -import { EditorInterface } from './engine'; +import { EditorInterface } from './editor'; import { NodeInterface } from './node'; import { TinyCanvasInterface } from './tiny-canvas'; import { RangeInterface } from './range'; @@ -10,7 +10,7 @@ import { import { CardActiveTrigger, CardType, SelectStyleType } from '../card/enum'; import { Placement } from './position'; -export interface CardOptions { +export interface CardOptions { editor: EditorInterface; value?: Partial; root?: NodeInterface; @@ -84,7 +84,7 @@ export type CardToolbarItemOptions = items: Array; }; -export interface CardEntry { +export interface CardEntry { prototype: CardInterface; new (options: CardOptions): CardInterface; /** @@ -125,7 +125,7 @@ export interface CardEntry { readonly lazyRender: boolean; } -export interface CardInterface { +export interface CardInterface { /** * 初始化调用 */ @@ -158,6 +158,9 @@ export interface CardInterface { * 可编辑的节点 */ readonly contenteditable: Array; + /** + * 卡片是否处于懒加载中 + */ readonly loading: boolean; /** * 卡片类型,设置卡片类型会触发card重新渲染 @@ -254,6 +257,22 @@ export interface CardInterface { rgb: string; }, ): NodeInterface | void; + /** + * 在卡片右侧光标容器位置按下左键触发,可以实现如何选中卡片内部自定义操作 + */ + onSelectLeft?(event: KeyboardEvent): boolean | void; + /** + * 在卡片左侧光标容器位置按下右键触发,可以实现如何选中卡片内部自定义操作 + */ + onSelectRight?(event: KeyboardEvent): boolean | void; + /** + * 在卡片下方按下上键触发(block类型有效),可以实现如何选中卡片内部自定义操作 + */ + onSelectUp?(event: KeyboardEvent): boolean | void; + /** + * 在卡片上方按下下键触发(block类型有效),可以实现如何选中卡片内部自定义操作 + */ + onSelectDown?(event: KeyboardEvent): boolean | void; /** * 激活状态变化时触发 * @param activated 是否激活 @@ -530,7 +549,7 @@ export interface CardModelInterface { * @param value 要更新的卡片值 * @param args 更新时渲染时额外的参数 */ - update( + update( selector: NodeInterface | Node | string, value: Partial, ...args: any diff --git a/packages/engine/src/types/editor.ts b/packages/engine/src/types/editor.ts new file mode 100644 index 00000000..56f9991f --- /dev/null +++ b/packages/engine/src/types/editor.ts @@ -0,0 +1,433 @@ +import { + PluginEntry, + CardEntry, + LanguageInterface, + NodeInterface, + CommandInterface, + RequestInterface, + CardModelInterface, + PluginModelInterface, + NodeModelInterface, + NodeIdInterface, + MarkModelInterface, + InlineModelInterface, + BlockModelInterface, + EventInterface, + SchemaInterface, + ConversionInterface, + ClipboardInterface, + CardInterface, + PluginOptions, +} from '.'; +import { ListModelInterface } from './list'; +import { EventListener } from './node'; + +export interface EditorOptions { + /** + * 语言,默认zh-CN + */ + lang?: string; + /** + * 本地化 + */ + locale?: Record; + /** + * 插件配置 + */ + plugins?: Array; + /** + * 卡片配置 + */ + cards?: Array; + /** + * 插件选项,每个插件具体选项请在插件查看 + */ + config?: Record; + /** + * 阅读器根节点,默认为阅读器所在节点的父节点 + */ + root?: Node; + /** + * 滚动条节点,查找父级样式 overflow 或者 overflow-y 为 auto 或者 scroll 的节点 + */ + scrollNode?: Node | (() => Node | null); + /** + * 懒惰渲染卡片(仅限已启用 lazyRender 的卡片),默认为 true + */ + lazyRender?: boolean; +} + +export interface EditorInterface { + options: T; + /** + * 类型 + */ + readonly kind: 'engine' | 'view' | 'editor'; + /** + * 语言 + */ + language: LanguageInterface; + /** + * 编辑器节点 + */ + readonly container: NodeInterface; + /** + * 滚动条节点 + */ + readonly scrollNode: NodeInterface | null; + /** + * 编辑器根节点,默认为编辑器父节点 + */ + readonly root: NodeInterface; + /** + * 编辑器命令 + */ + command: CommandInterface; + /** + * 请求 + */ + request: RequestInterface; + /** + * 卡片 + */ + card: CardModelInterface; + /** + * 插件 + */ + plugin: PluginModelInterface; + /** + * 节点管理 + */ + node: NodeModelInterface; + /** + * 节点id管理器 + */ + nodeId: NodeIdInterface; + /** + * List 列表标签管理 + */ + list: ListModelInterface; + /** + * Mark 标签管理 + */ + mark: MarkModelInterface; + /** + * inline 标签管理 + */ + inline: InlineModelInterface; + /** + * block 标签管理 + */ + block: BlockModelInterface; + /** + * 事件 + */ + event: EventInterface; + /** + * 标签过滤规则 + */ + schema: SchemaInterface; + /** + * 标签转换规则 + */ + conversion: ConversionInterface; + /** + * 剪切板 + */ + clipboard: ClipboardInterface; + /** + * 设置滚动节点 + * @param node 节点 + */ + setScrollNode(node: HTMLElement): void; + destroy(): void; + /** + * 绑定事件 + * @param eventType 事件类型 + * @param listener 事件回调 + * @param rewrite 是否重写 + */ + on = EventListener>( + eventType: string, + listener: F, + rewrite?: boolean, + ): void; + /** + * 全选ctrl+a键按下,返回false,终止处理其它监听 + * @param eventType + * @param listener + * @param rewrite + */ + on( + eventType: 'keydown:all', + listener: (event: KeyboardEvent) => boolean | void, + rewrite?: boolean, + ): void; + /** + * 卡片最小化时触发 + * @param eventType + * @param listener name:插件名称、args:参数 + * @param rewrite + */ + on( + eventType: 'card:minimize', + listener: (card: CardInterface) => void, + rewrite?: boolean, + ): void; + /** + * 卡片最大化时触发 + * @param eventType + * @param listener name:插件名称、args:参数 + * @param rewrite + */ + on( + eventType: 'card:maximize', + listener: (card: CardInterface) => void, + rewrite?: boolean, + ): void; + /** + * 解析DOM节点,生成符合标准的 XML 代码之前触发 + * @param root DOM节点 + */ + on( + eventType: 'parse:value-before', + listener: (root: NodeInterface) => void, + rewrite?: boolean, + ): void; + /** + * 解析DOM节点,生成符合标准的 XML,遍历子节点时触发。返回false跳过当前节点 + * @param node 当前遍历的节点 + * @param attributes 当前节点已过滤后的属性 + * @param styles 当前节点已过滤后的样式 + * @param value 当前已经生成的xml代码 + */ + on( + eventType: 'parse:value', + listener: ( + node: NodeInterface, + attributes: { [key: string]: string }, + styles: { [key: string]: string }, + value: Array, + ) => boolean | void, + rewrite?: boolean, + ): void; + /** + * 解析DOM节点,生成符合标准的 XML。生成xml代码结束后触发 + * @param value xml代码 + */ + on( + eventType: 'parse:value-after', + listener: (value: Array) => void, + rewrite?: boolean, + ): void; + /** + * 转换为HTML代码之前触发 + * @param root 需要转换的根节点 + */ + on( + eventType: 'parse:html-before', + listener: (root: NodeInterface) => void, + rewrite?: boolean, + ): void; + /** + * 转换为HTML代码 + * @param root 需要转换的根节点 + */ + on( + eventType: 'parse:html', + listener: (root: NodeInterface) => void, + rewrite?: boolean, + ): void; + /** + * 转换为HTML代码之后触发 + * @param root 需要转换的根节点 + */ + on( + eventType: 'parse:html-after', + listener: (root: NodeInterface) => void, + rewrite?: boolean, + ): void; + /** + * 复制DOM节点时触发 + * @param node 当前遍历的子节点 + */ + on( + eventType: 'copy', + listener: (root: NodeInterface) => void, + rewrite?: boolean, + ): void; + /** + * 移除绑定事件 + * @param eventType 事件类型 + * @param listener 事件回调 + */ + off(eventType: string, listener: EventListener): void; + /** + * 全选ctrl+a键按下,返回false,终止处理其它监听 + * @param eventType + * @param listener + */ + off( + eventType: 'keydown:all', + listener: (event: KeyboardEvent) => boolean | void, + ): void; + /** + * 卡片最小化时触发 + * @param eventType + * @param listener name:插件名称、args:参数 + */ + off( + eventType: 'card:minimize', + listener: (card: CardInterface) => void, + ): void; + /** + * 卡片最大化时触发 + * @param eventType + * @param listener name:插件名称、args:参数 + */ + off( + eventType: 'card:maximize', + listener: (card: CardInterface) => void, + ): void; + /** + * 解析DOM节点,生成符合标准的 XML 代码之前触发 + * @param root DOM节点 + */ + off( + eventType: 'parse:value-before', + listener: (root: NodeInterface) => void, + ): void; + /** + * 解析DOM节点,生成符合标准的 XML,遍历子节点时触发。返回false跳过当前节点 + * @param node 当前遍历的节点 + * @param attributes 当前节点已过滤后的属性 + * @param styles 当前节点已过滤后的样式 + * @param value 当前已经生成的xml代码 + */ + off( + eventType: 'parse:value', + listener: ( + node: NodeInterface, + attributes: { [key: string]: string }, + styles: { [key: string]: string }, + value: Array, + ) => boolean | void, + ): void; + /** + * 解析DOM节点,生成符合标准的 XML。生成xml代码结束后触发 + * @param value xml代码 + */ + off( + eventType: 'parse:value-after', + listener: (value: Array) => void, + ): void; + /** + * 转换为HTML代码之前触发 + * @param root 需要转换的根节点 + */ + off( + eventType: 'parse:html-before', + listener: (root: NodeInterface) => void, + ): void; + /** + * 转换为HTML代码 + * @param root 需要转换的根节点 + */ + off(eventType: 'parse:html', listener: (root: NodeInterface) => void): void; + /** + * 转换为HTML代码之后触发 + * @param root 需要转换的根节点 + */ + off( + eventType: 'parse:html-after', + listener: (root: NodeInterface) => void, + ): void; + /** + * 复制DOM节点时触发 + * @param node 当前遍历的子节点 + */ + off(eventType: 'copy', listener: (root: NodeInterface) => void): void; + /** + * 触发事件 + * @param eventType 事件名称 + * @param args 触发参数 + */ + trigger(eventType: string, ...args: any): R; + /** + * 全选ctrl+a键按下,返回false,终止处理其它监听 + * @param eventType + * @param listener + */ + trigger(eventType: 'keydown:all', event: KeyboardEvent): boolean | void; + /** + * 卡片最小化时触发 + * @param eventType + * @param listener name:插件名称、args:参数 + */ + trigger(eventType: 'card:minimize', card: CardInterface): void; + /** + * 卡片最大化时触发 + * @param eventType + * @param listener name:插件名称、args:参数 + */ + trigger(eventType: 'card:maximize', card: CardInterface): void; + /** + * 解析DOM节点,生成符合标准的 XML 代码之前触发 + * @param root DOM节点 + */ + trigger(eventType: 'parse:value-before', root: NodeInterface): void; + /** + * 解析DOM节点,生成符合标准的 XML,遍历子节点时触发。返回false跳过当前节点 + * @param node 当前遍历的节点 + * @param attributes 当前节点已过滤后的属性 + * @param styles 当前节点已过滤后的样式 + * @param value 当前已经生成的xml代码 + */ + trigger( + eventType: 'parse:value', + node: NodeInterface, + attributes: { [key: string]: string }, + styles: { [key: string]: string }, + value: Array, + ): boolean | void; + /** + * 解析DOM节点,生成符合标准的 XML。生成xml代码结束后触发 + * @param value xml代码 + */ + trigger(eventType: 'parse:value-after', value: Array): void; + /** + * 转换为HTML代码之前触发 + * @param root 需要转换的根节点 + */ + trigger(eventType: 'parse:html-before', root: NodeInterface): void; + /** + * 转换为HTML代码 + * @param root 需要转换的根节点 + */ + trigger(eventType: 'parse:html', root: NodeInterface): void; + /** + * 转换为HTML代码之后触发 + * @param root 需要转换的根节点 + */ + trigger(eventType: 'parse:html-after', root: NodeInterface): void; + /** + * 复制DOM节点时触发 + * @param node 当前遍历的子节点 + */ + trigger(eventType: 'copy', root: NodeInterface): void; + /** + * 显示成功的信息 + * @param message 信息 + */ + messageSuccess(message: string): void; + /** + * 显示错误信息 + * @param error 错误信息 + */ + messageError(error: string): void; + /** + * 消息确认 + * @param message 消息 + */ + messageConfirm(message: string): Promise; +} diff --git a/packages/engine/src/types/engine.ts b/packages/engine/src/types/engine.ts index af3c1b00..30e40f7d 100644 --- a/packages/engine/src/types/engine.ts +++ b/packages/engine/src/types/engine.ts @@ -1,29 +1,15 @@ -import { - EventInterface, - NodeInterface, - Selector, - EventListener, - NodeModelInterface, -} from './node'; +import { NodeInterface, Selector, EventListener } from './node'; import { ChangeInterface } from './change'; import { OTInterface } from './ot'; import { SchemaInterface } from './schema'; -import { ConversionInterface } from './conversion'; import { HistoryInterface } from './history'; -import { PluginEntry, PluginModelInterface, PluginOptions } from './plugin'; -import { CommandInterface } from './command'; -import { CardEntry, CardInterface, CardModelInterface } from './card'; -import { ClipboardData, ClipboardInterface } from './clipboard'; -import { LanguageInterface } from './language'; -import { MarkModelInterface } from './mark'; -import { ListModelInterface } from './list'; +import { CardInterface } from './card'; +import { ClipboardData } from './clipboard'; import { TypingInterface } from './typing'; -import { InlineModelInterface } from './inline'; -import { BlockModelInterface } from './block'; -import { RequestInterface } from './request'; import { RangeInterface } from './range'; import { Op } from 'sharedb'; -import { HotkeyInterface, NodeIdInterface } from './'; +import { EditorInterface, EditorOptions } from './editor'; +import { HotkeyInterface } from './hotkey'; /** * 编辑器容器接口 @@ -59,390 +45,7 @@ export interface ContainerInterface { */ destroy(): void; } - -export interface EditorInterface { - /** - * 类型 - */ - readonly kind: 'engine' | 'view'; - /** - * 语言 - */ - language: LanguageInterface; - /** - * 编辑器节点 - */ - container: NodeInterface; - /** - * 滚动条节点 - */ - readonly scrollNode: NodeInterface | null; - /** - * 编辑器根节点,默认为编辑器父节点 - */ - root: NodeInterface; - /** - * 编辑器命令 - */ - command: CommandInterface; - /** - * 请求 - */ - request: RequestInterface; - /** - * 卡片 - */ - card: CardModelInterface; - /** - * 插件 - */ - plugin: PluginModelInterface; - /** - * 节点管理 - */ - node: NodeModelInterface; - /** - * 节点id管理器 - */ - nodeId: NodeIdInterface; - /** - * List 列表标签管理 - */ - list: ListModelInterface; - /** - * Mark 标签管理 - */ - mark: MarkModelInterface; - /** - * inline 标签管理 - */ - inline: InlineModelInterface; - /** - * block 标签管理 - */ - block: BlockModelInterface; - /** - * 事件 - */ - event: EventInterface; - /** - * 标签过滤规则 - */ - schema: SchemaInterface; - /** - * 标签转换规则 - */ - conversion: ConversionInterface; - /** - * 剪切板 - */ - clipboard: ClipboardInterface; - - /** - * 设置滚动节点 - * @param node 节点 - */ - setScrollNode(node: HTMLElement): void; - /** - * 绑定事件 - * @param eventType 事件类型 - * @param listener 事件回调 - * @param rewrite 是否重写 - */ - on = EventListener>( - eventType: string, - listener: F, - rewrite?: boolean, - ): void; - /** - * 全选ctrl+a键按下,返回false,终止处理其它监听 - * @param eventType - * @param listener - * @param rewrite - */ - on( - eventType: 'keydown:all', - listener: (event: KeyboardEvent) => boolean | void, - rewrite?: boolean, - ): void; - /** - * 卡片最小化时触发 - * @param eventType - * @param listener name:插件名称、args:参数 - * @param rewrite - */ - on( - eventType: 'card:minimize', - listener: (card: CardInterface) => void, - rewrite?: boolean, - ): void; - /** - * 卡片最大化时触发 - * @param eventType - * @param listener name:插件名称、args:参数 - * @param rewrite - */ - on( - eventType: 'card:maximize', - listener: (card: CardInterface) => void, - rewrite?: boolean, - ): void; - /** - * 解析DOM节点,生成符合标准的 XML 代码之前触发 - * @param root DOM节点 - */ - on( - eventType: 'parse:value-before', - listener: (root: NodeInterface) => void, - rewrite?: boolean, - ): void; - /** - * 解析DOM节点,生成符合标准的 XML,遍历子节点时触发。返回false跳过当前节点 - * @param node 当前遍历的节点 - * @param attributes 当前节点已过滤后的属性 - * @param styles 当前节点已过滤后的样式 - * @param value 当前已经生成的xml代码 - */ - on( - eventType: 'parse:value', - listener: ( - node: NodeInterface, - attributes: { [key: string]: string }, - styles: { [key: string]: string }, - value: Array, - ) => boolean | void, - rewrite?: boolean, - ): void; - /** - * 解析DOM节点,生成符合标准的 XML。生成xml代码结束后触发 - * @param value xml代码 - */ - on( - eventType: 'parse:value-after', - listener: (value: Array) => void, - rewrite?: boolean, - ): void; - /** - * 转换为HTML代码之前触发 - * @param root 需要转换的根节点 - */ - on( - eventType: 'parse:html-before', - listener: (root: NodeInterface) => void, - rewrite?: boolean, - ): void; - /** - * 转换为HTML代码 - * @param root 需要转换的根节点 - */ - on( - eventType: 'parse:html', - listener: (root: NodeInterface) => void, - rewrite?: boolean, - ): void; - /** - * 转换为HTML代码之后触发 - * @param root 需要转换的根节点 - */ - on( - eventType: 'parse:html-after', - listener: (root: NodeInterface) => void, - rewrite?: boolean, - ): void; - /** - * 复制DOM节点时触发 - * @param node 当前遍历的子节点 - */ - on( - eventType: 'copy', - listener: (root: NodeInterface) => void, - rewrite?: boolean, - ): void; - /** - * 移除绑定事件 - * @param eventType 事件类型 - * @param listener 事件回调 - */ - off(eventType: string, listener: EventListener): void; - /** - * 全选ctrl+a键按下,返回false,终止处理其它监听 - * @param eventType - * @param listener - */ - off( - eventType: 'keydown:all', - listener: (event: KeyboardEvent) => boolean | void, - ): void; - /** - * 卡片最小化时触发 - * @param eventType - * @param listener name:插件名称、args:参数 - */ - off( - eventType: 'card:minimize', - listener: (card: CardInterface) => void, - ): void; - /** - * 卡片最大化时触发 - * @param eventType - * @param listener name:插件名称、args:参数 - */ - off( - eventType: 'card:maximize', - listener: (card: CardInterface) => void, - ): void; - /** - * 解析DOM节点,生成符合标准的 XML 代码之前触发 - * @param root DOM节点 - */ - off( - eventType: 'parse:value-before', - listener: (root: NodeInterface) => void, - ): void; - /** - * 解析DOM节点,生成符合标准的 XML,遍历子节点时触发。返回false跳过当前节点 - * @param node 当前遍历的节点 - * @param attributes 当前节点已过滤后的属性 - * @param styles 当前节点已过滤后的样式 - * @param value 当前已经生成的xml代码 - */ - off( - eventType: 'parse:value', - listener: ( - node: NodeInterface, - attributes: { [key: string]: string }, - styles: { [key: string]: string }, - value: Array, - ) => boolean | void, - ): void; - /** - * 解析DOM节点,生成符合标准的 XML。生成xml代码结束后触发 - * @param value xml代码 - */ - off( - eventType: 'parse:value-after', - listener: (value: Array) => void, - ): void; - /** - * 转换为HTML代码之前触发 - * @param root 需要转换的根节点 - */ - off( - eventType: 'parse:html-before', - listener: (root: NodeInterface) => void, - ): void; - /** - * 转换为HTML代码 - * @param root 需要转换的根节点 - */ - off(eventType: 'parse:html', listener: (root: NodeInterface) => void): void; - /** - * 转换为HTML代码之后触发 - * @param root 需要转换的根节点 - */ - off( - eventType: 'parse:html-after', - listener: (root: NodeInterface) => void, - ): void; - /** - * 复制DOM节点时触发 - * @param node 当前遍历的子节点 - */ - off(eventType: 'copy', listener: (root: NodeInterface) => void): void; - /** - * 触发事件 - * @param eventType 事件名称 - * @param args 触发参数 - */ - trigger(eventType: string, ...args: any): R; - /** - * 全选ctrl+a键按下,返回false,终止处理其它监听 - * @param eventType - * @param listener - */ - trigger(eventType: 'keydown:all', event: KeyboardEvent): boolean | void; - /** - * 卡片最小化时触发 - * @param eventType - * @param listener name:插件名称、args:参数 - */ - trigger(eventType: 'card:minimize', card: CardInterface): void; - /** - * 卡片最大化时触发 - * @param eventType - * @param listener name:插件名称、args:参数 - */ - trigger(eventType: 'card:maximize', card: CardInterface): void; - /** - * 解析DOM节点,生成符合标准的 XML 代码之前触发 - * @param root DOM节点 - */ - trigger(eventType: 'parse:value-before', root: NodeInterface): void; - /** - * 解析DOM节点,生成符合标准的 XML,遍历子节点时触发。返回false跳过当前节点 - * @param node 当前遍历的节点 - * @param attributes 当前节点已过滤后的属性 - * @param styles 当前节点已过滤后的样式 - * @param value 当前已经生成的xml代码 - */ - trigger( - eventType: 'parse:value', - node: NodeInterface, - attributes: { [key: string]: string }, - styles: { [key: string]: string }, - value: Array, - ): boolean | void; - /** - * 解析DOM节点,生成符合标准的 XML。生成xml代码结束后触发 - * @param value xml代码 - */ - trigger(eventType: 'parse:value-after', value: Array): void; - /** - * 转换为HTML代码之前触发 - * @param root 需要转换的根节点 - */ - trigger(eventType: 'parse:html-before', root: NodeInterface): void; - /** - * 转换为HTML代码 - * @param root 需要转换的根节点 - */ - trigger(eventType: 'parse:html', root: NodeInterface): void; - /** - * 转换为HTML代码之后触发 - * @param root 需要转换的根节点 - */ - trigger(eventType: 'parse:html-after', root: NodeInterface): void; - /** - * 复制DOM节点时触发 - * @param node 当前遍历的子节点 - */ - trigger(eventType: 'copy', root: NodeInterface): void; - /** - * 显示成功的信息 - * @param message 信息 - */ - messageSuccess(message: string): void; - /** - * 显示错误信息 - * @param error 错误信息 - */ - messageError(error: string): void; - /** - * 消息确认 - * @param message 消息 - */ - messageConfirm(message: string): Promise; -} - -export type EngineOptions = { - /** - * 本地化语言,默认 zh-CN - */ - lang?: string; - /** - * 本地化语言 - */ - locale?: { [key: string]: {} }; +export interface EngineOptions extends EditorOptions { /** * 样式名称 */ @@ -451,26 +54,6 @@ export type EngineOptions = { * tab 键的索引 */ tabIndex?: number; - /** - * 根节点 - */ - root?: Node; - /** - * 滚动条节点,查找父级样式 overflow 或者 overflow-y 为 auto 或者 scroll 的节点 - */ - scrollNode?: Node | (() => Node | null); - /** - * 插件配置 - */ - plugins?: Array; - /** - * 卡片配置 - */ - cards?: Array; - /** - * 插件的可选项 - */ - config?: { [k: string]: PluginOptions }; /** * 占位内容 */ @@ -479,24 +62,21 @@ export type EngineOptions = { * 是否只读 */ readonly?: boolean; - /** - * 懒惰渲染卡片(仅限已启用 lazyRender 的卡片),默认为 true - */ - lazyRender?: boolean; -}; +} -export interface Engine { +export interface Engine { /** * 构造函数 */ - new (selector: Selector, options?: EngineOptions): EngineInterface; + new (selector: Selector, options?: T): EngineInterface; } -export interface EngineInterface extends EditorInterface { +export interface EngineInterface + extends EditorInterface { /** * 选项 */ - options: EngineOptions; + options: T; /** * 是否只读 */ diff --git a/packages/engine/src/types/index.ts b/packages/engine/src/types/index.ts index 2d77dde0..7d87893a 100644 --- a/packages/engine/src/types/index.ts +++ b/packages/engine/src/types/index.ts @@ -23,3 +23,4 @@ export * from './tiny-canvas'; export * from './parser'; export * from './resizer'; export * from './position'; +export * from './editor'; diff --git a/packages/engine/src/types/inline.ts b/packages/engine/src/types/inline.ts index 1b21df7e..0b5638e5 100644 --- a/packages/engine/src/types/inline.ts +++ b/packages/engine/src/types/inline.ts @@ -1,5 +1,9 @@ import { NodeInterface } from './node'; -import { PluginInterface, ElementPluginInterface } from './plugin'; +import { + PluginInterface, + ElementPluginInterface, + PluginOptions, +} from './plugin'; import { RangeInterface } from './range'; import { SchemaInterface } from './schema'; @@ -60,7 +64,8 @@ export interface InlineModelInterface { flat(node: NodeInterface | RangeInterface, schema?: SchemaInterface): void; } -export interface InlineInterface extends ElementPluginInterface { +export interface InlineInterface + extends ElementPluginInterface { readonly kind: string; /** * 标签名称 diff --git a/packages/engine/src/types/list.ts b/packages/engine/src/types/list.ts index 9e90e617..3698a8d3 100644 --- a/packages/engine/src/types/list.ts +++ b/packages/engine/src/types/list.ts @@ -2,6 +2,7 @@ import { CardInterface } from './card'; import { NodeInterface } from './node'; import { BlockInterface } from './block'; import { RangeInterface } from './range'; +import { PluginOptions } from './plugin'; /** * 列表删除键处理器 @@ -12,7 +13,8 @@ export interface BackspaceInterface { /** * 列表接口 */ -export interface ListInterface extends BlockInterface { +export interface ListInterface + extends BlockInterface { /** * 自定义列表卡片名称 */ diff --git a/packages/engine/src/types/mark.ts b/packages/engine/src/types/mark.ts index 36da5033..bac83992 100644 --- a/packages/engine/src/types/mark.ts +++ b/packages/engine/src/types/mark.ts @@ -1,5 +1,9 @@ import { NodeInterface } from './node'; -import { ElementPluginInterface, PluginInterface } from './plugin'; +import { + ElementPluginInterface, + PluginInterface, + PluginOptions, +} from './plugin'; import { RangeInterface } from './range'; import { SchemaMark } from './schema'; @@ -115,7 +119,8 @@ export interface MarkModelInterface { repairCursor(node: NodeInterface | Node): void; } -export interface MarkInterface extends ElementPluginInterface { +export interface MarkInterface + extends ElementPluginInterface { readonly kind: string; /** * 标签名称 diff --git a/packages/engine/src/types/node.ts b/packages/engine/src/types/node.ts index d575da82..81619ef6 100644 --- a/packages/engine/src/types/node.ts +++ b/packages/engine/src/types/node.ts @@ -38,6 +38,10 @@ export interface EventInterface { * @param args 事件参数 */ trigger(eventType: string, ...args: any): R; + /** + * 注销事件 + */ + destroy(): void; } export type Selector = | string diff --git a/packages/engine/src/types/plugin.ts b/packages/engine/src/types/plugin.ts index 8c49bf63..a34825fa 100644 --- a/packages/engine/src/types/plugin.ts +++ b/packages/engine/src/types/plugin.ts @@ -1,7 +1,7 @@ import { BlockInterface, InlineInterface, MarkInterface } from '.'; import { CardInterface } from './card'; import { ConversionData } from './conversion'; -import { EditorInterface } from './engine'; +import { EditorInterface } from './editor'; import { NodeInterface } from './node'; import { SchemaGlobal, SchemaRule, SchemaValue } from './schema'; @@ -26,7 +26,7 @@ export interface PluginEntry { readonly pluginName: string; } -export interface PluginInterface { +export interface PluginInterface { readonly kind: string; readonly name: string; /** @@ -77,9 +77,12 @@ export interface PluginInterface { ...args: any ) => boolean | number | void, ): Promise; + + destroy?(): void; } -export interface ElementPluginInterface extends PluginInterface { +export interface ElementPluginInterface + extends PluginInterface { /** * 标签名称 */ @@ -190,9 +193,21 @@ export interface PluginModelInterface { * 获取一个插件 * @param pluginName 插件名称 */ - findPlugin(pluginName: string): PluginInterface | undefined; - findElementPlugin(pluginName: string): ElementPluginInterface | undefined; - findMarkPlugin(pluginName: string): MarkInterface | undefined; - findInlinePlugin(pluginName: string): InlineInterface | undefined; - findBlockPlugin(pluginName: string): BlockInterface | undefined; + findPlugin( + pluginName: string, + ): PluginInterface | undefined; + findElementPlugin( + pluginName: string, + ): ElementPluginInterface | undefined; + findMarkPlugin( + pluginName: string, + ): MarkInterface | undefined; + findInlinePlugin( + pluginName: string, + ): InlineInterface | undefined; + findBlockPlugin( + pluginName: string, + ): BlockInterface | undefined; + + destroy(): void; } diff --git a/packages/engine/src/types/range.ts b/packages/engine/src/types/range.ts index f8fb355a..af3b2bcc 100644 --- a/packages/engine/src/types/range.ts +++ b/packages/engine/src/types/range.ts @@ -1,5 +1,5 @@ import { Path } from 'sharedb'; -import { EditorInterface } from './engine'; +import { EditorInterface } from './editor'; import { NodeInterface } from './node'; import { SelectionInterface } from './selection'; diff --git a/packages/engine/src/types/typing.ts b/packages/engine/src/types/typing.ts index 2b43e3f1..80cb9bc5 100644 --- a/packages/engine/src/types/typing.ts +++ b/packages/engine/src/types/typing.ts @@ -1,10 +1,10 @@ import { EngineInterface } from './engine'; -import { EventListener } from './node'; export interface TypingHandle { prototype: TypingHandleInterface; new (engine: EngineInterface): TypingHandleInterface; } +export type TypingEventListener = (event: KeyboardEvent) => boolean | void; /** * 按键处理接口 */ @@ -12,7 +12,7 @@ export interface TypingHandleInterface { /** * 事件集合 */ - listeners: Array; + listeners: Array; /** * 按键类型 键盘按下 | 键盘弹起 */ @@ -25,12 +25,17 @@ export interface TypingHandleInterface { * 绑定事件 * @param listener 事件方法 */ - on(listener: EventListener): void; + on(listener: TypingEventListener): void; + /** + * 绑定到第一个事件 + * @param listener 事件方法 + */ + unshiftOn(listener: TypingEventListener): void; /** * 移除事件 * @param listener 事件方法 */ - off(listener: EventListener): void; + off(listener: TypingEventListener): void; /** * 触发事件 * @param event 键盘事件 diff --git a/packages/engine/src/types/view.ts b/packages/engine/src/types/view.ts index 3137edc0..6a4292fd 100644 --- a/packages/engine/src/types/view.ts +++ b/packages/engine/src/types/view.ts @@ -1,12 +1,13 @@ -import { CardEntry } from './card'; -import { EditorInterface } from './engine'; +import { EditorInterface, EditorOptions } from './editor'; import { NodeInterface } from './node'; -import { PluginEntry } from './plugin'; +export interface ViewOptions extends EditorOptions {} /** * 阅读器接口 */ -export interface ViewInterface extends EditorInterface { +export interface ViewInterface + extends EditorInterface { + options: T; /** * 渲染内容 * @param content 渲染的内容 @@ -26,38 +27,3 @@ export interface ViewInterface extends EditorInterface { */ trigger(eventType: 'render', value: NodeInterface): void; } - -export type ContentViewOptions = { - /** - * 语言,默认zh-CN - */ - lang?: string; - /** - * 本地化 - */ - locale?: { [key: string]: {} }; - /** - * 插件配置 - */ - plugins?: Array; - /** - * 卡片配置 - */ - cards?: Array; - /** - * 插件选项,每个插件具体选项请在插件查看 - */ - config?: { [k: string]: {} }; - /** - * 阅读器根节点,默认为阅读器所在节点的父节点 - */ - root?: Node; - /** - * 滚动条节点,查找父级样式 overflow 或者 overflow-y 为 auto 或者 scroll 的节点 - */ - scrollNode?: Node | (() => Node | null); - /** - * 懒惰渲染卡片(仅限已启用 lazyRender 的卡片),默认为 true - */ - lazyRender?: boolean; -}; diff --git a/packages/engine/src/typing/keydown/backspace.ts b/packages/engine/src/typing/keydown/backspace.ts index 9fea694f..de7a1bcd 100644 --- a/packages/engine/src/typing/keydown/backspace.ts +++ b/packages/engine/src/typing/keydown/backspace.ts @@ -1,33 +1,10 @@ -import { - EngineInterface, - EventListener, - NodeInterface, - TypingHandleInterface, -} from '../../types'; +import { NodeInterface, TypingHandleInterface } from '../../types'; import { $ } from '../../node'; +import DefaultKeydown from './default'; -class Backspace implements TypingHandleInterface { +class Backspace extends DefaultKeydown implements TypingHandleInterface { type: 'keydown' | 'keyup' = 'keydown'; hotkey: Array | string = 'backspace'; - private engine: EngineInterface; - listeners: Array = []; - - constructor(engine: EngineInterface) { - this.engine = engine; - } - - on(listener: EventListener) { - this.listeners.push(listener); - } - - off(listener: EventListener) { - for (let i = 0; i < this.listeners.length; i++) { - if (this.listeners[i] === listener) { - this.listeners.splice(i, 1); - break; - } - } - } trigger(event: KeyboardEvent) { const { change, container } = this.engine; @@ -137,10 +114,6 @@ class Backspace implements TypingHandleInterface { } } } - - destroy() { - this.listeners = []; - } } export default Backspace; diff --git a/packages/engine/src/typing/keydown/default.ts b/packages/engine/src/typing/keydown/default.ts index 39bd3e75..a375b46d 100644 --- a/packages/engine/src/typing/keydown/default.ts +++ b/packages/engine/src/typing/keydown/default.ts @@ -1,24 +1,28 @@ import { EngineInterface, - EventListener, + TypingEventListener, TypingHandleInterface, } from '../../types'; class DefaultKeydown implements TypingHandleInterface { type: 'keydown' | 'keyup' = 'keydown'; hotkey: string | string[] | ((event: KeyboardEvent) => boolean) = ''; - listeners: Array = []; - private engine: EngineInterface; + listeners: Array = []; + engine: EngineInterface; constructor(engine: EngineInterface) { this.engine = engine; } - on(listener: EventListener) { + on(listener: TypingEventListener) { this.listeners.push(listener); } - off(listener: EventListener) { + unshiftOn(listener: TypingEventListener) { + this.listeners.unshift(listener); + } + + off(listener: TypingEventListener) { for (let i = 0; i < this.listeners.length; i++) { if (this.listeners[i] === listener) { this.listeners.splice(i, 1); diff --git a/packages/engine/src/typing/keydown/delete.ts b/packages/engine/src/typing/keydown/delete.ts index cac1a706..3967068b 100644 --- a/packages/engine/src/typing/keydown/delete.ts +++ b/packages/engine/src/typing/keydown/delete.ts @@ -1,35 +1,12 @@ -import { - EngineInterface, - EventListener, - RangeInterface, - TypingHandleInterface, -} from '../../types'; +import { RangeInterface, TypingHandleInterface } from '../../types'; import { CARD_KEY } from '../../constants'; import Range from '../../range'; import { $ } from '../../node'; +import DefaultKeydown from './default'; -class Delete implements TypingHandleInterface { - private engine: EngineInterface; +class Delete extends DefaultKeydown implements TypingHandleInterface { type: 'keydown' | 'keyup' = 'keydown'; hotkey: string | string[] | ((event: KeyboardEvent) => boolean) = 'delete'; - listeners: Array = []; - - constructor(engine: EngineInterface) { - this.engine = engine; - } - - on(listener: EventListener) { - this.listeners.push(listener); - } - - off(listener: EventListener) { - for (let i = 0; i < this.listeners.length; i++) { - if (this.listeners[i] === listener) { - this.listeners.splice(i, 1); - break; - } - } - } getNext(node: Node): Node | null { return $(node).isEditable() @@ -153,8 +130,5 @@ class Delete implements TypingHandleInterface { if (result === false) break; } } - destroy(): void { - this.listeners = []; - } } export default Delete; diff --git a/packages/engine/src/typing/keydown/enter.ts b/packages/engine/src/typing/keydown/enter.ts index 751b2967..8b4bfaa9 100644 --- a/packages/engine/src/typing/keydown/enter.ts +++ b/packages/engine/src/typing/keydown/enter.ts @@ -1,31 +1,9 @@ -import { - EngineInterface, - EventListener, - TypingHandleInterface, -} from '../../types'; +import { TypingHandleInterface } from '../../types'; +import DefaultKeydown from './default'; -class Enter implements TypingHandleInterface { +class Enter extends DefaultKeydown implements TypingHandleInterface { type: 'keydown' | 'keyup' = 'keydown'; hotkey: Array | string = 'enter'; - listeners: Array = []; - private engine: EngineInterface; - - constructor(engine: EngineInterface) { - this.engine = engine; - } - - on(listener: EventListener) { - this.listeners.push(listener); - } - - off(listener: EventListener) { - for (let i = 0; i < this.listeners.length; i++) { - if (this.listeners[i] === listener) { - this.listeners.splice(i, 1); - break; - } - } - } trigger(event: KeyboardEvent) { const { change } = this.engine; @@ -51,10 +29,6 @@ class Enter implements TypingHandleInterface { ); this.engine.trigger('select'); } - - destroy() { - this.listeners = []; - } } export default Enter; diff --git a/packages/engine/src/typing/keydown/left.ts b/packages/engine/src/typing/keydown/left.ts index 04a28d15..8577b091 100644 --- a/packages/engine/src/typing/keydown/left.ts +++ b/packages/engine/src/typing/keydown/left.ts @@ -1,39 +1,12 @@ import isHotkey from 'is-hotkey'; -import { EventListener, TypingHandleInterface } from '../../types'; - -class Left implements TypingHandleInterface { +import { TypingHandleInterface } from '../../types'; +import Default from './default'; +class Left extends Default implements TypingHandleInterface { type: 'keydown' | 'keyup' = 'keydown'; hotkey = (event: KeyboardEvent) => isHotkey('left', event) || isHotkey('shift+left', event) || isHotkey('ctrl+a', event) || isHotkey('ctrl+b', event); - - listeners: Array = []; - - on(listener: EventListener) { - this.listeners.push(listener); - } - - off(listener: EventListener) { - for (let i = 0; i < this.listeners.length; i++) { - if (this.listeners[i] === listener) { - this.listeners.splice(i, 1); - break; - } - } - } - - trigger(event: KeyboardEvent) { - for (let i = 0; i < this.listeners.length; i++) { - const listener = this.listeners[i]; - const result = listener(event); - if (result === false) break; - } - } - - destroy() { - this.listeners = []; - } } export default Left; diff --git a/packages/engine/src/typing/keydown/right.ts b/packages/engine/src/typing/keydown/right.ts index 308dff85..50b31c1e 100644 --- a/packages/engine/src/typing/keydown/right.ts +++ b/packages/engine/src/typing/keydown/right.ts @@ -1,48 +1,13 @@ import isHotkey from 'is-hotkey'; -import { - EngineInterface, - EventListener, - TypingHandleInterface, -} from '../../types'; +import { TypingHandleInterface } from '../../types'; +import DefaultKeydown from './default'; -class Right implements TypingHandleInterface { +class Right extends DefaultKeydown implements TypingHandleInterface { type: 'keydown' | 'keyup' = 'keydown'; hotkey = (event: KeyboardEvent) => isHotkey('right', event) || isHotkey('shift+right', event) || isHotkey('ctrl+e', event) || isHotkey('ctrl+f', event); - - private engine: EngineInterface; - listeners: Array = []; - - constructor(engine: EngineInterface) { - this.engine = engine; - } - - on(listener: EventListener) { - this.listeners.push(listener); - } - - off(listener: EventListener) { - for (let i = 0; i < this.listeners.length; i++) { - if (this.listeners[i] === listener) { - this.listeners.splice(i, 1); - break; - } - } - } - - trigger(event: KeyboardEvent) { - for (let i = 0; i < this.listeners.length; i++) { - const listener = this.listeners[i]; - const result = listener(event); - if (result === false) break; - } - } - - destroy() { - this.listeners = []; - } } export default Right; diff --git a/packages/engine/src/typing/keydown/shift-enter.ts b/packages/engine/src/typing/keydown/shift-enter.ts index 41053738..b18b2a67 100644 --- a/packages/engine/src/typing/keydown/shift-enter.ts +++ b/packages/engine/src/typing/keydown/shift-enter.ts @@ -1,33 +1,11 @@ -import { - EngineInterface, - EventListener, - TypingHandleInterface, -} from '../../types'; +import { TypingHandleInterface } from '../../types'; import { $ } from '../../node'; +import DefaultKeydown from './default'; -class ShitEnter implements TypingHandleInterface { - private engine: EngineInterface; +class ShitEnter extends DefaultKeydown implements TypingHandleInterface { type: 'keydown' | 'keyup' = 'keydown'; hotkey: string | string[] | ((event: KeyboardEvent) => boolean) = 'shift+enter'; - listeners: Array = []; - - constructor(engine: EngineInterface) { - this.engine = engine; - } - - on(listener: EventListener) { - this.listeners.push(listener); - } - - off(listener: EventListener) { - for (let i = 0; i < this.listeners.length; i++) { - if (this.listeners[i] === listener) { - this.listeners.splice(i, 1); - break; - } - } - } trigger(event: KeyboardEvent): void { const { change, inline, block } = this.engine; @@ -68,9 +46,6 @@ class ShitEnter implements TypingHandleInterface { this.engine.scrollNode, ); } - destroy(): void { - this.listeners = []; - } } export default ShitEnter; diff --git a/packages/engine/src/typing/keydown/tab.ts b/packages/engine/src/typing/keydown/tab.ts index 18576370..37883dd1 100644 --- a/packages/engine/src/typing/keydown/tab.ts +++ b/packages/engine/src/typing/keydown/tab.ts @@ -1,36 +1,14 @@ -import { EngineInterface, TypingHandleInterface } from '../../types'; +import { TypingHandleInterface } from '../../types'; +import DefaultKeydown from './default'; -class Tab implements TypingHandleInterface { - private engine: EngineInterface; +class Tab extends DefaultKeydown implements TypingHandleInterface { type: 'keydown' | 'keyup' = 'keydown'; hotkey: string | string[] | ((event: KeyboardEvent) => boolean) = 'tab'; - listeners: Array = []; - - constructor(engine: EngineInterface) { - this.engine = engine; - } - - on(listener: EventListener) { - this.listeners.push(listener); - } - - off(listener: EventListener) { - for (let i = 0; i < this.listeners.length; i++) { - if (this.listeners[i] === listener) { - this.listeners.splice(i, 1); - break; - } - } - } trigger(event: KeyboardEvent): void { const { node } = this.engine; event.preventDefault(); node.insertText(' '); } - - destroy(): void { - this.listeners = []; - } } export default Tab; diff --git a/packages/engine/src/typing/keyup/backspace.ts b/packages/engine/src/typing/keyup/backspace.ts index 3c815b1d..692fca78 100644 --- a/packages/engine/src/typing/keyup/backspace.ts +++ b/packages/engine/src/typing/keyup/backspace.ts @@ -1,30 +1,9 @@ -import { - EngineInterface, - EventListener, - TypingHandleInterface, -} from '../../types'; +import { TypingHandleInterface } from '../../types'; +import DefaultKeyup from './default'; -class Backspace implements TypingHandleInterface { +class Backspace extends DefaultKeyup implements TypingHandleInterface { type: 'keydown' | 'keyup' = 'keyup'; hotkey: Array | string = 'backspace'; - private engine: EngineInterface; - listeners: Array = []; - constructor(engine: EngineInterface) { - this.engine = engine; - } - - on(listener: EventListener) { - this.listeners.push(listener); - } - - off(listener: EventListener) { - for (let i = 0; i < this.listeners.length; i++) { - if (this.listeners[i] === listener) { - this.listeners.splice(i, 1); - break; - } - } - } trigger(event: KeyboardEvent) { const { change } = this.engine; @@ -41,10 +20,6 @@ class Backspace implements TypingHandleInterface { if (result === false) break; } } - - destroy() { - this.listeners = []; - } } export default Backspace; diff --git a/packages/engine/src/utils/index.ts b/packages/engine/src/utils/index.ts index 6c5af09b..fc20b61c 100644 --- a/packages/engine/src/utils/index.ts +++ b/packages/engine/src/utils/index.ts @@ -1,4 +1,4 @@ -import { EditorInterface, EngineInterface } from '../types'; +import { EditorInterface, EngineInterface, ViewInterface } from '../types'; import TinyCanvas from './tiny-canvas'; export * from './string'; export * from './user-agent'; @@ -15,3 +15,10 @@ export const isEngine = ( ): editor is EngineInterface => { return editor.kind === 'engine'; }; +/** + * 是否是View + * @param editor 编辑器 + */ +export const isView = (editor: EditorInterface): editor is ViewInterface => { + return editor.kind === 'view'; +}; diff --git a/packages/engine/src/view.ts b/packages/engine/src/view.ts index bbec1c1f..c27eb534 100644 --- a/packages/engine/src/view.ts +++ b/packages/engine/src/view.ts @@ -1,152 +1,17 @@ -import NodeModel, { Event, $ } from './node'; -import language from './locales'; -import { - EventInterface, - NodeInterface, - Selector, - EventListener, - NodeModelInterface, -} from './types/node'; -import schemaDefaultData from './constants/schema'; -import Schema from './schema'; -import Conversion from './parser/conversion'; -import { ViewInterface, ContentViewOptions } from './types/view'; -import { CardModelInterface } from './types/card'; -import { PluginModelInterface } from './types/plugin'; -import { SchemaInterface } from './types/schema'; -import { ConversionInterface } from './types/conversion'; -import CardModel from './card'; -import PluginModel from './plugin'; -import { ClipboardInterface } from './types/clipboard'; -import Clipboard from './clipboard'; -import { LanguageInterface } from './types/language'; -import Language from './language'; +import { ViewInterface, ViewOptions } from './types/view'; import Parser from './parser'; -import { - CommandInterface, - MarkModelInterface, - NodeIdInterface, - RequestInterface, -} from './types'; -import { BlockModelInterface } from './types/block'; -import { InlineModelInterface } from './types/inline'; -import { ListModelInterface } from './types/list'; -import List from './list'; -import Mark from './mark'; -import Inline from './inline'; -import Block from './block'; -import Command from './command'; -import Request from './request'; -import NodeId from './node/id'; -import { DATA_ELEMENT, ROOT } from './constants'; +import Editor from './editor'; +import { Selector } from './types'; -class View implements ViewInterface { - private options: ContentViewOptions = { - lang: 'zh-CN', - plugins: [], - cards: [], - }; +class View + extends Editor + implements ViewInterface +{ readonly kind = 'view'; - root: NodeInterface; - language: LanguageInterface; - container: NodeInterface; - card: CardModelInterface; - plugin: PluginModelInterface; - node: NodeModelInterface; - list: ListModelInterface; - mark: MarkModelInterface; - inline: InlineModelInterface; - block: BlockModelInterface; - clipboard: ClipboardInterface; - event: EventInterface; - schema: SchemaInterface; - conversion: ConversionInterface; - command: CommandInterface; - request: RequestInterface; - nodeId: NodeIdInterface; - #_scrollNode: NodeInterface | null = null; - constructor(selector: Selector, options?: ContentViewOptions) { - this.options = { ...this.options, ...options }; - this.language = new Language(this.options.lang || 'zh-CN', language); - this.event = new Event(); - this.command = new Command(this); - this.schema = new Schema(); - this.schema.add(schemaDefaultData); - this.conversion = new Conversion(this); - this.card = new CardModel(this, this.options.lazyRender); - this.clipboard = new Clipboard(this); - this.plugin = new PluginModel(this); - this.node = new NodeModel(this); - this.nodeId = new NodeId(this); - this.list = new List(this); - this.mark = new Mark(this); - this.inline = new Inline(this); - this.block = new Block(this); - this.clipboard = new Clipboard(this); - this.request = new Request(); - this.container = $(selector); - this.root = $( - this.options.root || this.container.parent() || document.body, - ); - this.container.addClass('am-engine-view'); - this.container.attributes(DATA_ELEMENT, ROOT); - this.mark.init(); - this.inline.init(); - this.block.init(); - this.list.init(); - this.card.init(this.options.cards || []); - this.plugin.init(this.options.plugins || [], this.options.config || {}); - this.nodeId.init(); - } - - setScrollNode(node?: HTMLElement) { - this.#_scrollNode = node ? $(node) : null; - } - - get scrollNode(): NodeInterface | null { - if (this.#_scrollNode) return this.#_scrollNode; - const { scrollNode } = this.options; - let sn = scrollNode - ? typeof scrollNode === 'function' - ? scrollNode() - : scrollNode - : null; - // 查找父级样式 overflow 或者 overflow-y 为 auto 或者 scroll 的节点 - const targetValues = ['auto', 'scroll']; - let parent = this.container.parent(); - while (!sn && parent && parent.length > 0 && parent.name !== 'body') { - if ( - targetValues.includes(parent.css('overflow')) || - targetValues.includes(parent.css('overflow-y')) - ) { - sn = parent.get(); - break; - } else { - parent = parent.parent(); - } - } - if (sn === null) sn = document.documentElement; - this.#_scrollNode = sn ? $(sn) : null; - return this.#_scrollNode; - } - - on = EventListener>( - eventType: string, - listener: F, - rewrite?: boolean, - ) { - this.event.on(eventType, listener, rewrite); - return this; - } - - off(eventType: string, listener: EventListener) { - this.event.off(eventType, listener); - return this; - } - - trigger(eventType: string, ...args: any): R { - return this.event.trigger(eventType, ...args); + constructor(selector: Selector, options?: ViewOptions) { + super(selector, options); + this.init(); } render(content: string, trigger: boolean = true) { @@ -157,19 +22,6 @@ class View implements ViewInterface { if (trigger) this.trigger('render', this.container); }); } - - messageSuccess(message: string) { - console.log(`success:${message}`); - } - - messageError(error: string) { - console.log(`error:${error}`); - } - - messageConfirm(message: string): Promise { - console.log(`confirm:${message}`); - return Promise.reject(false); - } } export default View; diff --git a/packages/toolbar-vue/src/plugin/component/popup.ts b/packages/toolbar-vue/src/plugin/component/popup.ts index e6f38544..013e6645 100644 --- a/packages/toolbar-vue/src/plugin/component/popup.ts +++ b/packages/toolbar-vue/src/plugin/component/popup.ts @@ -19,7 +19,7 @@ export default class Popup { constructor(editor: EditorInterface, options: PopupOptions = {}) { this.#options = options; this.#editor = editor; - this.#root = $(`
Test
`); + this.#root = $(`
`); document.body.append(this.#root[0]); if (isEngine(editor)) { this.#editor.on('select', this.onSelect); @@ -127,7 +127,7 @@ export default class Popup { destroy() { this.#root.remove(); if (isEngine(this.#editor)) { - this.#editor.on('select', this.onSelect); + this.#editor.off('select', this.onSelect); } else { document.removeEventListener('selectionchange', this.onSelect); } diff --git a/packages/toolbar-vue/src/plugin/index.ts b/packages/toolbar-vue/src/plugin/index.ts index 67e48cc8..d50659b0 100644 --- a/packages/toolbar-vue/src/plugin/index.ts +++ b/packages/toolbar-vue/src/plugin/index.ts @@ -49,21 +49,22 @@ class ToolbarPlugin< static get pluginName() { return 'toolbar'; } + private popup?: ToolbarPopup; init() { if (isEngine(this.editor)) { - this.editor.on('keydown:slash', (event) => this.onSlash(event)); - this.editor.on('parse:value', (node) => this.paserValue(node)); + this.editor.on('keydown:slash', this.onSlash); + this.editor.on('parse:value', this.paserValue); } this.editor.language.add(locales); if (this.options.popup) { - new ToolbarPopup(this.editor, { + this.popup = new ToolbarPopup(this.editor, { items: this.options.popup.items, }); } } - paserValue(node: NodeInterface) { + paserValue = (node: NodeInterface) => { if ( node.isCard() && node.attributes('name') === ToolbarComponent.cardName @@ -71,9 +72,9 @@ class ToolbarPlugin< return false; } return true; - } + }; - onSlash(event: KeyboardEvent) { + onSlash = (event: KeyboardEvent) => { if (!isEngine(this.editor)) return; const { change } = this.editor; let range = change.range.get(); @@ -108,11 +109,17 @@ class ToolbarPlugin< change.range.select(range); } } - } + }; execute(...args: any): void { throw new Error('Method not implemented.'); } + + destroy() { + this.popup?.destroy(); + this.editor.off('keydown:slash', this.onSlash); + this.editor.off('parse:value', this.paserValue); + } } export { ToolbarComponent }; export type { ToolbarValue }; diff --git a/packages/toolbar/src/plugin/component/popup.tsx b/packages/toolbar/src/plugin/component/popup.tsx index 40fcf24b..dd93957c 100644 --- a/packages/toolbar/src/plugin/component/popup.tsx +++ b/packages/toolbar/src/plugin/component/popup.tsx @@ -19,7 +19,7 @@ export default class Popup { constructor(editor: EditorInterface, options: PopupOptions = {}) { this.#options = options; this.#editor = editor; - this.#root = $(`
Test
`); + this.#root = $(`
`); document.body.append(this.#root[0]); if (isEngine(editor)) { this.#editor.on('select', this.onSelect); @@ -32,6 +32,7 @@ export default class Popup { } onSelect = () => { + if (this.#root.length === 0) return; const range = Range.from(this.#editor) ?.cloneRange() .shrinkToTextNode(); @@ -48,10 +49,8 @@ export default class Popup { return; } const subRanges = range.getSubRanges(); - if ( - subRanges.length === 0 || - (this.#editor.card.active && !this.#editor.card.active.isEditable) - ) { + const activeCard = this.#editor.card.active; + if (subRanges.length === 0 || (activeCard && !activeCard.isEditable)) { this.hide(); return; } @@ -132,7 +131,7 @@ export default class Popup { destroy() { this.#root.remove(); if (isEngine(this.#editor)) { - this.#editor.on('select', this.onSelect); + this.#editor.off('select', this.onSelect); } else { document.removeEventListener('selectionchange', this.onSelect); } diff --git a/packages/toolbar/src/plugin/index.ts b/packages/toolbar/src/plugin/index.ts index 8f499643..597338ef 100644 --- a/packages/toolbar/src/plugin/index.ts +++ b/packages/toolbar/src/plugin/index.ts @@ -54,20 +54,22 @@ class ToolbarPlugin< return 'toolbar'; } + private popup?: ToolbarPopup; + init() { if (isEngine(this.editor)) { - this.editor.on('keydown:slash', (event) => this.onSlash(event)); - this.editor.on('parse:value', (node) => this.paserValue(node)); + this.editor.on('keydown:slash', this.onSlash); + this.editor.on('parse:value', this.paserValue); } this.editor.language.add(locales); if (this.options.popup) { - new ToolbarPopup(this.editor, { + this.popup = new ToolbarPopup(this.editor, { items: this.options.popup.items, }); } } - paserValue(node: NodeInterface) { + paserValue = (node: NodeInterface) => { if ( node.isCard() && node.attributes('name') === ToolbarComponent.cardName @@ -75,9 +77,9 @@ class ToolbarPlugin< return false; } return true; - } + }; - onSlash(event: KeyboardEvent) { + onSlash = (event: KeyboardEvent) => { if (!isEngine(this.editor)) return; const { change } = this.editor; let range = change.range.get(); @@ -113,11 +115,17 @@ class ToolbarPlugin< change.range.select(range); } } - } + }; execute(...args: any): void { throw new Error('Method not implemented.'); } + + destroy() { + this.popup?.destroy(); + this.editor.off('keydown:slash', this.onSlash); + this.editor.off('parse:value', this.paserValue); + } } export { ToolbarComponent, ToolbarPopup }; export type { ToolbarValue }; diff --git a/plugins/alignment/src/index.ts b/plugins/alignment/src/index.ts index 8fe8eccb..d2a90bcd 100644 --- a/plugins/alignment/src/index.ts +++ b/plugins/alignment/src/index.ts @@ -34,7 +34,11 @@ export default class extends ElementPlugin { init() { super.init(); - this.editor.on('keydown:backspace', (event) => this.onBackspace(event)); + this.editor.on('keydown:backspace', this.onBackspace); + } + + destroy() { + this.editor.off('keydown:backspace', this.onBackspace); } execute(align?: 'left' | 'center' | 'right' | 'justify') { @@ -93,7 +97,7 @@ export default class extends ElementPlugin { ]; } - onBackspace(event: KeyboardEvent) { + onBackspace = (event: KeyboardEvent) => { if (!isEngine(this.editor)) return; const { change, block } = this.editor; const range = change.range.get(); @@ -123,5 +127,5 @@ export default class extends ElementPlugin { return false; } return; - } + }; } diff --git a/plugins/mark-range/src/index.ts b/plugins/mark-range/src/index.ts index 55eefd7e..df5a7698 100644 --- a/plugins/mark-range/src/index.ts +++ b/plugins/mark-range/src/index.ts @@ -1,4 +1,4 @@ -import { +import Engine, { $, CardEntry, DATA_TRANSIENT_ATTRIBUTES, @@ -16,6 +16,9 @@ import { EDITABLE_SELECTOR, CARD_SELECTOR, transformCustomTags, + isView, + View, + EditorInterface, } from '@aomao/engine'; import { Path } from 'sharedb'; @@ -93,7 +96,7 @@ export default class extends MarkPlugin { this.editor.on('change', (_, trigger) => { this.triggerChange(trigger !== 'local'); }); - this.editor.on('select', () => this.onSelectionChange()); + this.editor.on('select', this.onSelectionChange); this.editor.on('parse:value', (node, atts) => { const key = node.attributes(this.MARK_KEY); if (!!key) { @@ -129,10 +132,10 @@ export default class extends MarkPlugin { } return; }); - } else { + } else if (isView(this.editor)) { this.editor.container.document?.addEventListener( 'selectionchange', - () => this.onSelectionChange(), + this.onSelectionChange, ); } } @@ -553,7 +556,7 @@ export default class extends MarkPlugin { * 光标选择改变触发 * @returns */ - onSelectionChange() { + onSelectionChange = () => { if (this.executeBySelf) return; const { window } = this.editor.container; const selection = window?.getSelection(); @@ -577,7 +580,7 @@ export default class extends MarkPlugin { const selectInfo = this.getSelectInfo(range, true); this.editor.trigger(`${PLUGIN_NAME}:select`, range, selectInfo); this.range = range; - } + }; triggerChange(remote: boolean = false) { const addIds: { [key: string]: Array } = {}; @@ -635,7 +638,6 @@ export default class extends MarkPlugin { value: string; paths: Array<{ id: Array; path: Array }>; } { - const { node, card } = this.editor; const container = this.editor.container.clone(value ? false : true); container.css({ position: 'fixed', @@ -643,20 +645,23 @@ export default class extends MarkPlugin { clip: 'rect(0, 0, 0, 0)', }); $(document.body).append(container); + + const editor: EditorInterface = isEngine(this.editor) + ? new Engine(container, this.editor.options) + : new View(container, this.editor.options); + + const { node, card } = editor; if (value) container.html(transformCustomTags(value)); - card.render(container, undefined, false); - const selection = container.window?.getSelection(); const range = ( selection - ? Range.from(this.editor, selection) || - Range.create(this.editor) - : Range.create(this.editor) + ? Range.from(editor, selection) || Range.create(editor) + : Range.create(editor) ).cloneRange(); - const parser = new Parser(container, this.editor, undefined, false); - const { schema, conversion } = this.editor; + const parser = new Parser(container, editor, undefined, false); + const { schema, conversion } = editor; if (!range) { container.remove(); return { @@ -710,7 +715,7 @@ export default class extends MarkPlugin { cardNodes.each((_, index) => { const cardNode = cardNodes.eq(index); if (cardNode?.isEditableCard()) { - const card = this.editor.card.find(cardNode); + const card = editor.card.find(cardNode); if (card) { const value = card.getValue(); card.setValue(value || {}); @@ -718,6 +723,7 @@ export default class extends MarkPlugin { } }); value = parser.toValue(schema, conversion); + editor.destroy(); container.remove(); return { value, @@ -735,7 +741,6 @@ export default class extends MarkPlugin { paths: Array<{ id: Array; path: Array }>, value?: string, ): string { - const { card } = this.editor; const container = this.editor.container.clone(value ? false : true); if (value) value = Selection.removeTags(value); container.css({ @@ -744,19 +749,21 @@ export default class extends MarkPlugin { clip: 'rect(0, 0, 0, 0)', }); $(document.body).append(container); + const editor: EditorInterface = isEngine(this.editor) + ? new Engine(container, this.editor.options) + : new View(container, this.editor.options); + const { card } = editor; if (value) container.html(transformCustomTags(value)); - card.render(container, undefined, false); const selection = container.window?.getSelection(); const range = ( selection - ? Range.from(this.editor, selection) || - Range.create(this.editor) - : Range.create(this.editor) + ? Range.from(editor, selection) || Range.create(editor) + : Range.create(editor) ).cloneRange(); - const parser = new Parser(container, this.editor, undefined, false); - const { schema, conversion } = this.editor; + const parser = new Parser(container, editor, undefined, false); + const { schema, conversion } = editor; if (!range) { container.remove(); return value ? value : parser.toValue(schema, conversion); @@ -766,7 +773,7 @@ export default class extends MarkPlugin { (paths || []).forEach(({ id, path }) => { const pathRange = Range.fromPath( - this.editor, + editor, { start: { path: path[0] as number[], id: '', bi: -1 }, end: { path: path[1] as number[], id: '', bi: -1 }, @@ -795,7 +802,7 @@ export default class extends MarkPlugin { } } }); - this.editor.mark.wrap( + editor.mark.wrap( `<${this.tagName} ${this.MARK_KEY}="${key}" ${this.getIdName( key, )}="${id.join(',')}" />`, @@ -806,7 +813,7 @@ export default class extends MarkPlugin { cardNodes.each((_, index) => { const cardNode = cardNodes.eq(index); if (cardNode?.isEditableCard()) { - const card = this.editor.card.find(cardNode); + const card = editor.card.find(cardNode); if (card) { const value = card.getValue(); card.setValue(value || {}); @@ -814,7 +821,16 @@ export default class extends MarkPlugin { } }); value = parser.toValue(schema, conversion); + editor.destroy(); container.remove(); return value; } + + destroy() { + this.editor.off('select', this.onSelectionChange); + this.editor.container.document?.removeEventListener( + 'selectionchange', + this.onSelectionChange, + ); + } } diff --git a/plugins/mention/README.md b/plugins/mention/README.md index a81ec0f4..99e8d9ae 100644 --- a/plugins/mention/README.md +++ b/plugins/mention/README.md @@ -195,9 +195,7 @@ this.engine.on('mention:render-item', (data, root) => { `mention:loading`: 自定渲染加载状态 ```ts -this.engine.on('mention:loading', (data, root) => { - root.html(`
${data}
`); - // or +this.engine.on('mention:loading', (root) => { ReactDOM.render(
Loading...
, root.get()!, diff --git a/plugins/mention/src/component/collapse.ts b/plugins/mention/src/component/collapse.ts index 1c3cfbdf..0ea0c64c 100644 --- a/plugins/mention/src/component/collapse.ts +++ b/plugins/mention/src/component/collapse.ts @@ -23,7 +23,7 @@ export interface CollapseComponentInterface { unbindEvents(): void; bindEvents(): void; remove(): void; - render(target: NodeInterface, data: Array): void; + render(target: NodeInterface, data: Array | true): void; } class CollapseComponent implements CollapseComponentInterface { @@ -221,7 +221,7 @@ class CollapseComponent implements CollapseComponentInterface { return this.root?.find('.data-mention-component-body'); } - render(target: NodeInterface, data: Array) { + render(target: NodeInterface, data: Array | true) { this.remove(); this.root = $( `
`, @@ -232,13 +232,10 @@ class CollapseComponent implements CollapseComponentInterface { let body = this.getBody(); let result = null; - if ( - CollapseComponent.renderLoading || - (result = this.engine.trigger('mention:loading', this.root)) - ) { + if (typeof data === 'boolean' && data === true) { result = CollapseComponent.renderLoading ? CollapseComponent.renderLoading(this.root) - : result; + : this.engine.trigger('mention:loading', this.root); body = this.getBody(); if (result) body?.append(result); } else if (data.filter((item) => !!item.key).length === 0) { diff --git a/plugins/mention/src/component/index.ts b/plugins/mention/src/component/index.ts index cf9e991a..d449792b 100644 --- a/plugins/mention/src/component/index.ts +++ b/plugins/mention/src/component/index.ts @@ -220,11 +220,11 @@ class Mention extends Card { this.component?.render(this.root, defaultData); return; } - //if (Mention.renderLoading) { + // if (Mention.renderLoading) { CollapseComponent.renderLoading = Mention.renderLoading; - this.component?.render(this.root, []); + this.component?.render(this.root, true); CollapseComponent.renderLoading = undefined; - //} + // } Mention.search(keyword).then((data) => { this.component?.render(this.root, data); }); @@ -283,7 +283,7 @@ class Mention extends Card { const children = this.#container.children(); if (!mark) { // 移除所有标记 - this.editor.mark.unwrapByNodes(this.queryMarks()); + this.editor.mark.unwrapByNodes(this.queryMarks(false)); this.setValue({ marks: [] as string[], } as T); @@ -301,7 +301,7 @@ class Mention extends Card { } as T); } else { // 移除标记 - this.editor.mark.unwrapByNodes(this.queryMarks(), mark); + this.editor.mark.unwrapByNodes(this.queryMarks(false), mark); const marks = this.queryMarks().map( (child) => child.get()?.outerHTML || '', ); @@ -311,12 +311,12 @@ class Mention extends Card { } } - queryMarks() { + queryMarks(clone: boolean = true) { if (!this.#container) return []; return this.#container .allChildren() .filter((child) => child.isElement()) - .map((c) => c.clone()); + .map((c) => (clone ? c.clone() : c)); } render(): string | void | NodeInterface { diff --git a/plugins/status/src/components/index.ts b/plugins/status/src/components/index.ts index 85bb27b2..0a0110b3 100644 --- a/plugins/status/src/components/index.ts +++ b/plugins/status/src/components/index.ts @@ -177,7 +177,7 @@ class Status extends Card { const children = this.#container.children(); if (!mark) { // 移除所有标记 - this.editor.mark.unwrapByNodes(this.queryMarks()); + this.editor.mark.unwrapByNodes(this.queryMarks(false)); this.setValue({ marks: [] as string[], } as T); @@ -199,7 +199,7 @@ class Status extends Card { } as T); } else { // 移除标记 - this.editor.mark.unwrapByNodes(this.queryMarks(), mark); + this.editor.mark.unwrapByNodes(this.queryMarks(false), mark); const marks = this.queryMarks().map( (child) => child.get()?.outerHTML || '', ); @@ -210,12 +210,12 @@ class Status extends Card { this.#statusEditor?.updateActive(this.getColor()); } - queryMarks() { + queryMarks(clone: boolean = true) { if (!this.#container) return []; return this.#container .allChildren() .filter((child) => child.isElement()) - .map((c) => c.clone()); + .map((c) => (clone ? c.clone() : c)); } focusEditor() { diff --git a/plugins/table/src/component/helper.ts b/plugins/table/src/component/helper.ts index b8398abb..75023119 100644 --- a/plugins/table/src/component/helper.ts +++ b/plugins/table/src/component/helper.ts @@ -3,6 +3,7 @@ import { TableModel, TableModelCol, TableModelEmptyCol, + TableOptions, } from '../types'; import isInteger from 'lodash/isInteger'; import { @@ -12,6 +13,7 @@ import { isNode, NodeInterface, transformCustomTags, + EditorInterface, } from '@aomao/engine'; import Template from './template'; @@ -21,6 +23,12 @@ class Helper implements HelperInterface { text: string; }; + #editor: EditorInterface; + + constructor(editor: EditorInterface) { + this.#editor = editor; + } + isEmptyModelCol( model: TableModelCol | TableModelEmptyCol, ): model is TableModelEmptyCol { @@ -251,7 +259,11 @@ class Helper implements HelperInterface { const $tr = trs.eq(index); if (!$tr) return; let height = parseInt($tr.css('height')); - height = height || 35; + height = + height || + this.#editor.plugin.findPlugin('table')?.options + .rowMinHeight || + 0; $tr.css('height', height + 'px'); }); //补充可编辑器区域 @@ -603,7 +615,11 @@ class Helper implements HelperInterface { const $tr = trs.eq(index); if (!$tr) return; let height = parseInt($tr.css('height')); - height = height || 35; + height = + height || + this.#editor.plugin.findPlugin('table')?.options + .rowMinHeight || + 0; $tr.css('height', height + 'px'); }); return table; diff --git a/plugins/table/src/component/index.ts b/plugins/table/src/component/index.ts index 37e5b39a..35743c55 100644 --- a/plugins/table/src/component/index.ts +++ b/plugins/table/src/component/index.ts @@ -19,6 +19,7 @@ import { HelperInterface, TableCommandInterface, TableInterface, + TableOptions, TableSelectionInterface, TableValue, TemplateInterface, @@ -33,7 +34,7 @@ import { ColorTool, Palette } from './toolbar'; class TableComponent extends Card - implements TableInterface + implements TableInterface { readonly contenteditable: string[] = [ `div${Template.TABLE_TD_CONTENT_CLASS}`, @@ -65,13 +66,20 @@ class TableComponent }), ); + colMinWidth = + this.editor.plugin.findPlugin('table')?.options + .colMinWidth || 40; + rowMinHeight = + this.editor.plugin.findPlugin('table')?.options + .rowMinHeight || 35; + wrapper?: NodeInterface; - helper: HelperInterface = new Helper(); + helper: HelperInterface = new Helper(this.editor); template: TemplateInterface = new Template(this); selection: TableSelectionInterface = new TableSelection(this.editor, this); conltrollBar: ControllBarInterface = new ControllBar(this.editor, this, { - col_min_width: 40, - row_min_height: 35, + col_min_width: this.colMinWidth, + row_min_height: this.rowMinHeight, }); command: TableCommandInterface = new TableCommand(this.editor, this); scrollbar?: Scrollbar; @@ -337,6 +345,38 @@ class TableComponent return toolbars; } + onSelectLeft(event: KeyboardEvent) { + const { tableModel } = this.selection; + if (!tableModel) return; + for (let r = tableModel.rows - 1; r >= 0; r--) { + for (let c = tableModel.cols - 1; c >= 0; c--) { + const cell = tableModel.table[r][c]; + if (!this.helper.isEmptyModelCol(cell) && cell.element) { + event.preventDefault(); + this.selection.focusCell(cell.element, false); + return false; + } + } + } + return; + } + + onSelectRight(event: KeyboardEvent) { + const { tableModel } = this.selection; + if (!tableModel) return; + for (let r = 0; r < tableModel.rows; r++) { + for (let c = 0; c < tableModel.cols; c++) { + const cell = tableModel.table[r][c]; + if (!this.helper.isEmptyModelCol(cell) && cell.element) { + event.preventDefault(); + this.selection.focusCell(cell.element); + return false; + } + } + } + return; + } + updateAlign(event: MouseEvent, align: 'top' | 'middle' | 'bottom' = 'top') { event.preventDefault(); this.conltrollBar.setAlign(align); @@ -530,6 +570,7 @@ class TableComponent const tablePlugin = this.editor.plugin.components['table']; const tableOptions = tablePlugin?.options['overflow'] || {}; if (this.viewport) { + this.selection.refreshModel(); const overflowLeftConfig = tableOptions['maxLeftWidth'] ? { onScrollX: (x: number) => { diff --git a/plugins/table/src/index.ts b/plugins/table/src/index.ts index 865c03d0..dee8c900 100644 --- a/plugins/table/src/index.ts +++ b/plugins/table/src/index.ts @@ -16,17 +16,8 @@ import { } from '@aomao/engine'; import TableComponent, { Template, Helper } from './component'; import locales from './locale'; -import { TableInterface, TableValue } from './types'; +import { TableInterface, TableOptions, TableValue } from './types'; import './index.css'; -export interface TableOptions extends PluginOptions { - hotkey?: string | Array; - overflow?: { - maxLeftWidth?: () => number; - maxRightWidth?: () => number; - }; - markdown?: boolean; -} - class Table extends Plugin { static get pluginName() { return 'table'; @@ -294,7 +285,7 @@ class Table extends Plugin { if (width.endsWith('pt')) node.css(type, this.convertToPX(width)); }; const tables = root.find('table'); - const helper = new Helper(); + const helper = new Helper(this.editor); tables.each((_, index) => { let node = tables.eq(index); if (!node) return; @@ -554,4 +545,4 @@ class Table extends Plugin { export default Table; export { TableComponent }; -export type { TableValue }; +export type { TableValue, TableOptions }; diff --git a/plugins/table/src/types.ts b/plugins/table/src/types.ts index c5bf5793..9d159dbd 100644 --- a/plugins/table/src/types.ts +++ b/plugins/table/src/types.ts @@ -3,6 +3,7 @@ import { CardValue, ClipboardData, NodeInterface, + PluginOptions, } from '@aomao/engine'; import { EventEmitter2 } from 'eventemitter2'; @@ -149,19 +150,33 @@ export type TableModel = { table: Array>; }; -export interface TableInterface extends CardInterface { +export interface TableInterface + extends CardInterface { wrapper?: NodeInterface; helper: HelperInterface; template: TemplateInterface; selection: TableSelectionInterface; conltrollBar: ControllBarInterface; command: TableCommandInterface; + colMinWidth: number; + rowMinHeight: number; /** * 渲染 */ render(): string | NodeInterface | void; } +export interface TableOptions extends PluginOptions { + hotkey?: string | Array; + overflow?: { + maxLeftWidth?: () => number; + maxRightWidth?: () => number; + }; + colMinWidth?: number; + rowMinHeight?: number; + markdown?: boolean; +} + export type ControllOptions = { col_min_width: number; row_min_height: number;