diff --git a/packages/core/src/canvas/index.ts b/packages/core/src/canvas/index.ts index faffaea3d..7be4ffb81 100644 --- a/packages/core/src/canvas/index.ts +++ b/packages/core/src/canvas/index.ts @@ -696,6 +696,7 @@ export default class CanvasModule extends Module { } toggleFramesEvents(on: boolean) { + // Seems like this causing a bug for iframes in Chrome: https://issues.chromium.org/issues/41336877 const { style } = this.getFramesEl(); style.pointerEvents = on ? '' : 'none'; } diff --git a/packages/core/src/commands/index.ts b/packages/core/src/commands/index.ts index 0cd98354c..c1cf426c9 100644 --- a/packages/core/src/commands/index.ts +++ b/packages/core/src/commands/index.ts @@ -383,18 +383,13 @@ export default class CommandsModule extends Module { ... }); */ runCommand = 'command:run:', - _runCommand = 'run:', /** * @event `command:run:before:COMMAND-ID` Triggered before the command is called. @@ -38,7 +36,6 @@ export enum CommandsEvents { * }); */ abort = 'command:abort:', - _abort = 'abort:', /** * @event `command:stop` Triggered on stop of any command. @@ -48,7 +45,6 @@ export enum CommandsEvents { * }); */ stop = 'command:stop', - _stop = 'stop', /** * @event `command:stop:COMMAND-ID` Triggered on stop of a specific command. @@ -56,7 +52,6 @@ export enum CommandsEvents { * editor.on('command:run:my-command', ({ result, options }) => { ... }); */ stopCommand = 'command:stop:', - _stopCommand = 'stop:', /** * @event `command:stop:before:COMMAND-ID` Triggered before the command is called to stop. @@ -64,6 +59,22 @@ export enum CommandsEvents { * editor.on('command:stop:before:my-command', ({ options }) => { ... }); */ stopBeforeCommand = 'command:stop:before:', + + /** + * @event `command:call` Triggered on run or stop of a command. + * @example + * editor.on('command:call', ({ id, result, options, type }) => { + * console.log('Command id', id, 'command result', result, 'call type', type); + * }); + */ + call = 'command:call', + + /** + * @event `command:call:COMMAND-ID` Triggered on run or stop of a specific command. + * @example + * editor.on('command:call:my-command', ({ result, options, type }) => { ... }); + */ + callCommand = 'command:call:', } /**{END_EVENTS}*/ diff --git a/packages/core/src/commands/view/CommandAbstract.ts b/packages/core/src/commands/view/CommandAbstract.ts index 6ada4faec..d6c692f1c 100644 --- a/packages/core/src/commands/view/CommandAbstract.ts +++ b/packages/core/src/commands/view/CommandAbstract.ts @@ -35,6 +35,7 @@ export default class CommandAbstract extends Model { plhClass: string; freezClass: string; canvas: CanvasModule; + noStop?: boolean; constructor(o: any) { super(0); @@ -111,22 +112,26 @@ export default class CommandAbstract extends Model { callRun(editor: Editor, options: any = {}) { const { id } = this; editor.trigger(`${CommandsEvents.runBeforeCommand}${id}`, { options }); - editor.trigger(`${CommandsEvents._runCommand}${id}:before`, options); if (options.abort) { editor.trigger(`${CommandsEvents.abort}${id}`, { options }); - editor.trigger(`${CommandsEvents._abort}${id}`, options); return; } const sender = options.sender || editor; const result = this.run(editor, sender, options); const data = { id, result, options }; + const dataCall = { ...data, type: 'run' }; + + if (!this.noStop) { + editor.Commands.active[id] = result; + } + editor.trigger(`${CommandsEvents.runCommand}${id}`, data); + editor.trigger(`${CommandsEvents.callCommand}${id}`, dataCall); editor.trigger(CommandsEvents.run, data); - // deprecated - editor.trigger(`${CommandsEvents._runCommand}${id}`, result, options); - editor.trigger(CommandsEvents._run, id, result, options); + editor.trigger(CommandsEvents.call, dataCall); + return result; } @@ -140,15 +145,14 @@ export default class CommandAbstract extends Model { const { id } = this; const sender = options.sender || editor; editor.trigger(`${CommandsEvents.stopBeforeCommand}${id}`, { options }); - editor.trigger(`${CommandsEvents._stopCommand}${id}:before`, options); const result = this.stop(editor, sender, options); const data = { id, result, options }; + const dataCall = { ...data, type: 'stop' }; + delete editor.Commands.active[id]; editor.trigger(`${CommandsEvents.stopCommand}${id}`, data); + editor.trigger(`${CommandsEvents.callCommand}${id}`, dataCall); editor.trigger(CommandsEvents.stop, data); - - // deprecated - editor.trigger(`${CommandsEvents._stopCommand}${id}`, result, options); - editor.trigger(CommandsEvents._stop, id, result, options); + editor.trigger(CommandsEvents.call, dataCall); return result; } diff --git a/packages/core/src/commands/view/SelectComponent.ts b/packages/core/src/commands/view/SelectComponent.ts index 69f37e73b..462b9fca1 100644 --- a/packages/core/src/commands/view/SelectComponent.ts +++ b/packages/core/src/commands/view/SelectComponent.ts @@ -439,7 +439,6 @@ export default { const { keyHeight, keyWidth, currentUnit, keepAutoHeight, keepAutoWidth } = config; toggleBodyClass('add', ev, opts); modelToStyle = em.Styles.getModelToStyle(model); - canvas.toggleFramesEvents(false); const computedStyle = getComputedStyle(el); const modelStyle = modelToStyle.getStyle(); @@ -477,7 +476,6 @@ export default { onEnd(ev, opts); toggleBodyClass('remove', ev, opts); editor.trigger('component:resize', { ...resizeEventOpts, type: 'end' }); - canvas.toggleFramesEvents(true); showOffsets = true; self.activeResizer = false; }, diff --git a/packages/core/src/editor/model/Editor.ts b/packages/core/src/editor/model/Editor.ts index 88bd27369..1c9918a60 100644 --- a/packages/core/src/editor/model/Editor.ts +++ b/packages/core/src/editor/model/Editor.ts @@ -875,7 +875,9 @@ export default class EditorModel extends Model { this.storables.forEach((m) => { result = { ...result, ...m.store(1) }; }); - return JSON.parse(JSON.stringify(result)); + const project = JSON.parse(JSON.stringify(result)); + this.trigger(EditorEvents.projectGet, { project }); + return project; } loadData(project: ProjectData = {}, opts: EditorLoadOptions = {}): ProjectData { diff --git a/packages/core/src/editor/types.ts b/packages/core/src/editor/types.ts index 99960edb8..4dba074be 100644 --- a/packages/core/src/editor/types.ts +++ b/packages/core/src/editor/types.ts @@ -35,6 +35,13 @@ export enum EditorEvents { */ projectLoad = 'project:load', + /** + * @event `project:get` Event triggered on request of the project data. This can be used to extend the project with custom data. + * @example + * editor.on('project:get', ({ project }) => { project.myCustomKey = 'value' }); + */ + projectGet = 'project:get', + /** * @event `log` Log message triggered. * @example diff --git a/packages/core/src/parser/model/ParserCss.ts b/packages/core/src/parser/model/ParserCss.ts index 84f8c6fbc..c9ad2b513 100644 --- a/packages/core/src/parser/model/ParserCss.ts +++ b/packages/core/src/parser/model/ParserCss.ts @@ -8,15 +8,19 @@ import { ParserEvents } from '../types'; const ParserCss = (em?: EditorModel, config: ParserConfig = {}) => ({ /** * Parse CSS string to a desired model object - * @param {String} input CSS string + * @param {String} str CSS string * @return {Array} */ - parse(input: string, opts: { throwOnError?: boolean } = {}) { + parse(str: string, opts: { throwOnError?: boolean } = {}) { let output: CssRuleJSON[] = []; const { parserCss } = config; const editor = em?.Editor; let nodes: CssRuleJSON[] | ParsedCssRule[] = []; let error: unknown; + const Parser = em?.Parser; + const inputOptions = { input: str }; + Parser?.__emitEvent(ParserEvents.cssBefore, inputOptions); + const { input } = inputOptions; try { nodes = parserCss ? parserCss(input, editor!) : BrowserCssParser(input); @@ -26,7 +30,7 @@ const ParserCss = (em?: EditorModel, config: ParserConfig = {}) => ({ } nodes.forEach((node) => (output = output.concat(this.checkNode(node)))); - em?.Parser?.__emitEvent(ParserEvents.css, { input, output, nodes, error }); + 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 96c7b3121..d19edd128 100644 --- a/packages/core/src/parser/model/ParserHtml.ts +++ b/packages/core/src/parser/model/ParserHtml.ts @@ -312,6 +312,7 @@ const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boo */ parse(str: string, parserCss?: any, opts: HTMLParserOptions = {}) { const conf = em?.get('Config') || {}; + const Parser = em?.Parser; const res: HTMLParseResult = { html: [] }; const cf = { ...config, ...opts }; const preOptions = { @@ -325,7 +326,9 @@ const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boo asDocument: this.__checkAsDocument(str, preOptions), }; const { preParser, asDocument } = options; - const input = isFunction(preParser) ? preParser(str, { editor: em?.getEditor()! }) : str; + const inputOptions = { input: isFunction(preParser) ? preParser(str, { editor: em?.getEditor()! }) : str }; + Parser?.__emitEvent(ParserEvents.htmlBefore, inputOptions); + const { input } = inputOptions; const parseRes = isFunction(cf.parserHtml) ? cf.parserHtml(input, options) : BrowserParserHtml(input, options); let root = parseRes as HTMLElement; const docEl = parseRes as Document; @@ -365,7 +368,7 @@ const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boo if (styleStr) res.css = parserCss.parse(styleStr); } - em?.Parser?.__emitEvent(ParserEvents.htmlRoot, { input, root }); + Parser?.__emitEvent(ParserEvents.htmlRoot, { input, root }); let resHtml: HTMLParseResult['html'] = []; if (asDocument) { @@ -379,7 +382,7 @@ const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boo } res.html = resHtml; - em?.Parser?.__emitEvent(ParserEvents.html, { input, output: res, options }); + 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 index bf1e2bcf3..b9f3839d1 100644 --- a/packages/core/src/parser/types.ts +++ b/packages/core/src/parser/types.ts @@ -8,6 +8,17 @@ export enum ParserEvents { html = 'parse:html', htmlRoot = 'parse:html:root', + /** + * @event `parse:html:before` Event triggered before the HTML parsing starts. An object containing the input is passed as an argument. + * @example + * editor.on('parse:html:before', (options) => { + * console.log('Parser input', options.input); + * // You can also process the input and update `options.input` + * options.input += '
Extra content
'; + * }); + */ + htmlBefore = 'parse:html:before', + /** * @event `parse:css` On CSS parse, an object containing the input and the output of the parser is passed as an argument. * @example @@ -15,6 +26,17 @@ export enum ParserEvents { */ css = 'parse:css', + /** + * @event `parse:css:before` Event triggered before the CSS parsing starts. An object containing the input is passed as an argument. + * @example + * editor.on('parse:css:before', (options) => { + * console.log('Parser input', options.input); + * // You can also process the input and update `options.input` + * options.input += '.my-class { color: red; }'; + * }); + */ + cssBefore = 'parse:css:before', + /** * @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 diff --git a/packages/core/src/styles/scss/main.scss b/packages/core/src/styles/scss/main.scss index 5bade05a5..ea51ee427 100644 --- a/packages/core/src/styles/scss/main.scss +++ b/packages/core/src/styles/scss/main.scss @@ -99,18 +99,15 @@ $colorsAll: (one, var(--gjs-primary-color)), (two, var(--gjs-secondary-color)), border: none; color: inherit; } + .#{$app-prefix}no-app { height: 10px; } -.#{$app-prefix}test { - &btn { - color: '#fff'; - } -} .opac50 { @include opacity(0.5); } + .#{$app-prefix}checker-bg, .checker-bg { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg=='); diff --git a/packages/core/test/specs/commands/index.ts b/packages/core/test/specs/commands/index.ts index 667cfb118..310369afa 100644 --- a/packages/core/test/specs/commands/index.ts +++ b/packages/core/test/specs/commands/index.ts @@ -1,4 +1,3 @@ -import Backbone from 'backbone'; import Commands from '../../../src/commands'; import EditorModel from '../../../src/editor/model/Editor'; import { Command, CommandFunction } from '../../../src/commands/view/CommandAbstract'; @@ -24,7 +23,7 @@ describe('Commands', () => { }; commFunc = () => commResultRun; em = new EditorModel(); - em.set('Editor', { ...Backbone.Events }); + em.set('Editor', em); obj = em.Commands; }); diff --git a/packages/core/test/specs/commands/view/CommandAbstract.ts b/packages/core/test/specs/commands/view/CommandAbstract.ts index 12da6f389..9aa639209 100644 --- a/packages/core/test/specs/commands/view/CommandAbstract.ts +++ b/packages/core/test/specs/commands/view/CommandAbstract.ts @@ -20,15 +20,16 @@ describe('CommandAbstract', () => { const result = command.callRun(editor); const options = {}; const resOptions = { options, id: command.id, result: returnValue }; - expect(triggerSpy.mock.calls.length).toBe(6); + const resCallOptions = { ...resOptions, type: 'run' }; + + expect(triggerSpy.mock.calls.length).toBe(5); expect(triggerSpy.mock.calls[0]).toEqual([`${CommandsEvents.runBeforeCommand}test`, { options }]); - expect(triggerSpy.mock.calls[1]).toEqual(['run:test:before', options]); - expect(triggerSpy.mock.calls[2]).toEqual([`${CommandsEvents.runCommand}test`, resOptions]); + expect(triggerSpy.mock.calls[1]).toEqual([`${CommandsEvents.runCommand}test`, resOptions]); + expect(triggerSpy.mock.calls[2]).toEqual([`${CommandsEvents.callCommand}test`, resCallOptions]); expect(triggerSpy.mock.calls[3]).toEqual([CommandsEvents.run, resOptions]); - expect(triggerSpy.mock.calls[4]).toEqual(['run:test', returnValue, options]); - expect(triggerSpy.mock.calls[5]).toEqual(['run', 'test', returnValue, options]); + expect(triggerSpy.mock.calls[4]).toEqual([CommandsEvents.call, resCallOptions]); - expect(runSpy).toBeCalledTimes(1); + expect(runSpy).toHaveBeenCalledTimes(1); expect(result).toEqual(returnValue); }); @@ -40,13 +41,11 @@ describe('CommandAbstract', () => { runSpy.mockReturnValue(returnValue as any); const result = command.callRun(editor, options); - expect(triggerSpy.mock.calls.length).toBe(4); + expect(triggerSpy.mock.calls.length).toBe(2); expect(triggerSpy.mock.calls[0]).toEqual([`${CommandsEvents.runBeforeCommand}test`, { options }]); - expect(triggerSpy.mock.calls[1]).toEqual(['run:test:before', options]); - expect(triggerSpy.mock.calls[2]).toEqual([`${CommandsEvents.abort}test`, { options }]); - expect(triggerSpy.mock.calls[3]).toEqual(['abort:test', options]); + expect(triggerSpy.mock.calls[1]).toEqual([`${CommandsEvents.abort}test`, { options }]); - expect(runSpy).toBeCalledTimes(0); + expect(runSpy).toHaveBeenCalledTimes(0); expect(result).toEqual(undefined); }); @@ -58,16 +57,16 @@ describe('CommandAbstract', () => { const result = command.callStop(editor); const options = {}; const resOptions = { options, id: command.id, result: returnValue }; + const resCallOptions = { ...resOptions, type: 'stop' }; - expect(triggerSpy.mock.calls.length).toBe(6); + expect(triggerSpy.mock.calls.length).toBe(5); expect(triggerSpy.mock.calls[0]).toEqual([`${CommandsEvents.stopBeforeCommand}test`, { options }]); - expect(triggerSpy.mock.calls[1]).toEqual(['stop:test:before', options]); - expect(triggerSpy.mock.calls[2]).toEqual([`${CommandsEvents.stopCommand}test`, resOptions]); + expect(triggerSpy.mock.calls[1]).toEqual([`${CommandsEvents.stopCommand}test`, resOptions]); + expect(triggerSpy.mock.calls[2]).toEqual([`${CommandsEvents.callCommand}test`, resCallOptions]); expect(triggerSpy.mock.calls[3]).toEqual([CommandsEvents.stop, resOptions]); - expect(triggerSpy.mock.calls[4]).toEqual(['stop:test', returnValue, options]); - expect(triggerSpy.mock.calls[5]).toEqual(['stop', 'test', returnValue, options]); + expect(triggerSpy.mock.calls[4]).toEqual([CommandsEvents.call, resCallOptions]); - expect(runSpy).toBeCalledTimes(1); + expect(runSpy).toHaveBeenCalledTimes(1); expect(result).toEqual(returnValue); }); }); diff --git a/packages/core/test/specs/grapesjs/index.ts b/packages/core/test/specs/grapesjs/index.ts index f080edec5..8dc9e5f44 100644 --- a/packages/core/test/specs/grapesjs/index.ts +++ b/packages/core/test/specs/grapesjs/index.ts @@ -241,18 +241,25 @@ describe('GrapesJS', () => { const id = 'test-command'; const editor = grapesjs.init(config); const result: Record = {}; - editor.on(`run:${id}`, () => (result.run = 1)); - editor.on(`run:${id}:before`, () => (result.runBefore = 1)); - editor.on(`stop:${id}`, () => (result.stop = 1)); - editor.on(`stop:${id}:before`, () => (result.stopBefore = 1)); - editor.on(`abort:${id}`, () => (result.abort = 1)); + const events = editor.Commands.events; + editor.on(`${events.run}:${id}`, () => { + expect(editor.Commands.isActive(id)).toBe(true); + result.run = 1; + }); + editor.on(`${events.runBeforeCommand}${id}`, () => (result.runBefore = 1)); + editor.on(`${events.stop}:${id}`, () => { + expect(editor.Commands.isActive(id)).toBe(false); + result.stop = 1; + }); + editor.on(`${events.stopBeforeCommand}${id}`, () => (result.stopBefore = 1)); + editor.on(`${events.abort}${id}`, () => (result.abort = 1)); editor.Commands.add(id, { run() {}, stop() {}, }); editor.runCommand(id); editor.stopCommand(id); - editor.on(`run:${id}:before`, (opts) => (opts.abort = 1)); + editor.on(`${events.runBeforeCommand}${id}`, ({ options }) => (options.abort = 1)); editor.runCommand(id); expect(result).toEqual({ run: 1, diff --git a/packages/core/test/specs/parser/model/ParserCss.ts b/packages/core/test/specs/parser/model/ParserCss.ts index aa1ce3e4b..c72919b13 100644 --- a/packages/core/test/specs/parser/model/ParserCss.ts +++ b/packages/core/test/specs/parser/model/ParserCss.ts @@ -18,16 +18,25 @@ export const CSS_BG_OBJ = { describe('ParserCss', () => { let obj: ReturnType; - let config; - let customParser: any; - let em = { - getCustomParserCss: () => customParser, - trigger: () => {}, - } as unknown as EditorModel; + let em: EditorModel; beforeEach(() => { - config = {}; - obj = ParserCss(em, config); + em = new EditorModel({}); + obj = ParserCss(em, {}); + }); + + afterEach(() => { + em.destroy(); + }); + + test('Extend parser input', () => { + const str = ' .test1 { color:red; }'; + const result = { + selectors: ['test1'], + style: { color: 'red' }, + }; + em.on(em.Parser.events.cssBefore, (opts) => (opts.input += str)); + expect(obj.parse(str)).toEqual([result, result]); }); describe('parseSelector', () => { diff --git a/packages/core/test/specs/parser/model/ParserHtml.ts b/packages/core/test/specs/parser/model/ParserHtml.ts index c01680be0..53314ec60 100644 --- a/packages/core/test/specs/parser/model/ParserHtml.ts +++ b/packages/core/test/specs/parser/model/ParserHtml.ts @@ -6,9 +6,10 @@ import { CSS_BG_OBJ, CSS_BG_STR } from './ParserCss'; describe('ParserHtml', () => { let obj: ReturnType; + let em: Editor; beforeEach(() => { - const em = new Editor({}); + em = new Editor({}); const dom = new DomComponents(em); obj = ParserHtml(em, { textTags: ['br', 'b', 'i', 'u'], @@ -18,6 +19,19 @@ describe('ParserHtml', () => { obj.compTypes = dom.componentTypes; }); + afterEach(() => { + em.destroy(); + }); + + test('Extend parser input', () => { + const str = '
'; + const result = { tagName: 'div' }; + em.on(em.Parser.events.htmlBefore, (opts) => { + opts.input += str; + }); + expect(obj.parse(str).html).toEqual([result, result]); + }); + test('Simple div node', () => { const str = '
'; const result = [{ tagName: 'div' }];