diff --git a/packages/core/src/dom_components/config/config.ts b/packages/core/src/dom_components/config/config.ts
index a7bff2d54..405df657e 100644
--- a/packages/core/src/dom_components/config/config.ts
+++ b/packages/core/src/dom_components/config/config.ts
@@ -60,6 +60,14 @@ export interface DomComponentsConfig {
* work properly (eg. Web Components).
*/
useFrameDoc?: boolean;
+
+ /**
+ * Experimental!
+ * By default, the editor ensures unique attribute IDs for all components inside the project, no matter of the page.
+ * With this option enabled, the editor will keep same attributes IDs for components across different pages.
+ * This allows multiple components cross pages (eg. ) to share the same ID (eg. to keep the same styling).
+ */
+ keepAttributeIdsCrossPages?: boolean;
}
const config: () => DomComponentsConfig = () => ({
diff --git a/packages/core/src/dom_components/model/Component.ts b/packages/core/src/dom_components/model/Component.ts
index 61306246f..5261ac01c 100644
--- a/packages/core/src/dom_components/model/Component.ts
+++ b/packages/core/src/dom_components/model/Component.ts
@@ -72,6 +72,11 @@ import {
export interface IComponent extends ExtractMethods {}
export interface SetAttrOptions extends SetOptions, UpdateStyleOptions, DataWatchersOptions {}
export interface ComponentSetOptions extends SetOptions, DataWatchersOptions {}
+export interface CheckIdOptions {
+ keepIds?: string[];
+ idMap?: PrevToNewIdMap;
+ updatedIds?: Record;
+}
const escapeRegExp = (str: string) => {
return str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&');
@@ -2062,81 +2067,72 @@ export default class Component extends StyleableModel {
static ensureInList(model: Component) {
const list = Component.getList(model);
- const id = model.getId();
+ const propId = model.id as string | undefined;
+ const id = propId || model.getId();
const current = list[id];
if (!current) {
list[id] = model;
} else if (current !== model) {
+ const keepIdsCrossPages = model.em?.Components.config.keepAttributeIdsCrossPages;
const currentPage = current.page;
const modelPage = model.page;
const samePage = !!currentPage && !!modelPage && currentPage === modelPage;
+ const nextId = Component.getIncrementId(id, list);
- if (samePage) {
- const nextId = Component.getIncrementId(id, list);
+ if (samePage || !keepIdsCrossPages) {
model.setId(nextId);
- list[nextId] = model;
} else {
- const baseId = (model as any).ccid || id;
- let nextId = baseId;
-
- while (list[nextId] && list[nextId] !== model) {
- nextId = Component.getIncrementId(nextId, list);
- }
-
- (model as any).ccid = nextId;
- list[nextId] = model;
+ model.set({ id: nextId });
}
+
+ list[nextId] = model;
}
model.components().forEach((i) => Component.ensureInList(i));
}
- static createId(
- model: Component,
- opts: {
- keepIds?: string[];
- idMap?: PrevToNewIdMap;
- updatedIds?: Record;
- } = {},
- ) {
+ static createId(model: Component, opts: CheckIdOptions = {}) {
const list = Component.getList(model);
+ const keepIdsCrossPages = model.em?.Components.config.keepAttributeIdsCrossPages;
const { idMap = {} } = opts;
const attrs = model.get('attributes') || {};
const attrId = attrs.id as string | undefined;
+ const propId = model.id as string | undefined;
+ const currentId = propId ?? attrId;
let nextId: string;
- if (attrId) {
+ if (propId) {
+ nextId = Component.getIncrementId(propId, list, opts);
+ if (nextId !== propId) {
+ model.set({ id: nextId });
+ }
+ } else if (attrId) {
const existing = list[attrId] as Component | undefined;
if (!existing || existing === model) {
nextId = attrId;
- if (!list[nextId]) {
- list[nextId] = model;
- }
} else {
const existingPage = existing.page;
const newPage = model.page;
const samePage = !!existingPage && !!newPage && existingPage === newPage;
+ nextId = Component.getIncrementId(attrId, list, opts);
- if (samePage) {
- nextId = Component.getIncrementId(attrId, list, opts);
+ if (samePage || !keepIdsCrossPages) {
model.setId(nextId);
- if (attrId !== nextId) {
- idMap[attrId] = nextId;
- }
- list[nextId] = model;
} else {
- nextId = Component.getIncrementId(attrId, list, opts);
-
- list[nextId] = model;
+ model.set({ id: nextId });
}
}
} else {
nextId = Component.getNewId(list);
- list[nextId] = model;
}
+ if (!!currentId && currentId !== nextId) {
+ idMap[currentId] = nextId;
+ }
+
+ list[nextId] = model;
return nextId;
}
@@ -2169,20 +2165,14 @@ export default class Component extends StyleableModel {
}
static getList(model: Component) {
- const { em } = model;
- const dm = em?.Components;
- return dm?.componentsById ?? {};
+ return model.em?.Components?.componentsById ?? {};
}
static checkId(
components: ComponentDefinitionDefined | ComponentDefinitionDefined[],
styles: CssRuleJSON[] = [],
list: ObjectAny = {},
- opts: {
- keepIds?: string[];
- idMap?: PrevToNewIdMap;
- updatedIds?: Record;
- } = {},
+ opts: CheckIdOptions = {},
) {
opts.updatedIds = opts.updatedIds || {};
const comps = isArray(components) ? components : [components];
diff --git a/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts b/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts
index d9a033291..5c2b85934 100644
--- a/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts
+++ b/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts
@@ -104,7 +104,6 @@ describe('Collection component', () => {
expect(cmp.getInnerHTML()).toBe(innerHTML);
expect(cmp.toHTML()).toBe(`<${tagName} id="${cmp.getId()}">${innerHTML}${tagName}>`);
expect(cmp.getEl()?.innerHTML).toBe(innerHTML);
-
expect(JSON.parse(JSON.stringify(cmp.toJSON()))).toEqual({
tagName: cmp.tagName,
dataResolver: cmp.get('dataResolver'),
diff --git a/packages/core/test/specs/pages/pages-same-id-on-different-pages.ts b/packages/core/test/specs/pages/pages-same-id-on-different-pages.ts
index 328b931f7..40cf12820 100644
--- a/packages/core/test/specs/pages/pages-same-id-on-different-pages.ts
+++ b/packages/core/test/specs/pages/pages-same-id-on-different-pages.ts
@@ -1,111 +1,158 @@
import Editor from '../../../src/editor';
import EditorModel from '../../../src/editor/model/Editor';
import ComponentWrapper from '../../../src/dom_components/model/ComponentWrapper';
+import { setupTestEditor } from '../../common';
describe('Pages with same component ids across pages', () => {
let editor: Editor;
let em: EditorModel;
let pm: Editor['Pages'];
let domc: Editor['Components'];
- const getTitle = (wrapper: ComponentWrapper) => wrapper.components().at(0)!.components().at(0)!;
-
- beforeAll(() => {
- editor = new Editor({
- pageManager: {
- pages: [
- {
- id: 'p1',
- frames: [
- {
- component: {
- type: 'wrapper',
- attributes: { id: 'body' },
- components: [
- {
- tagName: 'section',
- components: [
- {
- tagName: 'h1',
- type: 'text',
- attributes: { id: 'main-title' },
- components: [{ type: 'textnode', content: 'A' }],
- },
- ],
- },
- ],
- },
- },
- ],
- },
- {
- id: 'p2',
- frames: [
- {
- component: {
- type: 'wrapper',
- attributes: { id: 'body' },
- components: [
- {
- tagName: 'section',
- components: [
- {
- tagName: 'h1',
- type: 'text',
- attributes: { id: 'main-title' },
- components: [{ type: 'textnode', content: 'B' }],
- },
- ],
- },
- ],
- },
- },
- ],
- },
- ],
+ const rootDefaultProps = {
+ type: 'wrapper',
+ head: { type: 'head' },
+ docEl: { tagName: 'html' },
+ stylable: [
+ 'background',
+ 'background-color',
+ 'background-image',
+ 'background-repeat',
+ 'background-attachment',
+ 'background-position',
+ 'background-size',
+ ],
+ };
+
+ const getTitle = (wrapper: ComponentWrapper) => wrapper.components().at(0);
+
+ const getRootComponent = ({ idBody = 'body', idTitle = 'main-title', contentTitle = 'A' } = {}) => ({
+ type: 'wrapper',
+ attributes: { id: idBody },
+ components: [
+ {
+ tagName: 'h1',
+ type: 'text',
+ attributes: { id: idTitle },
+ components: [{ type: 'textnode', content: contentTitle }],
},
- });
+ ],
+ });
+ beforeEach(() => {
+ ({ editor } = setupTestEditor());
em = editor.getModel();
pm = em.Pages;
domc = em.Components;
- pm.onLoad();
});
- afterAll(() => {
+ afterEach(() => {
editor.destroy();
});
- test('Handles pages with components having the same id across pages', () => {
- const pages = pm.getAll();
- expect(pages.length).toBe(2);
+ test('Default behavior with pages having components with same ids are incremented', () => {
+ editor.Pages.add({
+ id: 'page1',
+ frames: [{ component: getRootComponent() }],
+ });
+ editor.Pages.add({
+ id: 'page2',
+ frames: [{ component: getRootComponent({ contentTitle: 'B' }) }],
+ });
- const p1 = pages[0];
- const p2 = pages[1];
+ const root1 = pm.get('page1')!.getMainComponent();
+ const root2 = pm.get('page2')!.getMainComponent();
- const w1 = p1.getMainComponent();
- const w2 = p2.getMainComponent();
+ expect(editor.getHtml({ component: root1 })).toBe('A
');
+ expect(editor.getHtml({ component: root2 })).toBe('B
');
- expect(w1.getId()).toBe('body');
- expect(w2.getId()).toBe('body');
+ expect(JSON.parse(JSON.stringify(root1))).toEqual({
+ ...rootDefaultProps,
+ attributes: { id: 'body' },
+ components: [
+ {
+ tagName: 'h1',
+ type: 'text',
+ attributes: { id: 'main-title' },
+ components: [{ type: 'textnode', content: 'A' }],
+ },
+ ],
+ });
- const t1 = getTitle(w1);
- const t2 = getTitle(w2);
+ expect(JSON.parse(JSON.stringify(root2))).toEqual({
+ ...rootDefaultProps,
+ attributes: { id: 'body-2' },
+ components: [
+ {
+ tagName: 'h1',
+ type: 'text',
+ attributes: { id: 'main-title-2' },
+ components: [{ type: 'textnode', content: 'B' }],
+ },
+ ],
+ });
+ });
+
+ test('Handles pages with components having the same id across pages', () => {
+ editor.Components.config.keepAttributeIdsCrossPages = true;
+ editor.Pages.add({
+ id: 'page1',
+ frames: [{ component: getRootComponent() }],
+ });
+ editor.Pages.add({
+ id: 'page2',
+ frames: [{ component: getRootComponent({ contentTitle: 'B' }) }],
+ });
+ const page1 = pm.get('page1')!;
+ const page2 = pm.get('page2')!;
+ const root1 = page1.getMainComponent();
+ const root2 = page2.getMainComponent();
+
+ expect(root1.getId()).toBe('body');
+ expect(root2.getId()).toBe('body');
+
+ const title1 = getTitle(root1);
+ const title2 = getTitle(root2);
// IDs should be preserved per page but stored uniquely in the shared map
- expect(t1.getId()).toBe('main-title');
- expect(t2.getId()).toBe('main-title');
+ expect(title1.getId()).toBe('main-title');
+ expect(title2.getId()).toBe('main-title');
const all = domc.allById();
- expect(all['body']).toBe(w1);
- expect(all['body-2']).toBe(w2);
- expect(all['main-title']).toBe(t1);
- expect(all['main-title-2']).toBe(t2);
+ expect(all['body']).toBe(root1);
+ expect(all['body-2']).toBe(root2);
+ expect(all['main-title']).toBe(title1);
+ expect(all['main-title-2']).toBe(title2);
+
+ expect(editor.getHtml({ component: root1 })).toBe('A
');
+ expect(editor.getHtml({ component: root2 })).toBe('B
');
- const html1 = editor.getHtml({ component: w1 });
- const html2 = editor.getHtml({ component: w2 });
+ expect(JSON.parse(JSON.stringify(root1))).toEqual({
+ ...rootDefaultProps,
+ attributes: { id: 'body' },
+ components: [
+ {
+ tagName: 'h1',
+ type: 'text',
+ attributes: { id: 'main-title' },
+ components: [{ type: 'textnode', content: 'A' }],
+ },
+ ],
+ });
- expect(html1).toContain('A');
- expect(html2).toContain('B');
+ expect(JSON.parse(JSON.stringify(root2))).toEqual({
+ ...rootDefaultProps,
+ id: 'body-2',
+ attributes: { id: 'body' },
+ components: [
+ {
+ id: 'main-title-2',
+ tagName: 'h1',
+ type: 'text',
+ attributes: { id: 'main-title' },
+ components: [{ type: 'textnode', content: 'B' }],
+ },
+ ],
+ });
});
});