Browse Source

Improve pages/frames (#6790)

* Add skipFromStorage support to page and frames

* Add custom wrapper support to frames and refComponent

* Fix Component TS

* Ensure frame reference in components

* Add SelectComponentCommandOptions

* Update runDefault/stopDefault

* Fix component add event with page cloning

* Up TS
release-v0.23.3
Artur Arseniev 2 weeks ago
committed by GitHub
parent
commit
3889c8f582
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 60
      packages/core/src/canvas/model/Frame.ts
  2. 4
      packages/core/src/canvas/model/Frames.ts
  3. 53
      packages/core/src/canvas/view/FrameView.ts
  4. 34
      packages/core/src/commands/view/SelectComponent.ts
  5. 1
      packages/core/src/dom_components/index.ts
  6. 13
      packages/core/src/dom_components/model/Component.ts
  7. 19
      packages/core/src/dom_components/model/Components.ts
  8. 20
      packages/core/src/editor/model/Editor.ts
  9. 2
      packages/core/src/pages/index.ts
  10. 31
      packages/core/src/pages/model/Page.ts
  11. 13
      packages/core/test/specs/dom_components/model/Component.ts
  12. 238
      packages/core/test/specs/pages/index.ts

60
packages/core/src/canvas/model/Frame.ts

@ -2,7 +2,10 @@ 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 { ComponentsEvents } from '../../dom_components/types';
import Page from '../../pages/model/Page';
import { createId, isComponent, isObject } from '../../utils/mixins';
import FrameView from '../view/FrameView';
@ -13,6 +16,23 @@ import CanvasEvents from '../types';
const keyAutoW = '__aw';
const keyAutoH = '__ah';
export interface FrameProperties {
id?: string;
page?: Page;
component?: string | ComponentDefinition | ComponentDefinition[] | Component;
width?: string | number | null;
height?: string | number | null;
x?: number;
y?: number;
attributes?: Record<string, unknown>;
head?: { tag: string; attributes: any }[];
styles?: string | CssRuleJSON[];
refFrame?: string | Frame | null;
refComponent?: string | Component | null;
skipFromStorage?: boolean;
[key: string]: unknown;
}
const getDimension = (frame: Frame, type: 'width' | 'height') => {
const dim = frame.get(type);
const viewDim = frame.view?.el[type === 'width' ? 'offsetWidth' : 'offsetHeight'];
@ -37,6 +57,7 @@ const getDimension = (frame: Frame, type: 'width' | 'height') => {
*
*/
export default class Frame extends ModuleModel<CanvasModule> {
page?: Page;
defaults() {
return {
x: 0,
@ -58,8 +79,11 @@ export default class Frame extends ModuleModel<CanvasModule> {
/**
* @hideconstructor
*/
constructor(module: CanvasModule, attr: any) {
constructor(module: CanvasModule, attr: FrameProperties) {
const page = attr.page;
delete attr.page;
super(module, attr);
this.page = page;
const { em } = this;
const { styles, component } = this.attributes;
const domc = em.Components;
@ -71,8 +95,11 @@ export default class Frame extends ModuleModel<CanvasModule> {
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));
} else {
this.updateComponentFrame(component);
this.emitComponentAdd(component);
}
if (!styles) {
@ -112,17 +139,38 @@ export default class Frame extends ModuleModel<CanvasModule> {
return this.get('refFrame');
}
get refComponent(): Component | undefined {
return this.get('refComponent');
}
updateComponentFrame(component: Component) {
if (component.frame !== this) {
component.opt.frame = this;
}
component.components().forEach((child) => this.updateComponentFrame(child));
}
emitComponentAdd(component: Component, opts: Record<string, any> = {}) {
this.em.trigger(ComponentsEvents.add, component, opts);
component.components().forEach((child) => this.emitComponentAdd(child, opts));
}
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 {
@ -138,7 +186,7 @@ export default class Frame extends ModuleModel<CanvasModule> {
}
onRemove() {
!this.refFrame && this.getComponent().remove({ root: 1 });
!this.refFrame && !this.refComponent && this.getComponent().remove({ root: 1 });
}
changesUp(opt: any = {}) {
@ -224,7 +272,7 @@ export default class Frame extends ModuleModel<CanvasModule> {
}
getPage(): Page | undefined {
return (this.collection as unknown as Frames)?.page;
return this.page || (this.collection as unknown as Frames)?.page;
}
_emitUpdated(data = {}) {
@ -254,6 +302,8 @@ export default class Frame extends ModuleModel<CanvasModule> {
const defaults = result(this, 'defaults');
if (opts.fromUndo) delete obj.component;
delete obj.skipFromStorage;
delete obj.refComponent;
delete obj.styles;
delete obj.changesCount;
obj[keyAutoW] && delete obj.width;

4
packages/core/src/canvas/model/Frames.ts

@ -36,6 +36,10 @@ export default class Frames extends ModuleCollection<Frame> {
this.forEach((frame) => frame.initRefs());
}
toJSON(opts?: Parameters<Frame['toJSON']>[0]) {
return this.filter((frame) => !frame.get('skipFromStorage')).map((frame) => frame.toJSON(opts));
}
itemLoaded() {
this.loadedItems++;

53
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<Frame, HTMLIFrameElement> {
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<Frame, HTMLIFrameElement> {
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<Frame, HTMLIFrameElement> {
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<Frame, HTMLIFrameElement> {
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<Frame, HTMLIFrameElement> {
this.renderStyles({ prev: [] });
const colorWarn = '#ffca6f';
const rootType = this.getRootType();
append(
body,
@ -358,7 +379,7 @@ export default class FrameView extends ModuleView<Frame, HTMLIFrameElement> {
${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<Frame, HTMLIFrameElement> {
</style>`,
);
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<Frame, HTMLIFrameElement> {
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();

34
packages/core/src/commands/view/SelectComponent.ts

@ -12,6 +12,14 @@ import CommandAbstract from './CommandAbstract';
let showOffsets: boolean;
export interface SelectComponentCommandOptions {
onClick?: (ev: Event) => boolean | void;
}
export interface SelectComponentCommandStopOptions {
preserveSelected?: boolean;
}
export interface SelectComponentCommandRegistryRun {
'core:component-select': CommandPublicFnFromHandler<CommandSelectComponent['run']>;
'select-comp': CommandPublicFnFromHandler<CommandSelectComponent['run']>;
@ -41,9 +49,13 @@ export interface SelectComponentCommandRegistryStop {
* so those elements are inside the Local Tools box
*
*/
export default class CommandSelectComponent extends CommandAbstract {
export default class CommandSelectComponent extends CommandAbstract<
SelectComponentCommandOptions,
SelectComponentCommandStopOptions
> {
[key: string]: any;
activeResizer = false;
opts: SelectComponentCommandOptions = {};
init() {
this._upToolbar = debounce(() => {
@ -281,8 +293,16 @@ export default class CommandSelectComponent extends CommandAbstract {
}
onClick(ev: Event): void {
ev.stopPropagation();
ev.preventDefault();
const customHandler = this.opts.onClick;
if (customHandler) {
const handlerResult = customHandler(ev);
if (handlerResult !== true) return;
} else {
ev.stopPropagation();
ev.preventDefault();
}
const { em } = this;
if (em.get('_cmpDrag')) {
@ -597,15 +617,17 @@ export default class CommandSelectComponent extends CommandAbstract {
return this.canvas.getWindow();
}
run(editor: any) {
run(ed: any, s?: any, opts: SelectComponentCommandOptions = {}) {
if (!hasWin()) return;
this.editor = editor && editor.get('Editor');
this.opts = opts;
this.editor = this.em.Editor;
this.enable();
}
stop(ed?: any, sender?: any, opts: any = {}) {
stop(ed?: any, s?: any, opts: SelectComponentCommandStopOptions = {}) {
if (!hasWin()) return;
const { em, editor } = this;
this.opts = {};
this.onHovered();
this.stopSelectComponent();
!opts.preserveSelected && em.setSelected();

1
packages/core/src/dom_components/index.ts

@ -610,6 +610,7 @@ export default class ComponentManager extends ItemManagerModule<DomComponentsCon
...view,
...getExtendedObj(extendFnView, view, viewToExt),
});
// Object.setPrototypeOf(methods.view, viewToExt);
}
if (compType) {

13
packages/core/src/dom_components/model/Component.ts

@ -1025,7 +1025,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
* Returns component's classes as an array of strings
* @return {Array}
*/
getClasses() {
getClasses(): string[] {
const attr = this.getAttributes();
const classStr = attr.class;
return classStr ? classStr.split(' ') : [];
@ -1426,17 +1426,14 @@ export default class Component extends StyleableModel<ComponentProperties> {
* Override original clone method
* @private
* @ts-ignore */
clone(opt: { symbol?: boolean; symbolInv?: boolean } = {}): this {
clone(opt: { symbol?: boolean; symbolInv?: boolean; frame?: Frame } = {}): this {
const em = this.em;
const attr = this.dataResolverWatchers.getProps(this.attributes);
const opts = { ...this.opt };
const opts = { ...this.opt, frame: opt.frame ?? undefined };
const id = this.getId();
const cssc = em?.Css;
// @ts-ignore
attr.components = [];
// @ts-ignore
attr.classes = [];
// @ts-ignore
attr.traits = [];
if (isSymbolRoot(this)) {
@ -1448,11 +1445,9 @@ export default class Component extends StyleableModel<ComponentProperties> {
attr.components[i] = md.clone({ ...opt, _inner: 1 });
});
this.get('traits')!.each((md, i) => {
// @ts-ignore
attr.traits[i] = md.clone();
});
this.get('classes')!.each((md, i) => {
// @ts-ignore
attr.classes[i] = md.get('name');
});
@ -1520,7 +1515,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
* @param {Boolean} [opts.noCustom] Avoid custom name assigned to the component.
* @returns {String}
* */
getName(opts: { noCustom?: boolean } = {}) {
getName(opts: { noCustom?: boolean } = {}): string {
const { em } = this;
const { type, tagName, name } = this.attributes;
const defName = type || tagName;

19
packages/core/src/dom_components/model/Components.ts

@ -5,12 +5,14 @@ import { DomComponentsConfig } from '../config/config';
import EditorModel from '../../editor/model/Editor';
import ComponentManager from '..';
import CssRule from '../../css_composer/model/CssRule';
import type Frame from '../../canvas/model/Frame';
import {
ComponentAdd,
ComponentAddType,
ComponentDefinition,
ComponentDefinitionDefined,
ComponentOptions,
ComponentProperties,
} from './types';
import ComponentText from './ComponentText';
@ -113,10 +115,9 @@ const getComponentsFromDefs = (
});
};
export interface ComponentsOptions {
em: EditorModel;
config?: DomComponentsConfig;
export interface ComponentsOptions extends Omit<ComponentOptions, 'config'> {
domc?: ComponentManager;
config?: DomComponentsConfig;
}
interface AddComponentOptions extends AddOptions {
@ -402,10 +403,20 @@ Component> {
return model;
}
updateFrameRefs(model: Component, frame?: Frame) {
if (!frame || model.frame === frame) return;
model.opt.frame = frame;
model.components().forEach((child) => this.updateFrameRefs(child, frame));
}
onAdd(model: Component, c?: any, opts: { temporary?: boolean } = {}) {
const { domc, em } = this;
const avoidInline = em.config.avoidInlineStyle;
const allById = domc?.allById();
const frame = this.parent?.frame || this.opt.frame;
const skipAddEvent = !!this.parent && !frame;
this.updateFrameRefs(model, frame);
allById?.[model.getId()] !== model && domc?.Component.ensureInList(model);
if (!avoidInline && em.config.forceClass && !opts.temporary) {
@ -421,7 +432,7 @@ Component> {
model.__postAdd({ recursive: true });
if (em && !opts.temporary) {
if (em && !opts.temporary && !skipAddEvent) {
const triggerAdd = (model: Component) => {
em.trigger(ComponentsEvents.add, model, opts);
model.components().forEach((comp) => triggerAdd(comp));

20
packages/core/src/editor/model/Editor.ts

@ -977,10 +977,14 @@ export default class EditorModel extends Model {
* @private
*/
runDefault(opts = {}) {
const command = this.Commands.get(this.config.defaultCommand!);
const { Commands, config } = this;
const defCmd = config.defaultCommand!;
const command = Commands.get(defCmd);
if (!command || this.defaultRunning) return;
command.stop!(this as any, this, opts);
command.run!(this as any, this, opts);
Commands.stop(defCmd, opts);
Commands.run(defCmd, opts);
this.defaultRunning = true;
}
@ -990,11 +994,13 @@ export default class EditorModel extends Model {
* @private
*/
stopDefault(opts = {}) {
const commands = this.Commands;
if (!commands) return;
const command = commands.get(this.config.defaultCommand!);
const { Commands, config } = this;
const defCmd = config.defaultCommand!;
const command = Commands?.get(defCmd);
if (!command || !this.defaultRunning) return;
command.stop!(this as any, this, opts);
Commands.stop(defCmd, opts);
this.defaultRunning = false;
}

2
packages/core/src/pages/index.ts

@ -276,7 +276,7 @@ export default class PageManager extends ItemManagerModule<PageManagerConfig, Pa
}
store() {
return this.getProjectData();
return this.getProjectData(this.getAll().filter((page) => !page.get('skipFromStorage')));
}
load(data: any) {

31
packages/core/src/pages/model/Page.ts

@ -1,11 +1,13 @@
import { forEach, result } from 'underscore';
import { PageManagerConfig } from '../types';
import type { FrameProperties } from '../../canvas/model/Frame';
import Frames from '../../canvas/model/Frames';
import { Model } from '../../common';
import ComponentWrapper from '../../dom_components/model/ComponentWrapper';
import EditorModel from '../../editor/model/Editor';
import { CssRuleJSON } from '../../css_composer/model/CssRule';
import { ComponentDefinition } from '../../dom_components/model/types';
import Frame from '../../canvas/model/Frame';
/** @private */
export interface PageProperties {
@ -29,10 +31,20 @@ export interface PageProperties {
*/
styles?: string | CssRuleJSON[];
/**
* Frames to load with the page.
*/
frames?: FrameProperties[];
/**
* Skip page from project storage.
*/
skipFromStorage?: boolean;
[key: string]: unknown;
}
export interface PagePropertiesDefined extends Pick<PageProperties, 'id' | 'name'> {
export interface PagePropertiesDefined extends Pick<PageProperties, 'id' | 'name' | 'skipFromStorage'> {
frames: Frames;
[key: string]: unknown;
}
@ -47,17 +59,20 @@ export default class Page extends Model<PagePropertiesDefined> {
}
em: EditorModel;
constructor(props: any, opts: { em?: EditorModel; config?: PageManagerConfig } = {}) {
super(props, opts);
constructor(props: PageProperties, opts: { em?: EditorModel; config?: PageManagerConfig } = {}) {
super(props as any, opts);
const { em } = opts;
const defFrame: any = {};
const defFrame: FrameProperties = {};
this.em = em!;
if (!props.frames) {
defFrame.component = props.component;
defFrame.styles = props.styles;
['component', 'styles'].map((i) => this.unset(i));
}
const frms: any[] = props.frames || [defFrame];
const frms: FrameProperties[] = props.frames || [defFrame];
frms.forEach((frame) => {
frame.page = this;
});
const frames = new Frames(em!.Canvas, frms);
frames.page = this;
this.set('frames', frames);
@ -105,7 +120,7 @@ export default class Page extends Model<PagePropertiesDefined> {
* @example
* const arrayOfFrames = page.getAllFrames();
*/
getAllFrames() {
getAllFrames(): Frame[] {
return this.getFrames().models || [];
}
@ -115,7 +130,7 @@ export default class Page extends Model<PagePropertiesDefined> {
* @example
* const mainFrame = page.getMainFrame();
*/
getMainFrame() {
getMainFrame(): Frame {
return this.getFrames().at(0);
}
@ -135,6 +150,8 @@ export default class Page extends Model<PagePropertiesDefined> {
const obj = Model.prototype.toJSON.call(this, opts);
const defaults = result(this, 'defaults');
delete obj.skipFromStorage;
// Remove private keys
forEach(obj, (value, key) => {
key.indexOf('_') === 0 && delete obj[key];

13
packages/core/test/specs/dom_components/model/Component.ts

@ -471,6 +471,19 @@ describe('Component', () => {
expect(result[0].em).toEqual(em);
});
test('append() assigns the destination frame to added component trees', () => {
const wrapper = em.getWrapper()!;
const frame = em.Pages.getMain().getMainFrame();
const added = wrapper.append({
tagName: 'section',
components: [{ tagName: 'span', content: 'Child' }],
})[0];
const child = added.components().at(0);
expect(added.frame).toBe(frame);
expect(child?.frame).toBe(frame);
});
test('components() set new collection', () => {
obj.append([{}, {}]);
obj.components('<span>test</div>');

238
packages/core/test/specs/pages/index.ts

@ -281,6 +281,141 @@ describe('Managing pages', () => {
expect(rule2.getSelectorsString()).toBe(idSel2);
expect(rule2.getStyle()).toEqual({ color: 'blue' });
});
test('Skip pages from project storage while keeping runtime models', () => {
const storedPage = pm.add({
id: 'stored-page',
component: '<div>Stored page</div>',
})!;
const skippedPage = pm.add({
id: 'skipped-page',
skipFromStorage: true,
frames: [
{
id: 'skipped-page-frame',
component: '<div>Skipped page frame</div>',
},
],
})!;
const storedPage2 = pm.add({
id: 'stored-page-2',
component: '<div>Stored page 2</div>',
})!;
expect(pm.getAll().map((page) => page.getId())).toEqual([
pm.getMain().getId(),
storedPage.getId(),
skippedPage.getId(),
storedPage2.getId(),
]);
expect(skippedPage.getFrames().length).toBe(1);
const storedPages = editor.getProjectData().pages;
expect(storedPages.map((page: any) => page.id)).toEqual([
pm.getMain().getId(),
storedPage.getId(),
storedPage2.getId(),
]);
expect(storedPages.find((page: any) => page.id === skippedPage.getId())).toBeUndefined();
expect(storedPages.every((page: any) => !('skipFromStorage' in page))).toBe(true);
});
test('Skip frames from project storage while keeping runtime models', () => {
const page = pm.add({
id: 'frames-page',
frames: [
{
id: 'frame-1',
component: '<div>Frame 1</div>',
},
{
id: 'frame-2',
component: '<div>Frame 2</div>',
skipFromStorage: true,
},
{
id: 'frame-3',
component: '<div>Frame 3</div>',
},
],
})!;
expect(page.getFrames().map((frame) => frame.id)).toEqual(['frame-1', 'frame-2', 'frame-3']);
const storedPage = editor.getProjectData().pages.find((item: any) => item.id === page.getId());
expect(storedPage.frames.map((frame: any) => frame.id)).toEqual(['frame-1', 'frame-3']);
expect(storedPage.frames.every((frame: any) => !('skipFromStorage' in frame))).toBe(true);
expect(storedPage.frames[0].component).toBeTruthy();
expect(storedPage.frames[1].component).toBeTruthy();
});
test('Load project data with skipped pages and frames but omit them on the next store', () => {
editor.loadProjectData({
assets: [],
pages: [
{
id: 'page-stored',
frames: [
{
id: 'frame-stored',
component: '<div>Stored frame</div>',
},
{
id: 'frame-skipped',
component: '<div>Skipped frame</div>',
skipFromStorage: true,
},
],
},
{
id: 'page-skipped',
skipFromStorage: true,
frames: [
{
id: 'frame-on-skipped-page',
component: '<div>Skipped page frame</div>',
},
],
},
],
styles: [],
} 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']);
const storedPages = editor.getProjectData().pages;
expect(storedPages.map((page: any) => page.id)).toEqual(['page-stored']);
expect(storedPages[0].frames.map((frame: any) => frame.id)).toEqual(['frame-stored']);
});
test('Do not leak skipFromStorage in direct page/frame serialization', () => {
const page = pm.add({
id: 'serialized-page',
skipFromStorage: false,
frames: [
{
id: 'serialized-frame',
component: '<div>Serialized frame</div>',
skipFromStorage: false,
},
],
})!;
expect(page.toJSON()).not.toHaveProperty('skipFromStorage');
expect(page.getMainFrame().toJSON()).not.toHaveProperty('skipFromStorage');
});
});
describe('Pages in canvas', () => {
@ -349,4 +484,107 @@ 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('Moving a component tree across page frames updates its frame reference', () => {
const mainFrame = pm.getMain().getMainFrame();
const mainWrapper = mainFrame.getComponent();
const page = pm.add({
id: 'frame-target-page',
component: [],
})!;
const targetFrame = page.getMainFrame();
const targetWrapper = targetFrame.getComponent();
const target = mainWrapper.append({
tagName: 'section',
components: [{ tagName: 'span', content: 'Inner child' }],
})[0];
const child = target.components().at(0);
expect(target.frame).toBe(mainFrame);
expect(child?.frame).toBe(mainFrame);
targetWrapper.append(target);
expect(target.frame).toBe(targetFrame);
expect(child?.frame).toBe(targetFrame);
expect(target.parent()).toBe(targetWrapper);
});
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');
});
});

Loading…
Cancel
Save