diff --git a/packages/core/src/canvas/model/Frame.ts b/packages/core/src/canvas/model/Frame.ts index ab535265e..7e909f76e 100644 --- a/packages/core/src/canvas/model/Frame.ts +++ b/packages/core/src/canvas/model/Frame.ts @@ -2,6 +2,7 @@ import { forEach, isEmpty, isNumber, isString, keys, result } from 'underscore'; import CanvasModule from '..'; import { ModuleModel } from '../../abstract'; import { BoxRect, PrevToNewIdMap } from '../../common'; +import type Component from '../../dom_components/model/Component'; import ComponentWrapper from '../../dom_components/model/ComponentWrapper'; import { ComponentDefinition } from '../../dom_components/model/types'; import Page from '../../pages/model/Page'; @@ -16,7 +17,7 @@ const keyAutoH = '__ah'; export interface FrameProperties { id?: string; - component?: string | ComponentDefinition | ComponentDefinition[] | ComponentWrapper; + component?: string | ComponentDefinition | ComponentDefinition[] | Component; width?: string | number | null; height?: string | number | null; x?: number; @@ -25,6 +26,7 @@ export interface FrameProperties { head?: { tag: string; attributes: any }[]; styles?: string | CssRuleJSON[]; refFrame?: string | Frame | null; + refComponent?: string | Component | null; skipFromStorage?: boolean; [key: string]: unknown; } @@ -87,7 +89,7 @@ export default class Frame extends ModuleModel { if (!isComponent(component)) { const wrp = isObject(component) ? component : { components: component }; !wrp.type && (wrp.type = 'wrapper'); - const Wrapper = domc.getType('wrapper')!.model; + const Wrapper = (domc.getType(wrp.type as string) || domc.getType('wrapper')!).model; this.set('component', new Wrapper(wrp, modOpts)); } @@ -128,17 +130,25 @@ export default class Frame extends ModuleModel { return this.get('refFrame'); } + get refComponent(): Component | undefined { + return this.get('refComponent'); + } + get root() { const { refFrame } = this; return refFrame?.getComponent() || this.getComponent(); } initRefs() { - const { refFrame } = this; + const { refFrame, refComponent, em } = this; if (isString(refFrame)) { const frame = this.module.framesById[refFrame]; frame && this.set({ refFrame: frame }, { silent: true }); } + if (isString(refComponent)) { + const component = em.Components.getById(refComponent); + component && this.set({ refComponent: component }, { silent: true }); + } } getBoxRect(): BoxRect { @@ -154,7 +164,7 @@ export default class Frame extends ModuleModel { } onRemove() { - !this.refFrame && this.getComponent().remove({ root: 1 }); + !this.refFrame && !this.refComponent && this.getComponent().remove({ root: 1 }); } changesUp(opt: any = {}) { @@ -271,6 +281,7 @@ export default class Frame extends ModuleModel { if (opts.fromUndo) delete obj.component; delete obj.skipFromStorage; + delete obj.refComponent; delete obj.styles; delete obj.changesCount; obj[keyAutoW] && delete obj.width; diff --git a/packages/core/src/canvas/view/FrameView.ts b/packages/core/src/canvas/view/FrameView.ts index 9542b347e..927ea56bb 100644 --- a/packages/core/src/canvas/view/FrameView.ts +++ b/packages/core/src/canvas/view/FrameView.ts @@ -1,6 +1,6 @@ import { bindAll, debounce, isFunction, isString } from 'underscore'; import { ModuleView } from '../../abstract'; -import { BoxRect, ObjectAny } from '../../common'; +import { BoxRect } from '../../common'; import CssRulesView from '../../css_composer/view/CssRulesView'; import { type as typeHead } from '../../dom_components/model/ComponentHead'; import ComponentView from '../../dom_components/view/ComponentView'; @@ -36,6 +36,7 @@ export default class FrameView extends ModuleView { private tools: { [key: string]: HTMLElement } = {}; private wrapper?: ComponentWrapperView; private headView?: ComponentView; + private refComponentView?: ComponentView; private frameWrapView?: FrameWrapView; constructor(model: Frame, view?: FrameWrapView) { @@ -145,8 +146,12 @@ export default class FrameView extends ModuleView { return this.getDoc().querySelector('body') as HTMLBodyElement; } + getRootType() { + return this.model.root.get('type') || 'wrapper'; + } + getWrapper() { - return this.getBody().querySelector('[data-gjs-type=wrapper]') as HTMLElement; + return (this.wrapper?.el || this.getBody().querySelector(`[data-gjs-type="${this.getRootType()}"]`)) as HTMLElement; } getJsContainer() { @@ -177,6 +182,19 @@ export default class FrameView extends ModuleView { return this._getTool('[data-offset]'); } + getComponentView(component: any) { + const { Components } = this.em; + const type = component.get('type') || 'default'; + const dt = Components.getTypes(); + const { view = ComponentView } = Components.getType(type) || Components.getType('default') || {}; + + if (!view.getEvents) { + view.getEvents = ComponentView.getEvents; + } + + return { dt, view }; + } + getRect() { if (!this.rect) { this.rect = this.el.getBoundingClientRect(); @@ -220,6 +238,8 @@ export default class FrameView extends ModuleView { remove(...args: any) { this._toggleEffects(false); this.tools = {}; + this.refComponentView?.remove(); + this.refComponentView = undefined; this.wrapper?.remove(); ModuleView.prototype.remove.apply(this, args); return this; @@ -350,6 +370,7 @@ export default class FrameView extends ModuleView { this.renderStyles({ prev: [] }); const colorWarn = '#ffca6f'; + const rootType = this.getRootType(); append( body, @@ -358,7 +379,7 @@ export default class FrameView extends ModuleView { ${hasAutoHeight ? 'body { overflow: hidden }' : ''} - [data-gjs-type="wrapper"] { + [data-gjs-type="${rootType}"] { ${!hasAutoHeight ? 'min-height: 100vh;' : ''} padding-top: 0.001em; } @@ -426,7 +447,7 @@ export default class FrameView extends ModuleView { `, ); const { root } = model; - const { view } = em?.Components?.getType('wrapper') || {}; + const { view } = this.getComponentView(root); if (!view) return; if (isFunction(config.customRenderer)) { @@ -497,10 +518,34 @@ export default class FrameView extends ModuleView { this.droppable = new Droppable(em, this.wrapper?.el); } + this.renderRefComponent(rootView); this.loaded = true; model.trigger('loaded'); } + renderRefComponent(rootView: ComponentView) { + const frame = this.model; + const refComponent = frame.refComponent; + if (!refComponent) return; + + const { dt, view: viewClass } = this.getComponentView(refComponent); + + const view = + refComponent.getView(frame) || + new viewClass({ + model: refComponent, + config: { + ...refComponent.config, + em: this.em, + frameView: this, + }, + componentTypes: dt, + }); + + this.refComponentView = view.render(); + rootView.getChildrenContainer().appendChild(view.el); + } + _toggleEffects(enable: boolean) { const method = enable ? on : off; const win = this.getWindow(); diff --git a/packages/core/src/dom_components/index.ts b/packages/core/src/dom_components/index.ts index 66127ce73..17495a090 100644 --- a/packages/core/src/dom_components/index.ts +++ b/packages/core/src/dom_components/index.ts @@ -610,6 +610,7 @@ export default class ComponentManager extends ItemManagerModule { } as any); expect(pm.getAll().map((page) => page.getId())).toEqual(['page-stored', 'page-skipped']); - expect(pm.get('page-stored')?.getFrames().map((frame) => frame.id)).toEqual(['frame-stored', 'frame-skipped']); - expect(pm.get('page-skipped')?.getFrames().map((frame) => frame.id)).toEqual(['frame-on-skipped-page']); + expect( + pm + .get('page-stored') + ?.getFrames() + .map((frame) => frame.id), + ).toEqual(['frame-stored', 'frame-skipped']); + expect( + pm + .get('page-skipped') + ?.getFrames() + .map((frame) => frame.id), + ).toEqual(['frame-on-skipped-page']); const storedPages = editor.getProjectData().pages; expect(storedPages.map((page: any) => page.id)).toEqual(['page-stored']); @@ -474,4 +484,82 @@ describe('Pages in canvas', () => { await waitEditorEvent(em, CanvasEvents.frameLoadBody); expect(getPageContent()).toEqual('Page 2'); }); + + test('Page with refComponent renders the same model and keeps original ownership', async () => { + const mainPage = pm.getMain(); + const mainWrapper = mainPage.getMainComponent(); + const target = mainWrapper.append({ + attributes: { id: 'isolated-component' }, + content: 'Original content', + })[0]; + + const tempPage = pm.add( + { + id: 'temp-page', + frames: [{ refComponent: target }], + skipFromStorage: true, + }, + { select: true }, + )!; + + await waitEditorEvent(em, CanvasEvents.frameLoadBody); + expect(canvas.getBody().querySelector('#isolated-component')?.textContent).toBe('Original content'); + expect(target.parent()).toBe(mainWrapper); + + target.set('content', 'Updated content'); + expect(canvas.getBody().querySelector('#isolated-component')?.textContent).toBe('Updated content'); + + pm.select(mainPage); + await waitEditorEvent(em, CanvasEvents.frameLoadBody); + expect(canvas.getBody().querySelector('#isolated-component')?.textContent).toBe('Updated content'); + + pm.remove(tempPage); + expect(mainWrapper.components().models).toContain(target); + expect(target.parent()).toBe(mainWrapper); + }); + + test('Page supports a custom wrapper type for frames', async () => { + editor.Components.addType('wrapper-component', { + extend: 'wrapper', + model: { + defaults: { customWrapperFlag: true }, + getCustomWrapperFlag() { + return this.get('customWrapperFlag'); + }, + }, + view: { + onRender() { + this.el.setAttribute('data-custom-wrapper', 'true'); + }, + }, + }); + + const mainWrapper = pm.getMain().getMainComponent(); + const target = mainWrapper.append({ + attributes: { id: 'custom-wrapper-target' }, + content: 'Custom wrapper target', + })[0]; + + const tempPage = pm.add( + { + id: 'temp-page-custom-wrapper', + skipFromStorage: true, + frames: [ + { + component: { type: 'wrapper-component' }, + refComponent: target, + }, + ], + }, + { select: true }, + )!; + + await waitEditorEvent(em, CanvasEvents.frameLoadBody); + + const tempWrapper = tempPage.getMainComponent() as any; + expect(tempWrapper.is('wrapper-component')).toBe(true); + expect(tempWrapper.getCustomWrapperFlag()).toBe(true); + expect(canvas.getBody().querySelector('[data-custom-wrapper="true"]')).toBeTruthy(); + expect(canvas.getBody().querySelector('#custom-wrapper-target')?.textContent).toBe('Custom wrapper target'); + }); });