am-editor-002/docs/plugin/tutorials-card.md

28 KiB
Raw Blame History

Card component

Card component

Usually used for completely custom rendering content

Inheritance

Inherit the Card abstract class

import {Card} from'@aomao/engine'

export default class extends Card {
...
}

Example

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 card
		return $('<div>Card</div>');
		//Or take the initiative to append
		this.getCenter().append($('<div>Card</div>'));
	}
}

React rendering

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 components
import 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() {
		if (!this.container) return;
		// Get a node of type HTMLElement
		const element = this.container.get<HTMLElement>()!;
		//Use ReactDOM to render React components onto empty div nodes on the container
		ReactDOM.render(<ReactCommponent />, element);
	}

	/**
	 * Render the card
	 * */
	render() {
		// Render an empty div node
		this.container = $('<div></div>');
		return this.container;
	}

	/**
	 * Uninstall components
	 * */
	destroy() {
		super.destroy();
		const element = this.container.get<HTMLElement>();
		if (element) ReactDOM.unmountComponentAtNode(element);
	}
}

React card plugin example

Card plug-in 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';
	}
	// Plug-in initialization
	init() {
		// listen to events parsed into html
		this.editor.on('parse:html', (node) => this.parseHtml(node));
		// Set the entrance of the schema rule when monitoring and pasting
		this.editor.on('paste:schema', (schema) => this.pasteSchema(schema));
		// monitor the node loop when pasting
		this.editor.on('paste:each', (child) => this.pasteHtml(child));
	}
	// execution method
	execute() {
		if (!isEngine(this.editor)) return;
		const { card } = this.editor;
		card.insert(TestComponent.cardName);
	}
	// hot key
	hotkey() {
		return this.options.hotkey || 'mod+shift+0';
	}
	// Add the required schema when pasting
	pasteSchema(schema: SchemaInterface) {
		schema.add({
			type: 'block',
			name: 'div',
			attributes: {
				'data-type': {
					required: true,
					value: TestComponent.cardName,
				},
				'data-value': '*',
			},
		});
	}
	// parse the pasted html
	pasteHtml(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 html
	parseHtml(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 };

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() {
		ReactDOM.render(<TestComponent />, this.#container?.get<HTMLElement>());
	}

	destroy() {
		super.destroy();
		ReactDOM.unmountComponentAtNode(this.#container?.get<HTMLElement>()!);
	}
}
export default Test;

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.ts
import Test, { TestComponent } from './test';

const EngineDemo = () => {
	//Editor container
	const ref = useRef<HTMLDivElement | null>(null);
	//Engine instance
	const [engine, setEngine] = useState<EngineInterface>();
	//Editor content
	const [content, setContent] = useState<string>('Hello card!');

	useEffect(() => {
		if (!ref.current) return;
		//Instantiate the engine
		const engine = new Engine(ref.current, {
			plugins: [Test],
			cards: [TestComponent],
		});
		//Set the editor value
		engine.setValue(content);
		//Listen to the editor value change event
		engine.on('change', (value) => {
			setContent(value);
			console.log(`value:${value}`);
		});
		//Set the engine instance
		setEngine(engine);
	}, []);

	return <div ref={ref} />;
};
export default EngineDemo;

Use the shortcut key mod+shift+0 defined in test/index.ts to insert the card component just defined in the editor

Vue2 rendering

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 components
import 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() {
		if (!this.container) return;
		// Get a node of type HTMLElement
		const 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 successfully
		setTimeout(() => {
			this.vm = new Vue({
				render: (h) => {
					return h(VueComponent, {
						props: {},
					});
				},
			});
			element.append(vm.$mount().$el);
		}, 20);
	}

	/**
	 * Render the card
	 * */
	render() {
		// Render an empty div node
		this.container = $('<div></div>');
		return this.container;
	}

	/**
	 * Uninstall components
	 * */
	destroy() {
		super.destroy();
		this.vm?.$destroy();
		this.vm = undefined;
	}
}

Vue3 rendering

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 components
import 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() {
		if (!this.container) return;
		// Get a node of type HTMLElement
		const 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 successfully
		setTimeout(() => {
			this.vm = createApp(VueComponent);
			this.vm.mount(element);
		}, 20);
	}

	/**
	 * Render the card
	 * */
	render() {
		// Render an empty div node
		this.container = $('<div></div>');
		return this.container;
	}

	/**
	 * Uninstall components
	 * */
	destroy() {
		super.destroy();
		this.vm?.unmount();
		this.vm = undefined;
	}
}

