This commit is contained in:
yanmao 2021-12-30 17:49:32 +08:00
parent 012771fccc
commit cb6e0dd7e9
65 changed files with 1143 additions and 1233 deletions

View File

@ -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(`<div>${data}</div>`);
// or
this.engine.on('mention:loading', (root) => {
ReactDOM.render(
<div className="data-mention-loading">Loading...</div>,
root.get<HTMLElement>()!,

View File

@ -195,9 +195,7 @@ this.engine.on('mention:render-item', (data, root) => {
`mention:loading`: 自定渲染加载状态
```ts
this.engine.on('mention:loading', (data, root) => {
root.html(`<div>${data}</div>`);
// or
this.engine.on('mention:loading', (root) => {
ReactDOM.render(
<div className="data-mention-loading">Loading...</div>,
root.get<HTMLElement>()!,

View File

@ -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<T extends CardValue = {}> implements CardInterface<T> {
abstract class CardEntry<T extends CardValue = CardValue>
implements CardInterface<T>
{
protected readonly editor: EditorInterface;
readonly root: NodeInterface;
toolbarModel?: CardToolbarInterface;
@ -323,6 +325,10 @@ abstract class CardEntry<T extends CardValue = {}> implements CardInterface<T> {
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();

View File

@ -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<HTMLElement>()

View File

@ -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';

View File

@ -63,7 +63,7 @@ class Resize implements ResizeInterface {
if (start) {
this.card.setValue({
height: container.height(),
});
} as any);
start = false;
}
},

View File

@ -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';

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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';

View File

@ -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';
/**

View File

@ -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<T extends EditorOptions = EditorOptions>
implements EditorInterface<T>
{
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<HTMLElement>();
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<R = any, F extends EventListener<R> = EventListener<R>>(
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<R = any>(eventType: string, ...args: any): R {
return this.event.trigger<R>(eventType, ...args);
}
messageSuccess(message: string) {
console.log(`success:${message}`);
}
messageError(error: string) {
console.log(`error:${error}`);
}
messageConfirm(message: string): Promise<boolean> {
console.log(`confirm:${message}`);
return Promise.reject(false);
}
destroy() {
this.event.destroy();
this.plugin.destroy();
this.card.destroy();
this.container.empty();
}
}
export default Editor;

View File

@ -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<T extends EngineOptions = EngineOptions>
extends Editor<T>
implements EngineInterface<T>
{
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<HTMLElement>();
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<R = any, F extends EventListener<R> = EventListener<R>>(
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<R = any>(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<boolean> {
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();
}
}

View File

@ -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,
};

View File

@ -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';

View File

@ -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;

View File

@ -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,

View File

@ -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<T extends PluginOptions = {}>
abstract class PluginEntry<T extends PluginOptions = PluginOptions>
implements PluginInterface<T>
{
protected readonly editor: EditorInterface;
@ -55,6 +55,7 @@ abstract class PluginEntry<T extends PluginOptions = {}>
...args: any
) => boolean | number | void,
): Promise<void>;
destroy?(): void;
}
export default PluginEntry;

View File

@ -1,14 +1,15 @@
import ElementPluginEntry from './element';
import {
import type {
SchemaBlock,
BlockInterface,
NodeInterface,
PluginInterface,
PluginOptions,
} from '../types';
abstract class BlockEntry<T extends {} = {}>
abstract class BlockEntry<T extends PluginOptions = PluginOptions>
extends ElementPluginEntry<T>
implements BlockInterface
implements BlockInterface<T>
{
readonly kind: string = 'block';
/**

View File

@ -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<T extends PluginOptions>
abstract class ElementPluginEntry<T extends PluginOptions = PluginOptions>
extends PluginEntry<T>
implements ElementPluginInterface
implements ElementPluginInterface<T>
{
readonly kind: string = 'element';
/**

View File

@ -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<T extends PluginOptions = PluginOptions>(
pluginName: string,
): PluginInterface<T> | undefined {
const plugin = this.components[pluginName];
return plugin;
if (!plugin) return;
return plugin as PluginInterface<T>;
}
findElementPlugin(pluginName: string) {
const plugin = this.findPlugin(pluginName);
findElementPlugin<T extends PluginOptions = PluginOptions>(
pluginName: string,
): ElementPluginInterface<T> | undefined {
const plugin = this.findPlugin<T>(pluginName);
if (!plugin) return;
if (isElementPlugin(plugin)) {
return plugin as ElementPluginInterface;
return plugin as ElementPluginInterface<T>;
}
return;
}
findMarkPlugin(pluginName: string) {
findMarkPlugin<T extends PluginOptions = PluginOptions>(
pluginName: string,
): MarkInterface<T> | undefined {
const plugin = this.findPlugin(pluginName);
if (!plugin) return;
if (isMarkPlugin(plugin)) {
return plugin as MarkInterface;
return plugin as MarkInterface<T>;
}
return;
}
findInlinePlugin(pluginName: string) {
findInlinePlugin<T extends PluginOptions = PluginOptions>(
pluginName: string,
): InlineInterface<T> | undefined {
const plugin = this.findPlugin(pluginName);
if (!plugin) return;
if (isInlinePlugin(plugin)) {
return plugin as InlineInterface;
return plugin as InlineInterface<T>;
}
return;
}
findBlockPlugin(pluginName: string) {
findBlockPlugin<T extends PluginOptions = PluginOptions>(
pluginName: string,
): BlockInterface<T> | undefined {
const plugin = this.findPlugin(pluginName);
if (!plugin) return;
if (isBlockPlugin(plugin)) {
return plugin as BlockInterface;
return plugin as BlockInterface<T>;
}
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;

View File

@ -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<T extends {} = {}>
abstract class InlineEntry<T extends PluginOptions = PluginOptions>
extends ElementPluginEntry<T>
implements InlineInterface
implements InlineInterface<T>
{
readonly kind: string = 'inline';
/**

View File

@ -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<T extends {} = {}>
abstract class ListEntry<T extends PluginOptions = PluginOptions>
extends BlockEntry<T>
implements ListInterface
implements ListInterface<T>
{
cardName?: string;
private isPasteList: boolean = false;

View File

@ -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<T extends {} = {}>
abstract class MarkEntry<T extends PluginOptions = PluginOptions>
extends ElementPluginEntry<T>
implements MarkInterface
implements MarkInterface<T>
{
readonly kind: string = 'mark';
/**

View File

@ -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';

View File

@ -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<T extends PluginOptions = PluginOptions>
extends ElementPluginInterface<T> {
readonly kind: string;
/**
*

View File

@ -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<T extends CardValue = {}> {
export interface CardOptions<T extends CardValue = CardValue> {
editor: EditorInterface;
value?: Partial<T>;
root?: NodeInterface;
@ -84,7 +84,7 @@ export type CardToolbarItemOptions =
items: Array<DropdownSwitchOptions | DropdownButtonOptions>;
};
export interface CardEntry<T extends CardValue = {}> {
export interface CardEntry<T extends CardValue = CardValue> {
prototype: CardInterface;
new (options: CardOptions<T>): CardInterface;
/**
@ -125,7 +125,7 @@ export interface CardEntry<T extends CardValue = {}> {
readonly lazyRender: boolean;
}
export interface CardInterface<T extends CardValue = {}> {
export interface CardInterface<T extends CardValue = CardValue> {
/**
*
*/
@ -158,6 +158,9 @@ export interface CardInterface<T extends CardValue = {}> {
*
*/
readonly contenteditable: Array<string>;
/**
*
*/
readonly loading: boolean;
/**
* card重新渲染
@ -254,6 +257,22 @@ export interface CardInterface<T extends CardValue = {}> {
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<V extends CardValue = {}>(
update<V extends CardValue = CardValue>(
selector: NodeInterface | Node | string,
value: Partial<V>,
...args: any

View File

@ -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<string, {}>;
/**
*
*/
plugins?: Array<PluginEntry>;
/**
*
*/
cards?: Array<CardEntry>;
/**
*
*/
config?: Record<string, PluginOptions>;
/**
*
*/
root?: Node;
/**
* overflow overflow-y auto scroll
*/
scrollNode?: Node | (() => Node | null);
/**
* lazyRender true
*/
lazyRender?: boolean;
}
export interface EditorInterface<T extends EditorOptions = EditorOptions> {
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<R = any, F extends EventListener<R> = EventListener<R>>(
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节点 XMLfalse跳过当前节点
* @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<string>,
) => boolean | void,
rewrite?: boolean,
): void;
/**
* DOM节点 XMLxml代码结束后触发
* @param value xml代码
*/
on(
eventType: 'parse:value-after',
listener: (value: Array<string>) => 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节点 XMLfalse跳过当前节点
* @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<string>,
) => boolean | void,
): void;
/**
* DOM节点 XMLxml代码结束后触发
* @param value xml代码
*/
off(
eventType: 'parse:value-after',
listener: (value: Array<string>) => 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<R = any>(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节点 XMLfalse跳过当前节点
* @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<string>,
): boolean | void;
/**
* DOM节点 XMLxml代码结束后触发
* @param value xml代码
*/
trigger(eventType: 'parse:value-after', value: Array<string>): 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<boolean>;
}

View File

@ -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<R = any, F extends EventListener<R> = EventListener<R>>(
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节点 XMLfalse跳过当前节点
* @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<string>,
) => boolean | void,
rewrite?: boolean,
): void;
/**
* DOM节点 XMLxml代码结束后触发
* @param value xml代码
*/
on(
eventType: 'parse:value-after',
listener: (value: Array<string>) => 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节点 XMLfalse跳过当前节点
* @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<string>,
) => boolean | void,
): void;
/**
* DOM节点 XMLxml代码结束后触发
* @param value xml代码
*/
off(
eventType: 'parse:value-after',
listener: (value: Array<string>) => 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<R = any>(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节点 XMLfalse跳过当前节点
* @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<string>,
): boolean | void;
/**
* DOM节点 XMLxml代码结束后触发
* @param value xml代码
*/
trigger(eventType: 'parse:value-after', value: Array<string>): 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<boolean>;
}
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<PluginEntry>;
/**
*
*/
cards?: Array<CardEntry>;
/**
*
*/
config?: { [k: string]: PluginOptions };
/**
*
*/
@ -479,24 +62,21 @@ export type EngineOptions = {
*
*/
readonly?: boolean;
/**
* lazyRender true
*/
lazyRender?: boolean;
};
}
export interface Engine {
export interface Engine<T extends EngineOptions = EngineOptions> {
/**
*
*/
new (selector: Selector, options?: EngineOptions): EngineInterface;
new (selector: Selector, options?: T): EngineInterface<T>;
}
export interface EngineInterface extends EditorInterface {
export interface EngineInterface<T extends EngineOptions = EngineOptions>
extends EditorInterface<T> {
/**
*
*/
options: EngineOptions;
options: T;
/**
*
*/

View File

@ -23,3 +23,4 @@ export * from './tiny-canvas';
export * from './parser';
export * from './resizer';
export * from './position';
export * from './editor';

View File

@ -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<T extends PluginOptions = PluginOptions>
extends ElementPluginInterface<T> {
readonly kind: string;
/**
*

View File

@ -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<T extends PluginOptions = PluginOptions>
extends BlockInterface<T> {
/**
*
*/

View File

@ -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<T extends PluginOptions = PluginOptions>
extends ElementPluginInterface<T> {
readonly kind: string;
/**
*

View File

@ -38,6 +38,10 @@ export interface EventInterface {
* @param args
*/
trigger<R = any>(eventType: string, ...args: any): R;
/**
*
*/
destroy(): void;
}
export type Selector =
| string

View File

@ -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<T extends PluginOptions = {}> {
export interface PluginInterface<T extends PluginOptions = PluginOptions> {
readonly kind: string;
readonly name: string;
/**
@ -77,9 +77,12 @@ export interface PluginInterface<T extends PluginOptions = {}> {
...args: any
) => boolean | number | void,
): Promise<void>;
destroy?(): void;
}
export interface ElementPluginInterface extends PluginInterface {
export interface ElementPluginInterface<T extends PluginOptions = PluginOptions>
extends PluginInterface<T> {
/**
*
*/
@ -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<T extends PluginOptions = PluginOptions>(
pluginName: string,
): PluginInterface<T> | undefined;
findElementPlugin<T extends PluginOptions = PluginOptions>(
pluginName: string,
): ElementPluginInterface<T> | undefined;
findMarkPlugin<T extends PluginOptions = PluginOptions>(
pluginName: string,
): MarkInterface<T> | undefined;
findInlinePlugin<T extends PluginOptions = PluginOptions>(
pluginName: string,
): InlineInterface<T> | undefined;
findBlockPlugin<T extends PluginOptions = PluginOptions>(
pluginName: string,
): BlockInterface<T> | undefined;
destroy(): void;
}

View File

@ -1,5 +1,5 @@
import { Path } from 'sharedb';
import { EditorInterface } from './engine';
import { EditorInterface } from './editor';
import { NodeInterface } from './node';
import { SelectionInterface } from './selection';

View File

@ -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<EventListener>;
listeners: Array<TypingEventListener>;
/**
* |
*/
@ -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

View File

@ -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<T extends ViewOptions = ViewOptions>
extends EditorInterface<T> {
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<PluginEntry>;
/**
*
*/
cards?: Array<CardEntry>;
/**
*
*/
config?: { [k: string]: {} };
/**
*
*/
root?: Node;
/**
* overflow overflow-y auto scroll
*/
scrollNode?: Node | (() => Node | null);
/**
* lazyRender true
*/
lazyRender?: boolean;
};

View File

@ -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> | string = 'backspace';
private engine: EngineInterface;
listeners: Array<EventListener> = [];
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;

View File

@ -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<EventListener> = [];
private engine: EngineInterface;
listeners: Array<TypingEventListener> = [];
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);

View File

@ -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<EventListener> = [];
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;

View File

@ -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> | string = 'enter';
listeners: Array<EventListener> = [];
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;

View File

@ -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<EventListener> = [];
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;

View File

@ -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<EventListener> = [];
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;

View File

@ -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<EventListener> = [];
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;

View File

@ -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<EventListener> = [];
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;

View File

@ -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> | string = 'backspace';
private engine: EngineInterface;
listeners: Array<EventListener> = [];
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;

View File

@ -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';
};

View File

@ -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<T extends ViewOptions = ViewOptions>
extends Editor<T>
implements ViewInterface<T>
{
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<HTMLElement>();
break;
} else {
parent = parent.parent();
}
}
if (sn === null) sn = document.documentElement;
this.#_scrollNode = sn ? $(sn) : null;
return this.#_scrollNode;
}
on<R = any, F extends EventListener<R> = EventListener<R>>(
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<R = any>(eventType: string, ...args: any): R {
return this.event.trigger<R>(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<boolean> {
console.log(`confirm:${message}`);
return Promise.reject(false);
}
}
export default View;

View File

@ -19,7 +19,7 @@ export default class Popup {
constructor(editor: EditorInterface, options: PopupOptions = {}) {
this.#options = options;
this.#editor = editor;
this.#root = $(`<div class="data-toolbar-popup-wrapper">Test</div>`);
this.#root = $(`<div class="data-toolbar-popup-wrapper"></div>`);
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);
}

View File

@ -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 };

View File

@ -19,7 +19,7 @@ export default class Popup {
constructor(editor: EditorInterface, options: PopupOptions = {}) {
this.#options = options;
this.#editor = editor;
this.#root = $(`<div class="data-toolbar-popup-wrapper">Test</div>`);
this.#root = $(`<div class="data-toolbar-popup-wrapper"></div>`);
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);
}

View File

@ -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 };

View File

@ -34,7 +34,11 @@ export default class<T extends AlignmentOptions> extends ElementPlugin<T> {
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<T extends AlignmentOptions> extends ElementPlugin<T> {
];
}
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<T extends AlignmentOptions> extends ElementPlugin<T> {
return false;
}
return;
}
};
}

View File

@ -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<T extends MarkRangeOptions> extends MarkPlugin<T> {
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<T extends MarkRangeOptions> extends MarkPlugin<T> {
}
return;
});
} else {
} else if (isView(this.editor)) {
this.editor.container.document?.addEventListener(
'selectionchange',
() => this.onSelectionChange(),
this.onSelectionChange,
);
}
}
@ -553,7 +556,7 @@ export default class<T extends MarkRangeOptions> extends MarkPlugin<T> {
*
* @returns
*/
onSelectionChange() {
onSelectionChange = () => {
if (this.executeBySelf) return;
const { window } = this.editor.container;
const selection = window?.getSelection();
@ -577,7 +580,7 @@ export default class<T extends MarkRangeOptions> extends MarkPlugin<T> {
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<string> } = {};
@ -635,7 +638,6 @@ export default class<T extends MarkRangeOptions> extends MarkPlugin<T> {
value: string;
paths: Array<{ id: Array<string>; path: Array<Path> }>;
} {
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<T extends MarkRangeOptions> extends MarkPlugin<T> {
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<T extends MarkRangeOptions> extends MarkPlugin<T> {
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<T extends MarkRangeOptions> extends MarkPlugin<T> {
}
});
value = parser.toValue(schema, conversion);
editor.destroy();
container.remove();
return {
value,
@ -735,7 +741,6 @@ export default class<T extends MarkRangeOptions> extends MarkPlugin<T> {
paths: Array<{ id: Array<string>; path: Array<Path> }>,
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<T extends MarkRangeOptions> extends MarkPlugin<T> {
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<T extends MarkRangeOptions> extends MarkPlugin<T> {
(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<T extends MarkRangeOptions> extends MarkPlugin<T> {
}
}
});
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<T extends MarkRangeOptions> extends MarkPlugin<T> {
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<T extends MarkRangeOptions> extends MarkPlugin<T> {
}
});
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,
);
}
}

View File

@ -195,9 +195,7 @@ this.engine.on('mention:render-item', (data, root) => {
`mention:loading`: 自定渲染加载状态
```ts
this.engine.on('mention:loading', (data, root) => {
root.html(`<div>${data}</div>`);
// or
this.engine.on('mention:loading', (root) => {
ReactDOM.render(
<div className="data-mention-loading">Loading...</div>,
root.get<HTMLElement>()!,

View File

@ -23,7 +23,7 @@ export interface CollapseComponentInterface {
unbindEvents(): void;
bindEvents(): void;
remove(): void;
render(target: NodeInterface, data: Array<MentionItem>): void;
render(target: NodeInterface, data: Array<MentionItem> | 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<MentionItem>) {
render(target: NodeInterface, data: Array<MentionItem> | true) {
this.remove();
this.root = $(
`<div class="data-mention-component-list" ${DATA_ELEMENT}="${UI}"><div class="data-mention-component-body"></div></div>`,
@ -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) {

View File

@ -220,11 +220,11 @@ class Mention<T extends MentionValue = MentionValue> extends Card<T> {
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<T extends MentionValue = MentionValue> extends Card<T> {
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<T extends MentionValue = MentionValue> extends Card<T> {
} 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<HTMLElement>()?.outerHTML || '',
);
@ -311,12 +311,12 @@ class Mention<T extends MentionValue = MentionValue> extends Card<T> {
}
}
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 {

View File

@ -177,7 +177,7 @@ class Status<T extends StatusValue = StatusValue> extends Card<T> {
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<T extends StatusValue = StatusValue> extends Card<T> {
} 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<HTMLElement>()?.outerHTML || '',
);
@ -210,12 +210,12 @@ class Status<T extends StatusValue = StatusValue> extends Card<T> {
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() {

View File

@ -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<TableOptions>('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<TableOptions>('table')?.options
.rowMinHeight ||
0;
$tr.css('height', height + 'px');
});
return table;

View File

@ -19,6 +19,7 @@ import {
HelperInterface,
TableCommandInterface,
TableInterface,
TableOptions,
TableSelectionInterface,
TableValue,
TemplateInterface,
@ -33,7 +34,7 @@ import { ColorTool, Palette } from './toolbar';
class TableComponent<V extends TableValue = TableValue>
extends Card<V>
implements TableInterface
implements TableInterface<V>
{
readonly contenteditable: string[] = [
`div${Template.TABLE_TD_CONTENT_CLASS}`,
@ -65,13 +66,20 @@ class TableComponent<V extends TableValue = TableValue>
}),
);
colMinWidth =
this.editor.plugin.findPlugin<TableOptions>('table')?.options
.colMinWidth || 40;
rowMinHeight =
this.editor.plugin.findPlugin<TableOptions>('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<V extends TableValue = TableValue>
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<V extends TableValue = TableValue>
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) => {

View File

@ -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<string>;
overflow?: {
maxLeftWidth?: () => number;
maxRightWidth?: () => number;
};
markdown?: boolean;
}
class Table<T extends TableOptions = TableOptions> extends Plugin<T> {
static get pluginName() {
return 'table';
@ -294,7 +285,7 @@ class Table<T extends TableOptions = TableOptions> extends Plugin<T> {
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<T extends TableOptions = TableOptions> extends Plugin<T> {
export default Table;
export { TableComponent };
export type { TableValue };
export type { TableValue, TableOptions };

View File

@ -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<Array<TableModelCol | TableModelEmptyCol>>;
};
export interface TableInterface extends CardInterface {
export interface TableInterface<V extends TableValue = TableValue>
extends CardInterface<V> {
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<string>;
overflow?: {
maxLeftWidth?: () => number;
maxRightWidth?: () => number;
};
colMinWidth?: number;
rowMinHeight?: number;
markdown?: boolean;
}
export type ControllOptions = {
col_min_width: number;
row_min_height: number;