Browse Source

Fix ID conflict between pages in GrapesJS

pull/6660/head
Kaleniuk 2 months ago
parent
commit
03c1fb8e56
  1. 242
      packages/core/index.html
  2. 123
      packages/core/src/dom_components/model/Component.ts
  3. 16
      packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts

242
packages/core/index.html

@ -81,80 +81,196 @@
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
var editor = grapesjs.init({ const editortest = grapesjs.init({
showOffsets: 1, headless: true,
noticeOnUnload: 0, projectData: {
container: '#gjs', pages: [
height: '100%',
fromElement: true,
storageManager: { autoload: 0 },
styleManager: {
sectors: [
{ {
name: 'General', frames: [
open: false, {
buildProps: ['float', 'display', 'position', 'top', 'right', 'left', 'bottom'], component: {
}, type: 'wrapper',
{ stylable: [
name: 'Flex', 'background',
open: false, 'background-color',
buildProps: [ 'background-image',
'flex-direction', 'background-repeat',
'flex-wrap', 'background-attachment',
'justify-content', 'background-position',
'align-items', 'background-size',
'align-content', ],
'order', attributes: {
'flex-basis', id: 'body',
'flex-grow', },
'flex-shrink', components: [
'align-self', {
tagName: 'section',
components: [
{
tagName: 'h1',
type: 'text',
attributes: {
id: 'main-title',
},
components: [
{
type: 'textnode',
content: 'This is a simple title',
},
],
},
{
type: 'text',
components: [
{
type: 'textnode',
content: 'This is just a Lorem text: Lorem ipsum dolor sit amet',
},
],
},
],
},
],
},
},
], ],
id: 'ljc0blhC1ZeiCYyY',
}, },
{ {
name: 'Dimension', frames: [
open: false, {
buildProps: ['width', 'height', 'max-width', 'min-height', 'margin', 'padding'], component: {
}, type: 'wrapper',
{ stylable: [
name: 'Typography', 'background',
open: false, 'background-color',
buildProps: [ 'background-image',
'font-family', 'background-repeat',
'font-size', 'background-attachment',
'font-weight', 'background-position',
'letter-spacing', 'background-size',
'color', ],
'line-height', attributes: {
'text-shadow', id: 'body',
},
components: [
{
tagName: 'section',
components: [
{
tagName: 'h1',
type: 'text',
attributes: {
id: 'main-title',
},
components: [
{
type: 'textnode',
content: 'This is another title',
},
],
},
{
type: 'text',
components: [
{
type: 'textnode',
content: 'Some other text',
},
],
},
],
},
],
},
},
], ],
}, id: '1zKkQnGVzy8nKXKE',
{
name: 'Decorations',
open: false,
buildProps: [
'border-radius-c',
'background-color',
'border-radius',
'border',
'box-shadow',
'background',
],
},
{
name: 'Extra',
open: false,
buildProps: ['transition', 'perspective', 'transform'],
}, },
], ],
}, },
}); });
editor.BlockManager.add('testBlock', { let pages = editortest.Pages.getAll().map((page) => {
label: 'Block', const component = page.getMainComponent();
attributes: { class: 'gjs-fonts gjs-f-b1' }, return editortest.getHtml({ component });
content: `<div style="padding-top:50px; padding-bottom:50px; text-align:center">Test block</div>`,
}); });
console.log(pages);
// var editor = grapesjs.init({
// showOffsets: 1,
// noticeOnUnload: 0,
// container: '#gjs',
// height: '100%',
// fromElement: true,
// storageManager: { autoload: 0 },
// styleManager: {
// sectors: [
// {
// name: 'General',
// open: false,
// buildProps: ['float', 'display', 'position', 'top', 'right', 'left', 'bottom'],
// },
// {
// name: 'Flex',
// open: false,
// buildProps: [
// 'flex-direction',
// 'flex-wrap',
// 'justify-content',
// 'align-items',
// 'align-content',
// 'order',
// 'flex-basis',
// 'flex-grow',
// 'flex-shrink',
// 'align-self',
// ],
// },
// {
// name: 'Dimension',
// open: false,
// buildProps: ['width', 'height', 'max-width', 'min-height', 'margin', 'padding'],
// },
// {
// name: 'Typography',
// open: false,
// buildProps: [
// 'font-family',
// 'font-size',
// 'font-weight',
// 'letter-spacing',
// 'color',
// 'line-height',
// 'text-shadow',
// ],
// },
// {
// name: 'Decorations',
// open: false,
// buildProps: [
// 'border-radius-c',
// 'background-color',
// 'border-radius',
// 'border',
// 'box-shadow',
// 'background',
// ],
// },
// {
// name: 'Extra',
// open: false,
// buildProps: ['transition', 'perspective', 'transform'],
// },
// ],
// },
// });
// editor.BlockManager.add('testBlock', {
// label: 'Block',
// attributes: { class: 'gjs-fonts gjs-f-b1' },
// content: `<div style="padding-top:50px; padding-bottom:50px; text-align:center">Test block</div>`,
// });
</script> </script>
</body> </body>
</html> </html>

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