Vue card plugin example

Card plug-in 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';
	}
	// Plug-in initialization
	init() {
		// listen to events parsed into html
		this.editor.on('parse:html', (node) => this.parseHtml(node));
		// Set the entrance of the schema rule when monitoring and pasting
		this.editor.on('paste:schema', (schema) => this.pasteSchema(schema));
		// monitor the node loop when pasting
		this.editor.on('paste:each', (child) => this.pasteHtml(child));
	}
	// execution method
	execute() {
		if (!isEngine(this.editor)) return;
		const { card } = this.editor;
		card.insert(TestComponent.cardName);
	}
	// hot key
	hotkey() {
		return this.options.hotkey || 'mod+shift+0';
	}
	// Add the required schema when pasting
	pasteSchema(schema: SchemaInterface) {
		schema.add({
			type: 'block',
			name: 'div',
			attributes: {
				'data-type': {
					required: true,
					value: TestComponent.cardName,
				},
				'data-value': '*',
			},
		});
	}
	// parse the pasted html
	pasteHtml(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 html
	parseHtml(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() {
		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 container
    const container = ref<HTMLElement | null>(null);
    // Editor engine
    const engine = ref<EngineInterface | null>(null);
    onMounted(() => {
      // Instantiate the editor engine after the container is loaded
      if (container.value) {
        //Instantiate the engine
        const engineInstance = new Engine(container.value, {
          // enabled plugins
          plugins:[Test],
          // enabled card
          cards:[TestComponent],
        });

        engineInstance.setValue("<strong>Hello</strong>,This is demo");

        // listen to the editor value change event
        engineInstance.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 line
  • copy copy, you can copy the content of the card containing the root node to the clipboard
  • delete delete card
  • maximize to maximize the card
  • more more button, need additional configuration items property
  • dnd is the draggable icon button on the left side of the card

In addition, you can customize button properties or render React and Vue front-end framework components

Customizable toolbar UI types are:

  • button button
  • dropdown drop-down box
  • switch radio button
  • input input box
  • node 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 Toolbar
	toolbar(): 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 createApp
					console.log(`The button is loaded, ${node}`);
				},
			},
		];
	}

	// render div
	render() {
		return $('<div>Card</div>');
	}
}

Set card value

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 number
  • type card type
import {$, Card, CardType} from'@aomao/engine'

export default class extends Card<{ count: number }> {

  container?: NodeInterface

  static get cardName() {
    return'CardName';
  }

  static get cardType() {
    return CardType.BLOCK;
  }

  // click on the div
  onClick = () => {
    // Get card value
    const value = this.getValue() || {count: 0}
    // give count + 1
    const count = value.count + 1
    // Reset the card value, it will be saved to the data-card-value attribute on the root node of the card
    this.setValue({
      count,
    });
    // Set the content of the div
    this.container?.html(count)
  };

  // Render the div node
  render() {
    // Get the value of the card
    const value = this.getValue() || {count: 0}
    // Create a div node
    this.container = $(`<div>${value.count}</div>`)
    // bind the click event
    this.container.on("click" => () => this.onClick())
    // Return the node to load the container
    return this.container
  }
}

Combine with plugins

import { Plugin, isEngine } from '@aomao/engine';
// import cards
import 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 command
	execute() {
		// Reader does not execute
		if (!isEngine(this.editor)) return;
		const { card } = this.editor;
		//Insert the card and pass in the count initialization parameter
		card.insert(CardComponent.cardName, {
			count: this.otpions.defaultValue || 0,
		});
	}
}
export { CardComponent };

Static properties

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 required
	static 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 required
	static 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 changes
  • background background color change

toolbarFollowMouse

Whether the card toolbar follows the mouse position, the default flase

lazyRender

Whether to enable lazy loading, the rendering is triggered when the card node is visible in the view

Attributes

editor

EditEditor example

Type: EditorInterface

When the plug-in 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

Method

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;

updateBackgroundSelection

Update the editable card collaborative selection area

/**
 * Update the editable card collaborative selection area
 * @param range cursor
 */
updateBackgroundSelection?(range: RangeInterface): 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>