diff --git a/packages/core/src/dom_components/model/Components.ts b/packages/core/src/dom_components/model/Components.ts index 26b609155..8613ddf26 100644 --- a/packages/core/src/dom_components/model/Components.ts +++ b/packages/core/src/dom_components/model/Components.ts @@ -267,7 +267,7 @@ Component> { if (isWrapper && parsed.doctype) { const root = parent as ComponentWrapper; - const { components: bodyCmps, ...restBody } = (parsed.html as ComponentDefinitionDefined) || {}; + const { components: bodyCmps = [], ...restBody } = (parsed.html as ComponentDefinitionDefined) || {}; const { components: headCmps, ...restHead } = parsed.head || {}; components = bodyCmps!; root.set(restBody as any, opt); diff --git a/packages/core/src/editor/index.ts b/packages/core/src/editor/index.ts index 2ea4967b3..942254c38 100644 --- a/packages/core/src/editor/index.ts +++ b/packages/core/src/editor/index.ts @@ -8,19 +8,7 @@ * }); * ``` * - * ## Available Events - * - * You can make use of available events in this way - * ```js - * editor.on('EVENT-NAME', (some, argument) => { - * // do something - * }) - * ``` - * - * * `update` - The structure of the template is updated (its HTML/CSS) - * * `undo` - Undo executed - * * `redo` - Redo executed - * * `load` - Editor is loaded + * {REPLACE_EVENTS} * * ### Components * Check the [Components](/api/components.html) module. @@ -91,6 +79,7 @@ import UtilsModule from '../utils'; import html from '../utils/html'; import defConfig, { EditorConfig, EditorConfigKeys } from './config/config'; import EditorModel, { EditorLoadOptions } from './model/Editor'; +import { EditorEvents } from './types'; import EditorView from './view/EditorView'; export type ParsedRule = { @@ -130,6 +119,7 @@ export default class Editor implements IBaseModule { $: any; em: EditorModel; config: EditorConfigType; + events = EditorEvents; constructor(config: EditorConfig = {}, opts: any = {}) { const defaults = defConfig(); diff --git a/packages/core/src/editor/model/Editor.ts b/packages/core/src/editor/model/Editor.ts index fe2d85e13..88bd27369 100644 --- a/packages/core/src/editor/model/Editor.ts +++ b/packages/core/src/editor/model/Editor.ts @@ -3,7 +3,7 @@ import Backbone from 'backbone'; import $ from '../../utils/cash-dom'; import Extender from '../../utils/extender'; import { hasWin, isEmptyObj, wait } from '../../utils/mixins'; -import { AddOptions, Model, Collection, ObjectAny } from '../../common'; +import { Model, Collection, ObjectAny } from '../../common'; import Selected from './Selected'; import FrameView from '../../canvas/view/FrameView'; import Editor from '..'; @@ -45,6 +45,7 @@ import { CanvasSpotBuiltInTypes } from '../../canvas/model/CanvasSpot'; import DataSourceManager from '../../data_sources'; import { ComponentsEvents } from '../../dom_components/types'; import { InitEditorConfig } from '../..'; +import { EditorEvents } from '../types'; Backbone.$ = $; @@ -89,6 +90,7 @@ const logs = { export interface EditorLoadOptions { /** Clear the editor state (eg. dirty counter, undo manager, etc.). */ clear?: boolean; + initial?: boolean; } export default class EditorModel extends Model { @@ -333,6 +335,7 @@ export default class EditorModel extends Model { */ loadOnStart() { const { projectData, headless } = this.config; + const loadOpts: EditorLoadOptions = { initial: true }; const sm = this.Storage; // In `onLoad`, the module will try to load the data from its configurations. @@ -345,16 +348,16 @@ export default class EditorModel extends Model { }; if (headless) { - projectData && this.loadData(projectData); + projectData && this.loadData(projectData, loadOpts); postLoad(); } else { // Defer for storage load events. this._storageTimeout = setTimeout(async () => { if (projectData) { - this.loadData(projectData); + this.loadData(projectData, loadOpts); } else if (sm?.canAutoload()) { try { - await this.load(); + await this.load({}, loadOpts); } catch (error) { this.logError(error as string); } @@ -388,7 +391,7 @@ export default class EditorModel extends Model { if (!opts.isClear) { this.updateItr && clearTimeout(this.updateItr); - this.updateItr = setTimeout(() => this.trigger('update')); + this.updateItr = setTimeout(() => this.trigger(EditorEvents.update)); } if (this.config.noticeOnUnload) { @@ -851,7 +854,7 @@ export default class EditorModel extends Model { */ async load(options?: T, loadOptions: EditorLoadOptions = {}) { const result = await this.Storage.load(options); - this.loadData(result); + this.loadData(result, loadOptions); // Wait in order to properly update the dirty counter (#5385) await wait(); @@ -875,12 +878,15 @@ export default class EditorModel extends Model { return JSON.parse(JSON.stringify(result)); } - loadData(data: ProjectData = {}): ProjectData { - if (!isEmptyObj(data)) { + loadData(project: ProjectData = {}, opts: EditorLoadOptions = {}): ProjectData { + let loaded = false; + if (!isEmptyObj(project)) { this.storables.forEach((module) => module.clear()); - this.storables.forEach((module) => module.load(data)); + this.storables.forEach((module) => module.load(project)); + loaded = true; } - return data; + this.trigger(EditorEvents.projectLoad, { project, loaded, initial: !!opts.initial }); + return project; } /** @@ -1025,7 +1031,7 @@ export default class EditorModel extends Model { */ destroyAll() { const { config, view } = this; - this.trigger('destroy'); + this.trigger(EditorEvents.destroy); const editor = this.getEditor(); // @ts-ignore const { editors = [] } = config.grapesjs || {}; @@ -1048,7 +1054,7 @@ export default class EditorModel extends Model { editors.splice(editors.indexOf(editor), 1); //@ts-ignore hasWin() && $(config.el).empty().attr(this.attrsOrig); - this.trigger('destroyed'); + this.trigger(EditorEvents.destroyed); } getEditing(): Component | undefined { @@ -1066,12 +1072,13 @@ export default class EditorModel extends Model { } log(msg: string, opts: any = {}) { + const logEvent = EditorEvents.log; const { ns, level = 'debug' } = opts; - this.trigger('log', msg, opts); - level && this.trigger(`log:${level}`, msg, opts); + this.trigger(logEvent, msg, opts); + level && this.trigger(`${logEvent}:${level}`, msg, opts); if (ns) { - const logNs = `log-${ns}`; + const logNs = `${logEvent}-${ns}`; this.trigger(logNs, msg, opts); level && this.trigger(`${logNs}:${level}`, msg, opts); } diff --git a/packages/core/src/editor/types.ts b/packages/core/src/editor/types.ts new file mode 100644 index 000000000..99960edb8 --- /dev/null +++ b/packages/core/src/editor/types.ts @@ -0,0 +1,66 @@ +/**{START_EVENTS}*/ +export enum EditorEvents { + /** + * @event `update` Event triggered on any change of the project (eg. component added/removed, style changes, etc.) + * @example + * editor.on('update', () => { ... }); + */ + update = 'update', + + /** + * @event `undo` Undo executed. + * @example + * editor.on('undo', () => { ... }); + */ + undo = 'undo', + + /** + * @event `redo` Redo executed. + * @example + * editor.on('redo', () => { ... }); + */ + redo = 'redo', + + /** + * @event `load` Editor is loaded. At this stage, the project is loaded in the editor and elements in the canvas are rendered. + * @example + * editor.on('load', () => { ... }); + */ + load = 'load', + + /** + * @event `project:load` Project JSON loaded in the editor. The event is triggered on the initial load and on the `editor.loadProjectData` method. + * @example + * editor.on('project:load', ({ project, initial }) => { ... }); + */ + projectLoad = 'project:load', + + /** + * @event `log` Log message triggered. + * @example + * editor.on('log', (msg, opts) => { ... }); + */ + log = 'log', + + /** + * @event `telemetry:init` Initial telemetry data are sent. + * @example + * editor.on('telemetry:init', () => { ... }); + */ + telemetryInit = 'telemetry:init', + + /** + * @event `destroy` Editor started destroy (on `editor.destroy()`). + * @example + * editor.on('destroy', () => { ... }); + */ + destroy = 'destroy', + + /** + * @event `destroyed` Editor destroyed. + * @example + * editor.on('destroyed', () => { ... }); + */ + destroyed = 'destroyed', +} +/**{END_EVENTS}*/ diff --git a/packages/core/src/editor/view/EditorView.ts b/packages/core/src/editor/view/EditorView.ts index 18d20306b..a45d06327 100644 --- a/packages/core/src/editor/view/EditorView.ts +++ b/packages/core/src/editor/view/EditorView.ts @@ -2,6 +2,7 @@ import { View, $ } from '../../common'; import { getHostName } from '../../utils/host-name'; import { appendStyles } from '../../utils/mixins'; import EditorModel from '../model/Editor'; +import { EditorEvents } from '../types'; export default class EditorView extends View { constructor(model: EditorModel) { @@ -20,7 +21,7 @@ export default class EditorView extends View { } setTimeout(() => { - model.trigger('load', model.Editor); + model.trigger(EditorEvents.load, model.Editor); model.clearDirtyCount(); }); }); @@ -91,6 +92,6 @@ export default class EditorView extends View { } }); - this.trigger('telemetry:sent'); + this.trigger(EditorEvents.telemetryInit); } } diff --git a/packages/core/src/parser/index.ts b/packages/core/src/parser/index.ts index e4ec87f41..2349529c5 100644 --- a/packages/core/src/parser/index.ts +++ b/packages/core/src/parser/index.ts @@ -13,9 +13,8 @@ * ```js * const { Parser } = editor; * ``` - * ## Available Events - * * `parse:html` - On HTML parse, an object containing the input and the output of the parser is passed as an argument - * * `parse:css` - On CSS parse, an object containing the input and the output of the parser is passed as an argument + * + * {REPLACE_EVENTS} * * ## Methods * * [getConfig](#getconfig) @@ -25,14 +24,17 @@ * @module Parser */ import { Module } from '../abstract'; +import { ObjectAny } from '../common'; import EditorModel from '../editor/model/Editor'; import defConfig, { HTMLParserOptions, ParserConfig } from './config/config'; import ParserCss from './model/ParserCss'; import ParserHtml from './model/ParserHtml'; +import { ParserEvents } from './types'; export default class ParserModule extends Module { parserHtml: ReturnType; parserCss: ReturnType; + events = ParserEvents; constructor(em: EditorModel) { super(em, 'Parser', defConfig()); @@ -85,5 +87,11 @@ export default class ParserModule extends Module ({ /** @@ -10,13 +11,22 @@ const ParserCss = (em?: EditorModel, config: ParserConfig = {}) => ({ * @param {String} input CSS string * @return {Array} */ - parse(input: string) { + parse(input: string, opts: { throwOnError?: boolean } = {}) { let output: CssRuleJSON[] = []; const { parserCss } = config; const editor = em?.Editor; - const nodes = parserCss ? parserCss(input, editor!) : BrowserCssParser(input); + let nodes: CssRuleJSON[] | ParsedCssRule[] = []; + let error: unknown; + + try { + nodes = parserCss ? parserCss(input, editor!) : BrowserCssParser(input); + } catch (err) { + error = err; + if (opts.throwOnError) throw err; + } + nodes.forEach((node) => (output = output.concat(this.checkNode(node)))); - em?.trigger('parse:css', { input, output, nodes }); + em?.Parser?.__emitEvent(ParserEvents.css, { input, output, nodes, error }); return output; }, diff --git a/packages/core/src/parser/model/ParserHtml.ts b/packages/core/src/parser/model/ParserHtml.ts index e05c5bd1f..96c7b3121 100644 --- a/packages/core/src/parser/model/ParserHtml.ts +++ b/packages/core/src/parser/model/ParserHtml.ts @@ -6,9 +6,9 @@ import { HTMLParseResult, HTMLParserOptions, ParseNodeOptions, ParserConfig } fr import BrowserParserHtml from './BrowserParserHtml'; import { doctypeToString } from '../../utils/dom'; import { isDef } from '../../utils/mixins'; +import { ParserEvents } from '../types'; const modelAttrStart = 'data-gjs-'; -const event = 'parse:html'; const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boolean } = {}) => { return { @@ -365,7 +365,7 @@ const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boo if (styleStr) res.css = parserCss.parse(styleStr); } - em?.trigger(`${event}:root`, { input, root: root }); + em?.Parser?.__emitEvent(ParserEvents.htmlRoot, { input, root }); let resHtml: HTMLParseResult['html'] = []; if (asDocument) { @@ -379,7 +379,7 @@ const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boo } res.html = resHtml; - em?.trigger(event, { input, output: res, options }); + em?.Parser?.__emitEvent(ParserEvents.html, { input, output: res, options }); return res; }, diff --git a/packages/core/src/parser/types.ts b/packages/core/src/parser/types.ts new file mode 100644 index 000000000..bf1e2bcf3 --- /dev/null +++ b/packages/core/src/parser/types.ts @@ -0,0 +1,25 @@ +/**{START_EVENTS}*/ +export enum ParserEvents { + /** + * @event `parse:html` On HTML parse, an object containing the input and the output of the parser is passed as an argument. + * @example + * editor.on('parse:html', ({ input, output }) => { ... }); + */ + html = 'parse:html', + htmlRoot = 'parse:html:root', + + /** + * @event `parse:css` On CSS parse, an object containing the input and the output of the parser is passed as an argument. + * @example + * editor.on('parse:css', ({ input, output }) => { ... }); + */ + css = 'parse:css', + + /** + * @event `parse` Catch-all event for all the events mentioned above. An object containing all the available data about the triggered event is passed as an argument to the callback. + * @example + * editor.on('parse', ({ event, ... }) => { ... }); + */ + all = 'parse', +} +/**{END_EVENTS}*/ diff --git a/packages/core/src/undo_manager/index.ts b/packages/core/src/undo_manager/index.ts index 5ff3c2ba0..07deafeaa 100644 --- a/packages/core/src/undo_manager/index.ts +++ b/packages/core/src/undo_manager/index.ts @@ -29,6 +29,7 @@ import { isArray, isBoolean, isEmpty, unique, times } from 'underscore'; import { Module } from '../abstract'; import EditorModel from '../editor/model/Editor'; import defConfig, { UndoManagerConfig } from './config'; +import { EditorEvents } from '../editor/types'; export interface UndoGroup { index: number; @@ -144,7 +145,7 @@ export default class UndoManagerModule extends Module { em.getSelectedAll().map((c) => c.trigger('rerender:layer')); }); - ['undo', 'redo'].forEach((ev) => this.um.on(ev, () => em.trigger(ev))); + [EditorEvents.undo, EditorEvents.redo].forEach((ev) => this.um.on(ev, () => em.trigger(ev))); } postLoad() {