@ -316,7 +316,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
}; };
const attrs = this.dataResolverWatchers.getValueOrResolver('attributes', defaultAttrs); const attrs = this.dataResolverWatchers.getValueOrResolver('attributes', defaultAttrs);
this.setAttributes(attrs); this.setAttributes(attrs);
this.ccid = Component.createId(this, opt); this.ccid = Component.createId(this, opt as any);
this.preInit(); this.preInit();
this.initClasses(); this.initClasses();
this.initComponents(); this.initComponents();
@ -1644,31 +1644,39 @@ export default class Component extends StyleableModel<ComponentProperties> {
*/ */
toJSON(opts: ObjectAny = {}): ComponentDefinition { toJSON(opts: ObjectAny = {}): ComponentDefinition {
let obj = super.toJSON(opts, { attributes: this.getAttributes() }); let obj = super.toJSON(opts, { attributes: this.getAttributes() });
delete obj.dataResolverWatchers; delete (obj as any).dataResolverWatchers;
delete obj.attributes.class; delete (obj as any).attributes.class;
delete obj.toolbar; delete (obj as any).toolbar;
delete obj.traits; delete (obj as any).traits;
delete obj.status; delete (obj as any).status;
delete obj.open; // used in Layers delete (obj as any).open;
delete obj._undoexc; delete (obj as any)._undoexc;
delete obj.delegate; delete (obj as any).delegate;
if (this.collectionsStateMap && Object.getOwnPropertyNames(this.collectionsStateMap).length > 0) { if (this.collectionsStateMap && Object.getOwnPropertyNames(this.collectionsStateMap).length > 0) {
delete obj[keySymbol]; delete (obj as any)[keySymbol];
delete obj[keySymbolOvrd]; delete (obj as any)[keySymbolOvrd];
delete obj[keySymbols]; delete (obj as any)[keySymbols];
} }
if (!opts.fromUndo) { if (!opts.fromUndo) {
const symbol = obj[keySymbol]; const symbol = (obj as any)[keySymbol];
const symbols = obj[keySymbols]; const symbols = (obj as any)[keySymbols];
if (symbols && isArray(symbols)) { if (symbols && isArray(symbols)) {
obj[keySymbols] = symbols.filter((i) => i).map((i) => (i.getId ? i.getId() : i)); (obj as any)[keySymbols] = symbols.filter((i: any) => i).map((i: any) => (i.getId ? i.getId() : i));
} }
if (symbol && !isString(symbol)) { if (symbol && !isString(symbol)) {
obj[keySymbol] = symbol.getId(); (obj as any)[keySymbol] = symbol.getId();
} }
} }
// ★ Добавляем "реальный" component id,
// если он отличается от attributes.id
const attrs = this.get('attributes') || {};
if (this.ccid && attrs.id && this.ccid !== attrs.id) {
(obj as any).id = this.ccid;
}
if (this.em.getConfig().avoidDefaults) { if (this.em.getConfig().avoidDefaults) {
this.getChangedProps(obj); this.getChangedProps(obj);
} }
@ -2066,33 +2074,88 @@ export default class Component extends StyleableModel<ComponentProperties> {
const current = list[id]; const current = list[id];
if (!current) { if (!current) {
// Insert in list // Первое появление такого id в трекере
list[id] = model; list[id] = model;
} else if (current !== model) { } else if (current !== model) {
// Create new ID const currentPage = current.page;
const nextId = Component.getIncrementId(id, list); const modelPage = model.page;
model.setId(nextId); const samePage = !!currentPage && !!modelPage && currentPage === modelPage;
list[nextId] = model;
if (samePage) {
// ★ Та же страница: старое поведение
const nextId = Component.getIncrementId(id, list);
model.setId(nextId);
list[nextId] = model;
} else {
// ★ Другая страница:
// - attributes.id НЕ меняем
// - убеждаемся, что внутренний ccid уникален в трекере
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.components().forEach((i) => Component.ensureInList(i)); model.components().forEach((i) => Component.ensureInList(i));
} }
static createId(model: Component, opts: any = {}) { static createId(
model: Component,
opts: {
keepIds?: string[];
idMap?: PrevToNewIdMap;
updatedIds?: Record<string, ComponentDefinitionDefined[]>;
} = {},
) {
const list = Component.getList(model); const list = Component.getList(model);
const { idMap = {} } = opts; const { idMap = {} } = opts;
let { id } = model.get('attributes')!; const attrs = model.get('attributes') || {};
let nextId; const attrId = attrs.id as string | undefined;
let nextId: string;
if (id) {
nextId = Component.getIncrementId(id, list, opts); if (attrId) {
model.setId(nextId); const existing = list[attrId] as Component | undefined;
if (id !== nextId) idMap[id] = nextId;
// Первый раз видим такой id — сохраняем как есть
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;
if (samePage) {
// ★ Та же страница: старое поведение — меняем attributes.id
nextId = Component.getIncrementId(attrId, list, opts);
model.setId(nextId);
if (attrId !== nextId) {
idMap[attrId] = nextId;
}
list[nextId] = model;
} else {
// ★ Другая страница:
// - attributes.id оставляем attrId
// - создаём только внутренний ID для трекинга
nextId = Component.getIncrementId(attrId, list, opts);
// не вызываем setId, чтобы не трогать attributes.id
list[nextId] = model;
}
}
} else { } else {
// Без attributes.id — как раньше
nextId = Component.getNewId(list); nextId = Component.getNewId(list);
list[nextId] = model;
} }
list[nextId] = model;
return nextId; return nextId;
} }

16
packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts

@ -104,12 +104,16 @@ describe('Collection component', () => {
expect(cmp.getInnerHTML()).toBe(innerHTML); expect(cmp.getInnerHTML()).toBe(innerHTML);
expect(cmp.toHTML()).toBe(`<${tagName} id="${cmp.getId()}">${innerHTML}</${tagName}>`); expect(cmp.toHTML()).toBe(`<${tagName} id="${cmp.getId()}">${innerHTML}</${tagName}>`);
expect(cmp.getEl()?.innerHTML).toBe(innerHTML); expect(cmp.getEl()?.innerHTML).toBe(innerHTML);
expect(JSON.parse(JSON.stringify(cmp.toJSON()))).toEqual({
tagName: cmp.tagName, const json = JSON.parse(JSON.stringify(cmp.toJSON()));
dataResolver: cmp.get('dataResolver'), expect(json).toEqual(
type: cmp.getType(), expect.objectContaining({
attributes: cmp.getAttributes(), tagName,
}); dataResolver: cmp.get('dataResolver'),
type: cmp.getType(),
attributes: cmp.getAttributes(),
}),
);
}; };
const checkRecordsWithInnerCmp = () => { const checkRecordsWithInnerCmp = () => {

Loading…
Cancel
Save