Odoo18-Base/addons/html_editor/static/src/editor.js
2025-01-06 10:57:38 +07:00

231 lines
7.7 KiB
JavaScript

import { MAIN_PLUGINS } from "./plugin_sets";
import { removeClass } from "./utils/dom";
import { isEmpty } from "./utils/dom_info";
import { resourceSequenceSymbol, withSequence } from "./utils/resource";
import { initElementForEdition } from "./utils/sanitize";
/**
* @typedef { import("./plugin_sets").SharedMethods } SharedMethods
* @typedef {typeof import("./plugin").Plugin} PluginConstructor
**/
/**
* @typedef { Object } CollaborationConfig
* @property { string } collaboration.peerId
* @property { Object } collaboration.busService
* @property { Object } collaboration.collaborationChannel
* @property { String } collaboration.collaborationChannel.collaborationModelName
* @property { String } collaboration.collaborationChannel.collaborationFieldName
* @property { Number } collaboration.collaborationChannel.collaborationResId
* @property { 'start' | 'focus' } [collaboration.collaborativeTrigger]
* @typedef { Object } EditorConfig
* @property { string } [content]
* @property { boolean } [allowInlineAtRoot]
* @property { PluginConstructor[] } [Plugins]
* @property { boolean } [disableFloatingToolbar]
* @property { string[] } [classList]
* @property { Object } [localOverlayContainers]
* @property { Object } [embeddedComponentInfo]
* @property { Object } [resources]
* @property { string } [direction="ltr"]
* @property { Function } [onChange]
* @property { Function } [onEditorReady]
* @property { boolean } [dropImageAsAttachment]
* @property { CollaborationConfig } [collaboration]
* @property { Function } getRecordInfo
*/
function sortPlugins(plugins) {
const initialPlugins = new Set(plugins);
const inResult = new Set();
// need to sort them
const result = [];
let P;
function findPlugin() {
for (const P of initialPlugins) {
if (P.dependencies.every((dep) => inResult.has(dep))) {
initialPlugins.delete(P);
return P;
}
}
}
while ((P = findPlugin())) {
inResult.add(P.id);
result.push(P);
}
if (initialPlugins.size) {
const messages = [];
for (const P of initialPlugins) {
messages.push(
`"${P.id}" is missing (${P.dependencies
.filter((d) => !inResult.has(d))
.join(", ")})`
);
}
throw new Error(`Missing dependencies: ${messages.join(", ")}`);
}
return result;
}
export class Editor {
/**
* @param { EditorConfig } config
*/
constructor(config, services) {
this.isDestroyed = false;
this.config = config;
this.services = services;
this.resources = null;
this.plugins = [];
/** @type { HTMLElement } **/
this.editable = null;
/** @type { Document } **/
this.document = null;
/** @ts-ignore @type { SharedMethods } **/
this.shared = {};
}
attachTo(editable) {
if (this.isDestroyed || this.editable) {
throw new Error("Cannot re-attach an editor");
}
this.editable = editable;
this.document = editable.ownerDocument;
if (this.config.content) {
editable.innerHTML = this.config.content;
if (isEmpty(editable)) {
editable.innerHTML = "<p><br></p>";
}
}
this.preparePlugins();
editable.setAttribute("contenteditable", true);
initElementForEdition(editable, { allowInlineAtRoot: !!this.config.allowInlineAtRoot });
editable.classList.add("odoo-editor-editable");
if (this.config.classList) {
editable.classList.add(...this.config.classList);
}
if (this.config.height) {
editable.style.height = this.config.height;
}
this.startPlugins();
this.config.onEditorReady?.();
}
preparePlugins() {
const Plugins = sortPlugins(this.config.Plugins || MAIN_PLUGINS);
const plugins = new Map();
for (const P of Plugins) {
if (P.id === "") {
throw new Error(`Missing plugin id (class ${P.name})`);
}
if (plugins.has(P.id)) {
throw new Error(`Duplicate plugin id: ${P.id}`);
}
const imports = {};
for (const dep of P.dependencies) {
if (plugins.has(dep)) {
imports[dep] = {};
for (const h of plugins.get(dep).shared) {
imports[dep][h] = this.shared[dep][h];
}
} else {
throw new Error(`Missing dependency for plugin ${P.id}: ${dep}`);
}
}
plugins.set(P.id, P);
const plugin = new P(this.document, this.editable, imports, this.config, this.services);
this.plugins.push(plugin);
const exports = {};
for (const h of P.shared) {
if (!(h in plugin)) {
throw new Error(`Missing helper implementation: ${h} in plugin ${P.id}`);
}
exports[h] = plugin[h].bind(plugin);
}
this.shared[P.id] = exports;
}
const resources = this.createResources();
for (const plugin of this.plugins) {
plugin._resources = resources;
}
this.resources = resources;
}
startPlugins() {
for (const plugin of this.plugins) {
plugin.setup();
}
this.resources["normalize_handlers"].forEach((cb) => cb(this.editable));
this.resources["start_edition_handlers"].forEach((cb) => cb());
}
createResources() {
const resources = {};
function registerResources(obj) {
for (const key in obj) {
if (!(key in resources)) {
resources[key] = [];
}
resources[key].push(obj[key]);
}
}
if (this.config.resources) {
registerResources(this.config.resources);
}
for (const plugin of this.plugins) {
if (plugin.resources) {
registerResources(plugin.resources);
}
}
for (const key in resources) {
const resource = resources[key]
.flat()
.map((r) => {
const isObjectWithSequence =
typeof r === "object" && r !== null && resourceSequenceSymbol in r;
return isObjectWithSequence ? r : withSequence(10, r);
})
.sort((a, b) => a[resourceSequenceSymbol] - b[resourceSequenceSymbol])
.map((r) => r.object);
resources[key] = resource;
Object.freeze(resources[key]);
}
return Object.freeze(resources);
}
getContent() {
return this.getElContent().innerHTML;
}
getElContent() {
const el = this.editable.cloneNode(true);
this.resources["clean_for_save_handlers"].forEach((cb) => cb({ root: el }));
return el;
}
destroy(willBeRemoved) {
if (this.editable) {
let plugin;
while ((plugin = this.plugins.pop())) {
plugin.destroy();
}
this.shared = {};
if (!willBeRemoved) {
// we only remove class/attributes when necessary. If we know that the editable
// element will be removed, no need to make changes that may require the browser
// to recompute the layout
this.editable.removeAttribute("contenteditable");
removeClass(this.editable, "odoo-editor-editable");
}
this.editable = null;
}
this.isDestroyed = true;
}
}