Card component
Usually used for completely custom rendering content
Inherit the Card
abstract class
import {Card} from'@aomao/engine'export default class extends Card {...}
Rendering
Rendering a card needs to display the render
method, which is an abstract method and must be implemented
import { $, Card } from '@aomao/engine';export default class extends Card {static get cardName() {return 'CardName';}static get cardType() {return CardType.BLOCK;}render() {//Return the node, it will be automatically appended to the center position of the cardreturn $('<div>Card</div>');//Or take the initiative to appendthis.getCenter().append($('<div>Card</div>'));}}
React components
import React from 'react';export default () => <div>React Commponent</div>;
Card components
import ReactDOM from 'react-dom';import { $, Card, CardType } from '@aomao/engine';// import custom react componentsimport ReactCommponent from 'ReactCommponent';export default class extends Card {container?: NodeInterface;static get cardName() {return 'CardName';}static get cardType() {return CardType.BLOCK;}/*** After the card is rendered successfully, the empty div node has been loaded in the editor* */didRender() {super.didRender();if (!this.container) return;// Get a node of type HTMLElementconst element = this.container.get<HTMLElement>()!;//Use ReactDOM to render React components onto empty div nodes on the containerReactDOM.render(<ReactCommponent />, element);}/*** Render the card* */render() {// Render an empty div nodethis.container = $('<div></div>');return this.container;}/*** Uninstall components* */destroy() {super.destroy();const element = this.container.get<HTMLElement>();if (element) ReactDOM.unmountComponentAtNode(element);}}
Card plugin file, main function: insert card, convert/parse card
test/index.ts
import {$,Plugin,NodeInterface,CARD_KEY,isEngine,SchemaInterface,PluginOptions,decodeCardValue,encodeCardValue,} from '@aomao/engine';import TestComponent from './component';export interface Options extends PluginOptions {hotkey?: string | Array<string>;}export default class extends Plugin<Options> {static get pluginName() {return 'test';}// Plugin initializationinit() {// listen to events parsed into htmlthis.editor.on('parse:html', (node) => this.parseHtml(node));// Set the entrance of the schema rule when monitoring and pastingthis.editor.on('paste:schema', (schema) => this.pasteSchema(schema));// monitor the node loop when pastingthis.editor.on('paste:each', (child) => this.pasteHtml(child));}// execution methodexecute() {if (!isEngine(this.editor)) return;const { card } = this.editor;card.insert(TestComponent.cardName);}// hotkeyhotkey() {return this.options.hotkey || 'mod+shift+f';}// Add the required schema when pastingpasteSchema(schema: SchemaInterface) {schema.add({type: 'block',name: 'div',attributes: {'data-type': {required: true,value: TestComponent.cardName,},'data-value': '*',},});}// parse the pasted htmlpasteHtml(node: NodeInterface) {if (!isEngine(this.editor)) return;if (node.isElement()) {const type = node.attributes('data-type');if (type === TestComponent.cardName) {const value = node.attributes('data-value');const cardValue = decodeCardValue(value);this.editor.card.replaceNode(node,TestComponent.cardName,cardValue,);node.remove();return false;}}return true;}// parse into htmlparseHtml(root: NodeInterface) {root.find(`[${CARD_KEY}=${TestComponent.cardName}]`).each((cardNode) => {const node = $(cardNode);const card = this.editor.card.find(node) as TestComponent;const value = card?.getValue();if (value) {node.empty();const div = $(`<div data-type="${TestComponent.cardName}" data-value="${encodeCardValue(value,)}">Card to html</div>`,);node.replaceWith(div);} else node.remove();},);}}export { TestComponent };
react component, presents the view and interaction of the card
test/component/test.jsx
import { FC } from 'react';const TestComponent: FC = () => <div>This is Test Plugin</div>;export default TestComponent;
The card component, which mainly loads the react component into the editor
test/component/index.tsx
import {$,Card,CardToolbarItemOptions,CardType,isEngine,NodeInterface,ToolbarItemOptions,} from '@aomao/engine';import ReactDOM from 'react-dom';import TestComponent from './test';class Test extends Card {static get cardName() {return 'test';}static get cardType() {return CardType.BLOCK;}#container?: NodeInterface;toolbar(): Array<ToolbarItemOptions | CardToolbarItemOptions> {if (!isEngine(this.editor) || this.editor.readonly) return [];return [{type: 'dnd',},{type: 'copy',},{type: 'delete',},{type: 'node',node: $('<span>Test button</span>'),didMount: (node) => {node.on('click', () => {alert('test button');});},},];}render() {this.#container = $('<div>Loading</div>');return this.#container; // Or use this.getCenter().append(this.#container) to avoid returning this.#container}didRender() {super.didRender();ReactDOM.render(<TestComponent />, this.#container?.get<HTMLElement>());}destroy() {super.destroy();ReactDOM.unmountComponentAtNode(this.#container?.get<HTMLElement>()!);}}export default Test;export type { TestValue };
Test Plugin file, main functions: unpacking, converting/parsing package
test/index.ts
import {$,Plugin,NodeInterface,CARD_KEY,isEngine,SchemaInterface,PluginOptions,decodeCardValue,encodeCardValue,} from '@aomao/engine';import TestComponent from './component';import type { TestValue } from './component';export interface Options extends PluginOptions {hotkey?: string | Array<string>;}export default class extends Plugin<Options> {static get pluginName() {return 'test';}// initializationinit() {// Listen for events parsed into htmlthis.editor.on('parse:html', this.parseHtml);// Set the entrance of the schema rule when monitoring and pastingthis.editor.on('paste:schema', this.pasteSchema);// monitor the node loop when pastingthis.editor.on('paste:each', this.pasteHtml);}// execution methodexecute() {if (!isEngine(this.editor)) return;const { card } = this.editor;card.insert<TestValue>(TestComponent.cardName, {text: 'This is card value',});}// hotkeyhotkey() {return this.options.hotkey || 'mod+shift+f';}// Add the required schema when pastingpasteSchema = (schema: SchemaInterface) => {schema.add({type: 'block',name: 'div',attributes: {'data-type': {required: true,value: TestComponent.cardName,},'data-value': '*',},});};// parse the pasted htmlpasteHtml = (node: NodeInterface) => {if (!isEngine(this.editor)) return;if (node.isElement()) {const type = node.attributes('data-type');if (type === TestComponent.cardName) {const value = node.attributes('data-value');const cardValue = decodeCardValue(value);this.editor.card.replaceNode(node,TestComponent.cardName,cardValue,);node.remove();return false;}}return true;};// parse into htmlparseHtml = (root: NodeInterface) => {root.find(`[${CARD_KEY}="${TestComponent.cardName}"],[${READY_CARD_KEY}="${TestComponent.cardName}"]`,).each((cardNode) => {const node = $(cardNode);const card = this.editor.card.find<TestValue, TestComponent>(node);const value = card?.getValue();if (value) {node.empty();const div = $(`<div data-type="${TestComponent.cardName}" data-value="${encodeCardValue(value)}">${value.text}</div>`,);node.replaceWith(div);} else node.remove();});};// destroy event bindingdestroy() {this.editor.off('parse:html', this.parseHtml);this.editor.off('paste:schema', this.pasteSchema);this.editor.off('paste:each', this.pasteHtml);}}export { TestComponent };export type { TestValue };
Use card plugins
import React, { useEffect, useRef, useState } from 'react';import Engine, { EngineInterface } from '@aomao/engine';// Import custom card plugins and card components test/index.tsimport Test, { TestComponent } from './test';const EngineDemo = () => {//Editor containerconst ref = useRef<HTMLDivElement | null>(null);//Engine instanceconst [engine, setEngine] = useState<EngineInterface>();//Editor contentconst [content, setContent] = useState<string>('Hello card!');useEffect(() => {if (!ref.current) return;//Instantiate the engineconst engine = new Engine(ref.current, {plugins: [Test],cards: [TestComponent],});//Set the editor valueengine.setValue(content);//Listen to the editor value change eventengine.on('change', () => {const value = engine.getValue();setContent(value);console.log(`value:${value}`);});//Set the engine instancesetEngine(engine);}, []);return <div ref={ref} />;};export default EngineDemo;
Use the shortcut key mod+shift+f
defined in test/index.ts
to insert the card component just defined in the editor
Vue components
<template><div>Vue Component</div></template><script lang="ts">import { Component, Prop, Vue } from "vue-property-decorator";@Component({})export default class VueComponent extends Vue {}</script>
Card components
import Vue from 'vue';import { $, Card, CardType } from '@aomao/engine';// import custom vue componentsimport VueCommponent from 'VueCommponent';export default class extends Card {container?: NodeInterface;private vm?: Vue;static get cardName() {return 'CardName';}static get cardType() {return CardType.BLOCK;}/*** After the card is rendered successfully, the empty div node has been loaded in the editor* */didRender() {super.didRender();if (!this.container) return;// Get a node of type HTMLElementconst element = this.container.get<HTMLElement>()!;//Use createApp to render the Vue component to the empty div node on the container//Add a delay, otherwise it may not be rendered successfullysetTimeout(() => {this.vm = new Vue({render: (h) => {return h(VueComponent, {props: {},});},});element.append(vm.$mount().$el);}, 20);}/*** Render the card* */render() {// Render an empty div nodethis.container = $('<div></div>');return this.container;}/*** Uninstall components* */destroy() {super.destroy();this.vm?.$destroy();this.vm = undefined;}}
Vue components
<template><div>Vue Component</div></template><script lang="ts">import {defineComponent} from'vue'export default defineComponent({name:"am-vue-component",})</script>
Card components
import { createApp, App } from 'vue';import { $, Card, CardType } from '@aomao/engine';// import custom vue componentsimport VueCommponent from 'VueCommponent';export default class extends Card {container?: NodeInterface;private vm?: App;static get cardName() {return 'CardName';}static get cardType() {return CardType.BLOCK;}/*** After the card is rendered successfully, the empty div node has been loaded in the editor* */didRender() {super.didRender();if (!this.container) return;// Get a node of type HTMLElementconst element = this.container.get<HTMLElement>()!;//Use createApp to render the Vue component to the empty div node on the container//Add a delay, otherwise it may not be rendered successfullysetTimeout(() => {this.vm = createApp(VueComponent);this.vm.mount(element);}, 20);}/*** Render the card* */render() {// Render an empty div nodethis.container = $('<div></div>');return this.container;}/*** Uninstall components* */destroy() {super.destroy();this.vm?.unmount();this.vm = undefined;}}
Card plugin file, main function: insert card, convert/parse card
test/index.ts
import {$,Plugin,NodeInterface,CARD_KEY,isEngine,SchemaInterface,PluginOptions,decodeCardValue,encodeCardValue,} from '@aomao/engine';import TestComponent from './component';export interface Options extends PluginOptions {hotkey?: string | Array<string>;}export default class extends Plugin<Options> {static get pluginName() {return 'test';}// Plugin initializationinit() {// listen to events parsed into htmlthis.editor.on('parse:html', (node) => this.parseHtml(node));// Set the entrance of the schema rule when monitoring and pastingthis.editor.on('paste:schema', (schema) => this.pasteSchema(schema));// monitor the node loop when pastingthis.editor.on('paste:each', (child) => this.pasteHtml(child));}// execution methodexecute() {if (!isEngine(this.editor)) return;const { card } = this.editor;card.insert(TestComponent.cardName);}// hotkeyhotkey() {return this.options.hotkey || 'mod+shift+0';}// Add the required schema when pastingpasteSchema(schema: SchemaInterface) {schema.add({type: 'block',name: 'div',attributes: {'data-type': {required: true,value: TestComponent.cardName,},'data-value': '*',},});}// parse the pasted htmlpasteHtml(node: NodeInterface) {if (!isEngine(this.editor)) return;if (node.isElement()) {const type = node.attributes('data-type');if (type === TestComponent.cardName) {const value = node.attributes('data-value');const cardValue = decodeCardValue(value);this.editor.card.replaceNode(node,TestComponent.cardName,cardValue,);node.remove();return false;}}return true;}// parse into htmlparseHtml(root: NodeInterface) {root.find(`[${CARD_KEY}=${TestComponent.cardName}]`).each((cardNode) => {const node = $(cardNode);const card = this.editor.card.find(node) as TestComponent;const value = card?.getValue();if (value) {node.empty();const div = $(`<div data-type="${TestComponent.cardName}" data-value="${encodeCardValue(value)}"></div>`,);node.replaceWith(div);} else node.remove();},);}}export { TestComponent };
vue component, presents the view and interaction of the card
test/component/test.vue
<template><div><div>This is test plugin</div></div></template><style lang="less"></style>
The card component, which mainly loads the vue component into the editor
test/component/index.ts
import {$,Card,CardToolbarItemOptions,CardType,isEngine,NodeInterface,ToolbarItemOptions,} from '@aomao/engine';import { App, createApp } from 'vue';import TestVue from './test.vue';class Test extends Card {static get cardName() {return 'test';}static get cardType() {return CardType.BLOCK;}#container?: NodeInterface;#vm?: App;toolbar(): Array<ToolbarItemOptions | CardToolbarItemOptions> {if (!isEngine(this.editor) || this.editor.readonly) return [];return [{type: 'dnd',},{type: 'copy',},{type: 'delete',},{type: 'node',node: $('<span>Test button</span>'),didMount: (node) => {node.on('click', () => {alert('test button');});},},];}render() {this.#container = $('<div>Loading</div>');return this.#container; // Or use this.getCenter().append(this.#container) to avoid returning this.#container}didRender() {super.didRender();this.#vm = createApp(TestVue, {});this.#vm.mount(this.#container?.get<HTMLElement>());}destroy() {super.destroy();this.#vm?.unmount();}}export default Test;
Use card plugins
<template><div ref="container"></div></template><script lang="ts">import {defineComponent, onMounted, onUnmounted, ref} from "vue";import Engine, {$,EngineInterface,isMobile,NodeInterface,removeUnit,} from "@aomao/engine";import Test, {TestComponent} from "./test";export default defineComponent({name: "engine-demo",setup() {// editor containerconst container = ref<HTMLElement | null>(null);// Editor engineconst engine = ref<EngineInterface | null>(null);onMounted(() => {// Instantiate the editor engine after the container is loadedif (container.value) {//Instantiate the engineconst engineInstance = new Engine(container.value, {// enabled pluginsplugins:[Test],// enabled cardcards:[TestComponent],});engineInstance.setValue("<strong>Hello</strong>,This is demo");// listen to the editor value change eventengineInstance.on("change", (editorValue) => {console.log("value", editorValue);});engine.value = engineInstance;}});onUnmounted(() => {if (engine.value) engine.value.destroy();});return {container,engine,};},});</script>
Use the shortcut key mod+shift+0
defined in test/index.ts
to insert the card component just defined in the editor
Toolbar
To implement the card toolbar, you need to rewrite the toolbar
method
The toolbar has implemented some default buttons and events, just pass in the name to use
separator
dividing linecopy
copy, you can copy the content of the card containing the root node to the clipboarddelete
delete cardmaximize
to maximize the cardmore
more button, need additional configuration items
propertydnd
is the draggable icon button on the left side of the cardIn addition, you can customize button properties or render React
and Vue
front-end framework components
Customizable toolbar UI types are:
button
buttondropdown
drop-down boxswitch
radio buttoninput
input boxnode
a node of type NodeInterface
For the configuration of each type, please see its Type Definition
import {$,Card,CardToolbarItemOptions,ToolbarItemOptions,} from '@aomao/engine';export default class extends Card {static get cardName() {return 'CardName';}static get cardType() {return CardType.BLOCK;}// Card Toolbartoolbar(): Array<CardToolbarItemOptions | ToolbarItemOptions> {return [// Drag the button on the left{type: 'dnd',},// copy{type: 'copy',},// delete{type: 'delete',},// split line{type: 'separator',},// Custom node{type: 'node',node: $('<div />'),didMount: (node) => {//After loading, you can use the front-end framework to render components to the node node. Vue needs to add delay to use createAppconsole.log(`The button is loaded, ${node}`);},},];}// render divrender() {return $('<div>Card</div>');}}
The default type of card value CardValue
Two values of id
and type
are provided by default, and the custom value cannot be the same as the default value
id
unique card numbertype
card typeimport {$, Card, CardType} from'@aomao/engine'export default class extends Card<{ count: number }> {container?: NodeInterfacestatic get cardName() {return'CardName';}static get cardType() {return CardType.BLOCK;}// click on the divonClick = () => {// Get card valueconst value = this.getValue() || {count: 0}// give count + 1const count = value.count + 1// Reset the card value, it will be saved to the data-card-value attribute on the root node of the cardthis.setValue({count,});// Set the content of the divthis.container?.html(count)};// Render the div noderender() {// Get the value of the cardconst value = this.getValue() || {count: 0}// Create a div nodethis.container = $(`<div>${value.count}</div>`)// bind the click eventthis.container.on("click" => () => this.onClick())// Return the node to load the containerreturn this.container}}
import { Plugin, isEngine } from '@aomao/engine';// import cardsimport CardComponent from './component';type Options = {defaultValue?: number;};export default class extends Plugin<Options> {static get pluginName() {return 'card-plugin';}// The plugin executes the command, call engine.command.excute("card-plugin") to execute the current commandexecute() {// Reader does not executeif (!isEngine(this.editor)) return;const { card } = this.editor;//Insert the card and pass in the count initialization parametercard.insert(CardComponent.cardName, {count: this.otpions.defaultValue || 0,});}}export { CardComponent };
cardName
CardName, read-only static attribute, required
Type: string
The CardName is unique and cannot be repeated with all the CardNames passed into the engine
export default class extends Plugin {//Define the CardName, it is requiredstatic get cardName() {return 'CardName';}}
cardType
Card type, read-only static property, required
Type: CardType
There are two types of CardType
, inline
and block
export default class extends Plugin {//Define the card type, it is requiredstatic get cardType() {return CardType.BLOCK;}}
autoActivate
Whether it can be activated automatically, the default is false
autoSelected
Whether it can be selected automatically, the default is true
singleSelectable
Whether it can be selected individually, the default is true
collab
Whether you can participate in collaboration, when other authors edit the card, it will cover a layer of shadow
focus
Can focus
selectStyleType
The style of the selected yes, the default is the border change, optional values:
border
border changesbackground
background color changelazyRender
Whether to enable lazy loading, the rendering is triggered when the card node is visible in the view
editor
EditEditor example
Type: EditorInterface
When the plugin is instantiated, the editor instance will be passed in. We can access it through this
import {Card, isEngine} from'@aomao/engine'export default class extends Card<Options> {...init() {console.log(isEngine(this.editor)? "Engine": "Reader")}}
id
Read only
Type: string
Card id, each card has a unique ID, we can use this ID to find instances of card components
type
The card type, the static property cardType
of the card class is obtained by default. If there is a type
value in getValue()
, this value will be used as the type
When setting a new type
value to the card, the current card will be removed and the new type
will be used to re-render the card at the current card position
Type: CardType
isEditable
Read only
Type: boolean
Whether the card is editable
contenteditable
Editable node, optional
One or more CSS selectors can be set, and these nodes will become editable
The value of the editable area needs to be customized and saved. It is recommended to save it in the value
of the card
import {Card, isEngine} from'@aomao/engine'export default class extends Card<Options> {...contenteditable = ["div.card-editor-container"]render(){return "<div><div>Thi is Card</div><div class=\"card-editor-container\">Editable here</div></div>"}}
readonly
Is it read-only
Type: boolean
root
Card root node
Type: NodeInterface
activated
Activate now
Type: boolean
selected
Whether selected
Type: boolean
isMaximize
Whether to maximize
Type: boolean
activatedByOther
Activator, effective in cooperative state
Type: string | false
selectedByOther
Selected person, valid in collaboration state
Type: string | false
toolbarModel
Toolbar operation class
Type: CardToolbarInterface
resizeModel
Size adjustment operation class
Type: ResizeInterface
resize
Whether the card size can be changed or passed into the rendering node
Type: boolean | (() => NodeInterface);
If specified, the resizeModel
attribute will be instantiated
init
Initialization, optional
init?(): void;
find
Find the DOM node in the Card
/*** Find the DOM node in the Card* @param selector*/find(selector: string): NodeInterface;
findByKey
Get the DOM node in the current Card through the value of data-card-element
/*** Get the DOM node in the current Card through the value of data-card-element* @param key key*/findByKey(key: string): NodeInterface;
getCenter
Get the central node of the card, which is the outermost node of the custom content area of the card
/*** Get the central node of the card*/getCenter(): NodeInterface;
isCenter
Determine whether the node belongs to the central node of the card
/*** Determine whether the node belongs to the central node of the card* @param node node*/isCenter(node: NodeInterface): boolean;
isCursor
Determine whether the node is at the left and right cursors of the card
/*** Determine whether the node is at the left and right cursors of the card* @param node node*/isCursor(node: NodeInterface): boolean;
isLeftCursor
Determine whether the node is at the left cursor of the card
/*** Determine whether the node is at the left cursor of the card* @param node node*/isLeftCursor(node: NodeInterface): boolean;
isRightCursor
Determine whether the node is at the right cursor of the card
/*** Determine whether the node is at the right cursor of the card* @param node node*/isRightCursor(node: NodeInterface): boolean;
focus
Focus card
/*** Focus card* @param range cursor* @param toStart is the starting position*/focus(range: RangeInterface, toStart?: boolean): void;
onFocus
Triggered when the card is focused
/*** Triggered when the card is focused*/onFocus?(): void;
activate
Activate Card
/*** Activate Card* @param activated Whether to activate*/activate(activated: boolean): void;
select
Choose Card
/*** Choose Card* @param selected is it selected*/select(selected: boolean): void;
onSelect
Triggered when the selected state changes
/*** Trigger when the selected state changes* @param selected is it selected*/onSelect(selected: boolean): void;
onSelectByOther
In the cooperative state, trigger when the selected state changes
/*** In the cooperative state, trigger when the selected state changes* @param selected is it selected* @param value {color: collaborator color, rgb: color rgb format}*/onSelectByOther(selected: boolean,value?: {color: string;rgb: string;},): NodeInterface | void;
onActivate
Triggered when the activation state changes
/*** Triggered when the activation status changes* @param activated Whether to activate*/onActivate(activated: boolean): void;
onActivateByOther
In the cooperative state, trigger when the activation state changes
/*** In the cooperative state, trigger when the activation state changes* @param activated Whether to activate* @param value {color: collaborator color, rgb: color rgb format}*/onActivateByOther(activated: boolean,value?: {color: string;rgb: string;},): NodeInterface | void;
onChange
Trigger when the editable area value changes
/*** Trigger when the editor area value changes* @param node editable area node*/onChange?(node: NodeInterface): void;
setValue
Set card value
/*** Set card value* @param value*/setValue(value: CardValue): void;
getValue
Get card value
/*** Get card value*/getValue(): (CardValue & {id: string }) | undefined;
toolbar
Toolbar configuration items
/*** Toolbar configuration items*/toolbar?(): Array<CardToolbarItemOptions | ToolbarItemOptions>;
maximize
Maximize card
/*** Maximize*/maximize(): void;
minimize
Minimize the card
/*** minimize*/minimize(): void;
render
Render the card
/*** Render the card*/render(): NodeInterface | string | void;
destroy
destroy
/*** Destroy*/destroy?(): void;
didInsert
Triggered after inserting a card into the editor
/*** Trigger after insertion*/didInsert?(): void;
didUpdate
Triggered after updating the card
/*** Triggered after update*/didUpdate?(): void;
beforeRender
Triggered before the card is rendered after the lazy rendering is turned on
beforeRender(): void
didRender
Triggered after the card is successfully rendered
/*** Triggered after rendering*/didRender(): void;
drawBackground
Render the editable card collaborative selection area
/*** Rendering the collaborative selection area of the editor card* @param node background canvas* @param range render cursor*/drawBackground?(node: NodeInterface,range: RangeInterface,targetCanvas: TinyCanvasInterface,): DOMRect | RangeInterface[] | void | false;
getSelectionNodes
/*** Get all nodes selected in the editable area*/getSelectionNodes?(): Array<NodeInterface>