diff --git a/packages/core/src/abstract/Module.ts b/packages/core/src/abstract/Module.ts index 8889ea4331..e82533449c 100644 --- a/packages/core/src/abstract/Module.ts +++ b/packages/core/src/abstract/Module.ts @@ -130,6 +130,7 @@ export abstract class ItemManagerModule< all: TCollection; view?: View; events!: Record; + protected _itemCache = new Map(); constructor( em: EditorModel, @@ -207,6 +208,64 @@ export abstract class ItemManagerModule< }, {} as any); } + protected _makeCacheKey(selectors: any, state?: string, width?: string) { + const sels = Array.isArray(selectors) + ? selectors.map((s) => (typeof s === 'string' ? s : s.toString())).join(',') + : typeof selectors === 'string' + ? selectors + : selectors?.toString() || ''; + return `${sels}|${state || ''}|${width || ''}`; + } + + protected _cacheItem(item: Model) { + const key = this._makeCacheKey(item); + this._itemCache.set(key, item); + } + + protected _uncacheItem(item: Model) { + const key = this._makeCacheKey(item); + this._itemCache.delete(key); + } + + protected _clearItemCache() { + this._itemCache.clear(); + } + + protected _onItemAdd(item: Model) { + this._cacheItem(item); + } + + protected _onItemRemove(item: Model) { + this._uncacheItem(item); + } + + protected _onItemsReset(collection: Collection) { + this._clearItemCache(); + collection.each((item: Model) => this._cacheItem(item)); + } + + protected _onItemKeyChange(item: Model) { + let oldKey: string | undefined; + for (const [key, cachedItem] of (this._itemCache as any).entries()) { + if (cachedItem === item) { + oldKey = key; + break; + } + } + + if (oldKey) { + this._itemCache.delete(oldKey); + } + + this._cacheItem(item); + } + + protected _setupListeners() { + this.em.listenTo(this.all, 'add', this._onItemAdd.bind(this)); + this.em.listenTo(this.all, 'remove', this._onItemRemove.bind(this)); + this.em.listenTo(this.all, 'reset', this._onItemsReset.bind(this)); + } + __initListen(opts: any = {}) { const { all, em, events } = this; all && diff --git a/packages/core/src/css_composer/index.ts b/packages/core/src/css_composer/index.ts index 32b1581793..4063c6aa92 100644 --- a/packages/core/src/css_composer/index.ts +++ b/packages/core/src/css_composer/index.ts @@ -87,7 +87,7 @@ export default class CssComposer extends ItemManagerModule(); /** * Initializes module. Automatically called with a new instance of the editor * @param {Object} config Configurations @@ -104,6 +104,12 @@ export default class CssComposer extends ItemManagerModule, ): CssRule | undefined { + const key = this._makeCacheKey(selectors, state, width); + const cached = this._ruleCache.get(key); + if (cached) return cached; + let slc = selectors; if (isString(selectors)) { const sm = this.em.Selectors; @@ -212,7 +223,10 @@ export default class CssComposer extends ItemManagerModule rule.compare(slc, state, width, ruleProps)) || null; + + const rule = this.rules.find((r) => r.compare(slc, state, width, ruleProps)) || null; + if (rule) this._cacheItem(rule); + return rule; } getAll() { @@ -485,8 +499,9 @@ export default class CssComposer extends ItemManagerModule { * cssRule.getAtRule(); // "@media (min-width: 500px)" */ getAtRule() { - const type = this.get('atRuleType'); - const condition = this.get('mediaText'); - // Avoid breaks with the last condition + return CssRule.getAtRuleFromProps(this.attributes); + } + + static getAtRuleFromProps(cssRuleProps: Partial) { + const type = cssRuleProps.atRuleType; + const condition = cssRuleProps.mediaText; const typeStr = type ? `@${type}` : condition ? '@media' : ''; return typeStr + (condition && typeStr ? ` ${condition}` : ''); diff --git a/packages/core/src/selector_manager/index.ts b/packages/core/src/selector_manager/index.ts index 129cdfec81..d6c01cc440 100644 --- a/packages/core/src/selector_manager/index.ts +++ b/packages/core/src/selector_manager/index.ts @@ -128,7 +128,6 @@ export default class SelectorManager extends ItemManagerModule( @@ -152,16 +150,34 @@ export default class SelectorManager extends ItemManagerModule this.__trgCustom(), 0); + + this._setupListeners(); + this._onItemsReset(this.all); this.__initListen({ collections: [this.states, this.selected], propagate: [{ entity: this.states, event: this.events.state }], }); - em.on('change:state', (m, value) => em.trigger(evState, value)); - this.model.on('change:cFirst', (m, value) => em.trigger('selector:type', value)); + + const { em: editor } = this; + editor.on('change:state', (m, value) => editor.trigger(evState, value)); + this.model.on('change:cFirst', (m, value) => editor.trigger('selector:type', value)); const eventCmpUpdateCls = `${ComponentsEvents.update}:classes`; - em.on(`component:toggled ${eventCmpUpdateCls}`, this.__updateSelectedByComponents); + editor.on(`component:toggled ${eventCmpUpdateCls}`, this.__updateSelectedByComponents); const listenTo = `component:toggled ${eventCmpUpdateCls} change:device styleManager:update selector:state selector:type style:target`; - this.model.listenTo(em, listenTo, () => this.__update()); + this.model.listenTo(editor, listenTo, () => this.__update()); + } + + protected override _setupListeners() { + super._setupListeners(); + this.em.listenTo(this.all, 'change:name change:type', this._onItemKeyChange.bind(this)); + } + + protected override _makeCacheKey(selector: Selector): string { + return this.getCacheKey(selector.get('name')!, selector.get('type')!); + } + + private getCacheKey(name: string, type: number) { + return `${type}__${name}`; } __trgCustom(opts?: any) { @@ -233,6 +249,7 @@ export default class SelectorManager extends ItemManagerModule