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>
<script type="text/javascript">
var editor = grapesjs.init({
showOffsets: 1,
noticeOnUnload: 0,
container: '#gjs',
height: '100%',
fromElement: true,
storageManager: { autoload: 0 },
styleManager: {
sectors: [
const editortest = grapesjs.init({
headless: true,
projectData: {
pages: [
{
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',
frames: [
{
component: {
type: 'wrapper',
stylable: [
'background',
'background-color',
'background-image',
'background-repeat',
'background-attachment',
'background-position',
'background-size',
],
attributes: {
id: 'body',
},
components: [
{
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',
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',
frames: [
{
component: {
type: 'wrapper',
stylable: [
'background',
'background-color',
'background-image',
'background-repeat',
'background-attachment',
'background-position',
'background-size',
],
attributes: {
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',
},
],
},
],
},
],
},
},
],
},
{
name: 'Decorations',
open: false,
buildProps: [
'border-radius-c',
'background-color',
'border-radius',
'border',
'box-shadow',
'background',
],
},
{
name: 'Extra',
open: false,
buildProps: ['transition', 'perspective', 'transform'],
id: '1zKkQnGVzy8nKXKE',
},
],
},
});
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>`,
let pages = editortest.Pages.getAll().map((page) => {
const component = page.getMainComponent();
return editortest.getHtml({ component });
});
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>
</body>
</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);
this.setAttributes(attrs);
this.ccid = Component.createId(this, opt);
this.ccid = Component.createId(this, opt as any);
this.preInit();
this.initClasses();
this.initComponents();
@ -1644,31 +1644,39 @@ export default class Component extends StyleableModel<ComponentProperties> {
*/
toJSON(opts: ObjectAny = {}): ComponentDefinition {
let obj = super.toJSON(opts, { attributes: this.getAttributes() });
delete obj.dataResolverWatchers;
delete obj.attributes.class;
delete obj.toolbar;
delete obj.traits;
delete obj.status;
delete obj.open; // used in Layers
delete obj._undoexc;
delete obj.delegate;
delete (obj as any).dataResolverWatchers;
delete (obj as any).attributes.class;
delete (obj as any).toolbar;
delete (obj as any).traits;
delete (obj as any).status;
delete (obj as any).open;
delete (obj as any)._undoexc;
delete (obj as any).delegate;
if (this.collectionsStateMap && Object.getOwnPropertyNames(this.collectionsStateMap).length > 0) {
delete obj[keySymbol];
delete obj[keySymbolOvrd];
delete obj[keySymbols];
delete (obj as any)[keySymbol];
delete (obj as any)[keySymbolOvrd];
delete (obj as any)[keySymbols];
}
if (!opts.fromUndo) {
const symbol = obj[keySymbol];
const symbols = obj[keySymbols];
const symbol = (obj as any)[keySymbol];
const symbols = (obj as any)[keySymbols];
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)) {
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) {
this.getChangedProps(obj);
}
@ -2066,33 +2074,88 @@ export default class Component extends StyleableModel<ComponentProperties> {
const current = list[id];
if (!current) {
// Insert in list
// Первое появление такого id в трекере
list[id] = model;
} else if (current !== model) {
// Create new ID
const nextId = Component.getIncrementId(id, list);
model.setId(nextId);
list[nextId] = model;
const currentPage = current.page;
const modelPage = model.page;
const samePage = !!currentPage && !!modelPage && currentPage === modelPage;
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));
}
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 { idMap = {} } = opts;
let { id } = model.get('attributes')!;
let nextId;
if (id) {
nextId = Component.getIncrementId(id, list, opts);
model.setId(nextId);
if (id !== nextId) idMap[id] = nextId;
const attrs = model.get('attributes') || {};
const attrId = attrs.id as string | undefined;
let nextId: string;
if (attrId) {
const existing = list[attrId] as Component | undefined;
// Первый раз видим такой 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 {
// Без attributes.id — как раньше
nextId = Component.getNewId(list);
list[nextId] = model;
}
list[nextId] = model;
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.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'),
type: cmp.getType(),
attributes: cmp.getAttributes(),
});
const json = JSON.parse(JSON.stringify(cmp.toJSON()));
expect(json).toEqual(
expect.objectContaining({
tagName,
dataResolver: cmp.get('dataResolver'),
type: cmp.getType(),
attributes: cmp.getAttributes(),
}),
);
};
const checkRecordsWithInnerCmp = () => {

Loading…
Cancel
Save