@elementor/editor-canvas 4.3.0-beta2 → 4.3.0-beta3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -196,6 +196,7 @@ type ElementModel = {
196
196
  elType: string;
197
197
  settings: BackboneModel<Props>;
198
198
  editor_settings: Record<string, unknown>;
199
+ styles?: V1ElementModelProps['styles'];
199
200
  widgetType: string;
200
201
  editSettings?: BackboneModel<{
201
202
  inactive?: boolean;
package/dist/index.d.ts CHANGED
@@ -196,6 +196,7 @@ type ElementModel = {
196
196
  elType: string;
197
197
  settings: BackboneModel<Props>;
198
198
  editor_settings: Record<string, unknown>;
199
+ styles?: V1ElementModelProps['styles'];
199
200
  widgetType: string;
200
201
  editSettings?: BackboneModel<{
201
202
  inactive?: boolean;
package/dist/index.js CHANGED
@@ -2107,6 +2107,9 @@ var timeRangeTransformer = createTransformer((value) => {
2107
2107
  };
2108
2108
  });
2109
2109
 
2110
+ // src/transformers/shared/icon-transformer.ts
2111
+ var import_editor_controls = require("@elementor/editor-controls");
2112
+
2110
2113
  // src/transformers/shared/process-svg-content.ts
2111
2114
  var import_dompurify2 = __toESM(require("dompurify"));
2112
2115
  var SVG_INLINE_STYLES = "width: 100%; height: 100%; overflow: unset;";
@@ -2129,128 +2132,36 @@ function processSvgContent(svgText) {
2129
2132
  }
2130
2133
 
2131
2134
  // src/transformers/shared/icon-transformer.ts
2132
- var FONT_AWESOME_JSON = {
2133
- width: 0,
2134
- height: 1,
2135
- aliases: 2,
2136
- unicode: 3,
2137
- path: 4
2138
- };
2139
- var fontAwesomeJsonCache = /* @__PURE__ */ new Map();
2135
+ var EMPTY_ICON_RESULT = { html: null, url: null };
2136
+ var ICON_SVG_SIZE = "100%";
2137
+ var ICON_SVG_OVERFLOW = "visible";
2140
2138
  var iconTransformer = createTransformer(async (value, { signal }) => {
2141
2139
  const iconValue = typeof value.value === "string" ? value.value : null;
2142
2140
  const library = typeof value.library === "string" ? value.library : null;
2143
2141
  if (!iconValue || !library) {
2144
- return { html: null, url: null };
2142
+ return EMPTY_ICON_RESULT;
2145
2143
  }
2146
- const iconName = getFontAwesomeIconName(iconValue);
2147
- const jsonFileName = getFontAwesomeJsonFileName(library);
2148
- if (!iconName || !jsonFileName) {
2149
- return { html: null, url: null };
2144
+ const iconName = (0, import_editor_controls.getFontAwesome7IconName)(iconValue);
2145
+ if (!iconName) {
2146
+ return EMPTY_ICON_RESULT;
2150
2147
  }
2151
- const icons = await fetchFontAwesomeIcons(jsonFileName, signal);
2152
- const iconData = icons?.[iconName];
2148
+ const iconData = await (0, import_editor_controls.resolveFontAwesome7Icon)(library, iconName, signal);
2153
2149
  if (!iconData) {
2154
- return { html: null, url: null };
2150
+ return EMPTY_ICON_RESULT;
2155
2151
  }
2156
2152
  const svgText = buildFontAwesomeSvg(iconData);
2157
2153
  if (!svgText) {
2158
- return { html: null, url: null };
2154
+ return EMPTY_ICON_RESULT;
2159
2155
  }
2160
2156
  const html = processIconSvgContent(svgText);
2161
2157
  return { html, url: null };
2162
2158
  });
2163
- function getFontAwesomeIconName(iconValue) {
2164
- const match = iconValue.match(/^fa\S*\s+fa-(.+)$/);
2165
- return match?.[1] ?? null;
2166
- }
2167
- function getFontAwesomeJsonFileName(library) {
2168
- const fileName = library.replace(/^fa-/, "");
2169
- const config = getFontAwesome7EditorConfig();
2170
- if (!config?.jsonFiles.includes(fileName)) {
2171
- return null;
2172
- }
2173
- return fileName;
2174
- }
2175
- function getFontAwesome7EditorConfig() {
2176
- const config = window.elementorCommon?.config?.fontAwesome?.v7;
2177
- if (!config || !Array.isArray(config.jsonFiles) || typeof config.jsonBaseUrl !== "string" || config.jsonBaseUrl === "") {
2178
- return null;
2179
- }
2180
- return {
2181
- jsonFiles: config.jsonFiles,
2182
- jsonBaseUrl: config.jsonBaseUrl
2183
- };
2184
- }
2185
- async function fetchFontAwesomeIcons(jsonFileName, signal) {
2186
- const cached = fontAwesomeJsonCache.get(jsonFileName);
2187
- if (cached) {
2188
- return cached;
2189
- }
2190
- const icons = await loadFontAwesomeIcons(jsonFileName, signal);
2191
- if (icons) {
2192
- fontAwesomeJsonCache.set(jsonFileName, icons);
2193
- }
2194
- return icons;
2195
- }
2196
- async function loadFontAwesomeIcons(jsonFileName, signal) {
2197
- const config = getFontAwesome7EditorConfig();
2198
- if (!config?.jsonFiles.includes(jsonFileName)) {
2199
- return null;
2200
- }
2201
- try {
2202
- const response = await fetch(`${config.jsonBaseUrl}${jsonFileName}.json`, {
2203
- signal
2204
- });
2205
- if (!response.ok) {
2206
- return null;
2207
- }
2208
- const data = await response.json();
2209
- const icons = data.icons;
2210
- if (!icons || typeof icons !== "object") {
2211
- return null;
2212
- }
2213
- return indexFontAwesomeIcons(icons);
2214
- } catch {
2215
- return null;
2216
- }
2217
- }
2218
- function indexFontAwesomeIcons(icons) {
2219
- const index = {};
2220
- for (const [name, iconData] of Object.entries(icons)) {
2221
- if (!isValidIconTuple(iconData)) {
2222
- continue;
2223
- }
2224
- index[name] = iconData;
2225
- for (const alias of iconData[FONT_AWESOME_JSON.aliases]) {
2226
- if (typeof alias === "string" && alias !== "" && !index[alias]) {
2227
- index[alias] = iconData;
2228
- }
2229
- }
2230
- }
2231
- return index;
2232
- }
2233
- function isValidIconTuple(iconData) {
2234
- return Array.isArray(iconData) && iconData.length >= 5 && typeof iconData[FONT_AWESOME_JSON.width] === "number" && typeof iconData[FONT_AWESOME_JSON.height] === "number" && Array.isArray(iconData[FONT_AWESOME_JSON.aliases]);
2235
- }
2236
2159
  function buildFontAwesomeSvg(iconData) {
2237
- const width = iconData[FONT_AWESOME_JSON.width];
2238
- const height = iconData[FONT_AWESOME_JSON.height];
2239
- const paths = normalizePaths(iconData[FONT_AWESOME_JSON.path]);
2240
- if (paths.length === 0) {
2160
+ if (iconData.paths.length === 0) {
2241
2161
  return null;
2242
2162
  }
2243
- const pathMarkup = paths.map((path) => `<path d="${escapeSvgPath(path)}"></path>`).join("");
2244
- return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}">${pathMarkup}</svg>`;
2245
- }
2246
- function normalizePaths(pathData) {
2247
- if (typeof pathData === "string" && pathData !== "") {
2248
- return [pathData];
2249
- }
2250
- if (!Array.isArray(pathData)) {
2251
- return [];
2252
- }
2253
- return pathData.filter((path) => typeof path === "string" && path !== "");
2163
+ const pathMarkup = iconData.paths.map((path) => `<path d="${escapeSvgPath(path)}"></path>`).join("");
2164
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${iconData.width} ${iconData.height}">${pathMarkup}</svg>`;
2254
2165
  }
2255
2166
  function escapeSvgPath(path) {
2256
2167
  return path.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -2267,9 +2178,9 @@ function processIconSvgContent(svgText) {
2267
2178
  return null;
2268
2179
  }
2269
2180
  svgElement.setAttribute("aria-hidden", "true");
2270
- svgElement.style.setProperty("width", "100%");
2271
- svgElement.style.setProperty("height", "100%");
2272
- svgElement.style.setProperty("overflow", "visible");
2181
+ svgElement.style.setProperty("width", ICON_SVG_SIZE);
2182
+ svgElement.style.setProperty("height", ICON_SVG_SIZE);
2183
+ svgElement.style.setProperty("overflow", ICON_SVG_OVERFLOW);
2273
2184
  return svgElement.outerHTML;
2274
2185
  }
2275
2186
 
@@ -2665,10 +2576,10 @@ var transformSkewTransformer = createTransformer((value) => {
2665
2576
  });
2666
2577
 
2667
2578
  // src/transformers/styles/transition-transformer.ts
2668
- var import_editor_controls = require("@elementor/editor-controls");
2579
+ var import_editor_controls2 = require("@elementor/editor-controls");
2669
2580
  var getAllowedProperties = () => {
2670
2581
  const allowedProperties = /* @__PURE__ */ new Set();
2671
- import_editor_controls.transitionProperties.forEach((category) => {
2582
+ import_editor_controls2.transitionProperties.forEach((category) => {
2672
2583
  category.properties.forEach((property) => {
2673
2584
  allowedProperties.add(property.value);
2674
2585
  });
@@ -3111,6 +3022,7 @@ function createTemplatedElementView({
3111
3022
  }
3112
3023
 
3113
3024
  // src/legacy/create-nested-templated-element-type.ts
3025
+ var STYLES_REFERENCE_UNTRACKED = /* @__PURE__ */ Symbol("styles-reference-untracked");
3114
3026
  function canBeNestedTemplated(element) {
3115
3027
  return canBeTemplated(element) && "support_nesting" in element && !!element.support_nesting;
3116
3028
  }
@@ -3170,6 +3082,7 @@ function createNestedTemplatedElementView({
3170
3082
  return AtomicElementBaseView.extend({
3171
3083
  _abortController: null,
3172
3084
  _lastResolvedSettingsHash: null,
3085
+ _lastRenderedStyles: STYLES_REFERENCE_UNTRACKED,
3173
3086
  _domUpdateWasSkipped: false,
3174
3087
  template: false,
3175
3088
  attributes() {
@@ -3206,6 +3119,23 @@ function createNestedTemplatedElementView({
3206
3119
  this._initAlpine();
3207
3120
  });
3208
3121
  this.model.trigger("render:complete");
3122
+ this._notifyStylesChanged();
3123
+ },
3124
+ // `document/elements/create` fires before the nested template has fully painted, so the
3125
+ // styles provider rebuilds CSS too early and misses the new local class IDs. Dispatching
3126
+ // ELEMENT_STYLE_CHANGE_EVENT here — after the template and all children are in the DOM —
3127
+ // gives the provider a second chance to regenerate CSS against the live class names.
3128
+ //
3129
+ // The reference-equality guard prevents a parent re-render from emitting one event per
3130
+ // unchanged descendant. Use a sentinel for the initial state — otherwise elements with no
3131
+ // local styles (undefined === undefined) would never notify, and descendants like e-heading
3132
+ // that rely on this post-paint refresh would lose their CSS after detach.
3133
+ _notifyStylesChanged() {
3134
+ const styles = this.model.get("styles");
3135
+ if (this._lastRenderedStyles !== STYLES_REFERENCE_UNTRACKED && styles === this._lastRenderedStyles) {
3136
+ return;
3137
+ }
3138
+ this._lastRenderedStyles = styles;
3209
3139
  window.dispatchEvent(new CustomEvent(import_editor_elements6.ELEMENT_STYLE_CHANGE_EVENT));
3210
3140
  },
3211
3141
  async _renderTemplate() {
@@ -3505,7 +3435,7 @@ var ReplacementBase = class {
3505
3435
  // src/legacy/replacements/inline-editing/canvas-inline-editor.tsx
3506
3436
  var React10 = __toESM(require("react"));
3507
3437
  var import_react18 = require("react");
3508
- var import_editor_controls2 = require("@elementor/editor-controls");
3438
+ var import_editor_controls3 = require("@elementor/editor-controls");
3509
3439
  var import_ui5 = require("@elementor/ui");
3510
3440
  var import_react19 = require("@floating-ui/react");
3511
3441
 
@@ -3676,7 +3606,7 @@ var CanvasInlineEditor = ({
3676
3606
  return null;
3677
3607
  }
3678
3608
  return /* @__PURE__ */ React10.createElement(import_ui5.ThemeProvider, null, /* @__PURE__ */ React10.createElement(InlineEditingOverlay, { expectedTag, rootElement, id }), /* @__PURE__ */ React10.createElement(
3679
- import_editor_controls2.InlineEditor,
3609
+ import_editor_controls3.InlineEditor,
3680
3610
  {
3681
3611
  onEditorCreate: setEditor,
3682
3612
  mountElement: contentElement,
@@ -3728,7 +3658,7 @@ var InlineEditingToolbar = ({ anchor, editor, id }) => {
3728
3658
  pointerEvents: "none"
3729
3659
  }
3730
3660
  },
3731
- /* @__PURE__ */ React10.createElement(import_editor_controls2.InlineEditorToolbar, { editor, elementId: id })
3661
+ /* @__PURE__ */ React10.createElement(import_editor_controls3.InlineEditorToolbar, { editor, elementId: id })
3732
3662
  ));
3733
3663
  };
3734
3664
 
package/dist/index.mjs CHANGED
@@ -2059,6 +2059,12 @@ var timeRangeTransformer = createTransformer((value) => {
2059
2059
  };
2060
2060
  });
2061
2061
 
2062
+ // src/transformers/shared/icon-transformer.ts
2063
+ import {
2064
+ getFontAwesome7IconName,
2065
+ resolveFontAwesome7Icon
2066
+ } from "@elementor/editor-controls";
2067
+
2062
2068
  // src/transformers/shared/process-svg-content.ts
2063
2069
  import DOMPurify2 from "dompurify";
2064
2070
  var SVG_INLINE_STYLES = "width: 100%; height: 100%; overflow: unset;";
@@ -2081,128 +2087,36 @@ function processSvgContent(svgText) {
2081
2087
  }
2082
2088
 
2083
2089
  // src/transformers/shared/icon-transformer.ts
2084
- var FONT_AWESOME_JSON = {
2085
- width: 0,
2086
- height: 1,
2087
- aliases: 2,
2088
- unicode: 3,
2089
- path: 4
2090
- };
2091
- var fontAwesomeJsonCache = /* @__PURE__ */ new Map();
2090
+ var EMPTY_ICON_RESULT = { html: null, url: null };
2091
+ var ICON_SVG_SIZE = "100%";
2092
+ var ICON_SVG_OVERFLOW = "visible";
2092
2093
  var iconTransformer = createTransformer(async (value, { signal }) => {
2093
2094
  const iconValue = typeof value.value === "string" ? value.value : null;
2094
2095
  const library = typeof value.library === "string" ? value.library : null;
2095
2096
  if (!iconValue || !library) {
2096
- return { html: null, url: null };
2097
+ return EMPTY_ICON_RESULT;
2097
2098
  }
2098
- const iconName = getFontAwesomeIconName(iconValue);
2099
- const jsonFileName = getFontAwesomeJsonFileName(library);
2100
- if (!iconName || !jsonFileName) {
2101
- return { html: null, url: null };
2099
+ const iconName = getFontAwesome7IconName(iconValue);
2100
+ if (!iconName) {
2101
+ return EMPTY_ICON_RESULT;
2102
2102
  }
2103
- const icons = await fetchFontAwesomeIcons(jsonFileName, signal);
2104
- const iconData = icons?.[iconName];
2103
+ const iconData = await resolveFontAwesome7Icon(library, iconName, signal);
2105
2104
  if (!iconData) {
2106
- return { html: null, url: null };
2105
+ return EMPTY_ICON_RESULT;
2107
2106
  }
2108
2107
  const svgText = buildFontAwesomeSvg(iconData);
2109
2108
  if (!svgText) {
2110
- return { html: null, url: null };
2109
+ return EMPTY_ICON_RESULT;
2111
2110
  }
2112
2111
  const html = processIconSvgContent(svgText);
2113
2112
  return { html, url: null };
2114
2113
  });
2115
- function getFontAwesomeIconName(iconValue) {
2116
- const match = iconValue.match(/^fa\S*\s+fa-(.+)$/);
2117
- return match?.[1] ?? null;
2118
- }
2119
- function getFontAwesomeJsonFileName(library) {
2120
- const fileName = library.replace(/^fa-/, "");
2121
- const config = getFontAwesome7EditorConfig();
2122
- if (!config?.jsonFiles.includes(fileName)) {
2123
- return null;
2124
- }
2125
- return fileName;
2126
- }
2127
- function getFontAwesome7EditorConfig() {
2128
- const config = window.elementorCommon?.config?.fontAwesome?.v7;
2129
- if (!config || !Array.isArray(config.jsonFiles) || typeof config.jsonBaseUrl !== "string" || config.jsonBaseUrl === "") {
2130
- return null;
2131
- }
2132
- return {
2133
- jsonFiles: config.jsonFiles,
2134
- jsonBaseUrl: config.jsonBaseUrl
2135
- };
2136
- }
2137
- async function fetchFontAwesomeIcons(jsonFileName, signal) {
2138
- const cached = fontAwesomeJsonCache.get(jsonFileName);
2139
- if (cached) {
2140
- return cached;
2141
- }
2142
- const icons = await loadFontAwesomeIcons(jsonFileName, signal);
2143
- if (icons) {
2144
- fontAwesomeJsonCache.set(jsonFileName, icons);
2145
- }
2146
- return icons;
2147
- }
2148
- async function loadFontAwesomeIcons(jsonFileName, signal) {
2149
- const config = getFontAwesome7EditorConfig();
2150
- if (!config?.jsonFiles.includes(jsonFileName)) {
2151
- return null;
2152
- }
2153
- try {
2154
- const response = await fetch(`${config.jsonBaseUrl}${jsonFileName}.json`, {
2155
- signal
2156
- });
2157
- if (!response.ok) {
2158
- return null;
2159
- }
2160
- const data = await response.json();
2161
- const icons = data.icons;
2162
- if (!icons || typeof icons !== "object") {
2163
- return null;
2164
- }
2165
- return indexFontAwesomeIcons(icons);
2166
- } catch {
2167
- return null;
2168
- }
2169
- }
2170
- function indexFontAwesomeIcons(icons) {
2171
- const index = {};
2172
- for (const [name, iconData] of Object.entries(icons)) {
2173
- if (!isValidIconTuple(iconData)) {
2174
- continue;
2175
- }
2176
- index[name] = iconData;
2177
- for (const alias of iconData[FONT_AWESOME_JSON.aliases]) {
2178
- if (typeof alias === "string" && alias !== "" && !index[alias]) {
2179
- index[alias] = iconData;
2180
- }
2181
- }
2182
- }
2183
- return index;
2184
- }
2185
- function isValidIconTuple(iconData) {
2186
- return Array.isArray(iconData) && iconData.length >= 5 && typeof iconData[FONT_AWESOME_JSON.width] === "number" && typeof iconData[FONT_AWESOME_JSON.height] === "number" && Array.isArray(iconData[FONT_AWESOME_JSON.aliases]);
2187
- }
2188
2114
  function buildFontAwesomeSvg(iconData) {
2189
- const width = iconData[FONT_AWESOME_JSON.width];
2190
- const height = iconData[FONT_AWESOME_JSON.height];
2191
- const paths = normalizePaths(iconData[FONT_AWESOME_JSON.path]);
2192
- if (paths.length === 0) {
2115
+ if (iconData.paths.length === 0) {
2193
2116
  return null;
2194
2117
  }
2195
- const pathMarkup = paths.map((path) => `<path d="${escapeSvgPath(path)}"></path>`).join("");
2196
- return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}">${pathMarkup}</svg>`;
2197
- }
2198
- function normalizePaths(pathData) {
2199
- if (typeof pathData === "string" && pathData !== "") {
2200
- return [pathData];
2201
- }
2202
- if (!Array.isArray(pathData)) {
2203
- return [];
2204
- }
2205
- return pathData.filter((path) => typeof path === "string" && path !== "");
2118
+ const pathMarkup = iconData.paths.map((path) => `<path d="${escapeSvgPath(path)}"></path>`).join("");
2119
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${iconData.width} ${iconData.height}">${pathMarkup}</svg>`;
2206
2120
  }
2207
2121
  function escapeSvgPath(path) {
2208
2122
  return path.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -2219,9 +2133,9 @@ function processIconSvgContent(svgText) {
2219
2133
  return null;
2220
2134
  }
2221
2135
  svgElement.setAttribute("aria-hidden", "true");
2222
- svgElement.style.setProperty("width", "100%");
2223
- svgElement.style.setProperty("height", "100%");
2224
- svgElement.style.setProperty("overflow", "visible");
2136
+ svgElement.style.setProperty("width", ICON_SVG_SIZE);
2137
+ svgElement.style.setProperty("height", ICON_SVG_SIZE);
2138
+ svgElement.style.setProperty("overflow", ICON_SVG_OVERFLOW);
2225
2139
  return svgElement.outerHTML;
2226
2140
  }
2227
2141
 
@@ -3068,6 +2982,7 @@ function createTemplatedElementView({
3068
2982
  }
3069
2983
 
3070
2984
  // src/legacy/create-nested-templated-element-type.ts
2985
+ var STYLES_REFERENCE_UNTRACKED = /* @__PURE__ */ Symbol("styles-reference-untracked");
3071
2986
  function canBeNestedTemplated(element) {
3072
2987
  return canBeTemplated(element) && "support_nesting" in element && !!element.support_nesting;
3073
2988
  }
@@ -3127,6 +3042,7 @@ function createNestedTemplatedElementView({
3127
3042
  return AtomicElementBaseView.extend({
3128
3043
  _abortController: null,
3129
3044
  _lastResolvedSettingsHash: null,
3045
+ _lastRenderedStyles: STYLES_REFERENCE_UNTRACKED,
3130
3046
  _domUpdateWasSkipped: false,
3131
3047
  template: false,
3132
3048
  attributes() {
@@ -3163,6 +3079,23 @@ function createNestedTemplatedElementView({
3163
3079
  this._initAlpine();
3164
3080
  });
3165
3081
  this.model.trigger("render:complete");
3082
+ this._notifyStylesChanged();
3083
+ },
3084
+ // `document/elements/create` fires before the nested template has fully painted, so the
3085
+ // styles provider rebuilds CSS too early and misses the new local class IDs. Dispatching
3086
+ // ELEMENT_STYLE_CHANGE_EVENT here — after the template and all children are in the DOM —
3087
+ // gives the provider a second chance to regenerate CSS against the live class names.
3088
+ //
3089
+ // The reference-equality guard prevents a parent re-render from emitting one event per
3090
+ // unchanged descendant. Use a sentinel for the initial state — otherwise elements with no
3091
+ // local styles (undefined === undefined) would never notify, and descendants like e-heading
3092
+ // that rely on this post-paint refresh would lose their CSS after detach.
3093
+ _notifyStylesChanged() {
3094
+ const styles = this.model.get("styles");
3095
+ if (this._lastRenderedStyles !== STYLES_REFERENCE_UNTRACKED && styles === this._lastRenderedStyles) {
3096
+ return;
3097
+ }
3098
+ this._lastRenderedStyles = styles;
3166
3099
  window.dispatchEvent(new CustomEvent(ELEMENT_STYLE_CHANGE_EVENT2));
3167
3100
  },
3168
3101
  async _renderTemplate() {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elementor/editor-canvas",
3
3
  "description": "Elementor Editor Canvas",
4
- "version": "4.3.0-beta2",
4
+ "version": "4.3.0-beta3",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -37,26 +37,26 @@
37
37
  "react-dom": "^18.3.1"
38
38
  },
39
39
  "dependencies": {
40
- "@elementor/editor": "4.3.0-beta2",
41
- "@elementor/editor-controls": "4.3.0-beta2",
42
- "@elementor/editor-documents": "4.3.0-beta2",
43
- "@elementor/editor-elements": "4.3.0-beta2",
44
- "@elementor/editor-interactions": "4.3.0-beta2",
45
- "@elementor/editor-mcp": "4.3.0-beta2",
46
- "@elementor/editor-notifications": "4.3.0-beta2",
47
- "@elementor/editor-props": "4.3.0-beta2",
48
- "@elementor/editor-responsive": "4.3.0-beta2",
49
- "@elementor/editor-styles": "4.3.0-beta2",
50
- "@elementor/editor-styles-repository": "4.3.0-beta2",
51
- "@elementor/editor-ui": "4.3.0-beta2",
52
- "@elementor/editor-v1-adapters": "4.3.0-beta2",
53
- "@elementor/events": "4.3.0-beta2",
54
- "@elementor/http-client": "4.3.0-beta2",
55
- "@elementor/schema": "4.3.0-beta2",
56
- "@elementor/twing": "4.3.0-beta2",
40
+ "@elementor/editor": "4.3.0-beta3",
41
+ "@elementor/editor-controls": "4.3.0-beta3",
42
+ "@elementor/editor-documents": "4.3.0-beta3",
43
+ "@elementor/editor-elements": "4.3.0-beta3",
44
+ "@elementor/editor-interactions": "4.3.0-beta3",
45
+ "@elementor/editor-mcp": "4.3.0-beta3",
46
+ "@elementor/editor-notifications": "4.3.0-beta3",
47
+ "@elementor/editor-props": "4.3.0-beta3",
48
+ "@elementor/editor-responsive": "4.3.0-beta3",
49
+ "@elementor/editor-styles": "4.3.0-beta3",
50
+ "@elementor/editor-styles-repository": "4.3.0-beta3",
51
+ "@elementor/editor-ui": "4.3.0-beta3",
52
+ "@elementor/editor-v1-adapters": "4.3.0-beta3",
53
+ "@elementor/events": "4.3.0-beta3",
54
+ "@elementor/http-client": "4.3.0-beta3",
55
+ "@elementor/schema": "4.3.0-beta3",
56
+ "@elementor/twing": "4.3.0-beta3",
57
57
  "@elementor/ui": "1.37.5",
58
- "@elementor/utils": "4.3.0-beta2",
59
- "@elementor/wp-media": "4.3.0-beta2",
58
+ "@elementor/utils": "4.3.0-beta3",
59
+ "@elementor/wp-media": "4.3.0-beta3",
60
60
  "@floating-ui/react": "^0.27.5",
61
61
  "@wordpress/i18n": "^5.13.0",
62
62
  "dompurify": "^3.2.6"
@@ -12,7 +12,13 @@ import {
12
12
  setupTwigRenderer,
13
13
  waitForChildrenToComplete,
14
14
  } from './twig-rendering-utils';
15
- import { type ElementType, type ElementView, type LegacyWindow, type NestedTemplatedElementViewClass } from './types';
15
+ import {
16
+ type ElementModel,
17
+ type ElementType,
18
+ type ElementView,
19
+ type LegacyWindow,
20
+ type NestedTemplatedElementViewClass,
21
+ } from './types';
16
22
 
17
23
  export type NestedTemplatedElementConfig = TemplatedElementConfig & {
18
24
  allowed_child_types?: string[];
@@ -21,6 +27,8 @@ export type NestedTemplatedElementConfig = TemplatedElementConfig & {
21
27
 
22
28
  export type ModelExtensions = Record< string, unknown >;
23
29
 
30
+ const STYLES_REFERENCE_UNTRACKED = Symbol( 'styles-reference-untracked' );
31
+
24
32
  export type CreateNestedTemplatedElementTypeOptions = {
25
33
  type: string;
26
34
  renderer: DomRenderer;
@@ -108,6 +116,7 @@ export function createNestedTemplatedElementView( {
108
116
  return AtomicElementBaseView.extend( {
109
117
  _abortController: null as AbortController | null,
110
118
  _lastResolvedSettingsHash: null as string | null,
119
+ _lastRenderedStyles: STYLES_REFERENCE_UNTRACKED as typeof STYLES_REFERENCE_UNTRACKED | ElementModel[ 'styles' ],
111
120
  _domUpdateWasSkipped: false,
112
121
 
113
122
  template: false,
@@ -164,6 +173,27 @@ export function createNestedTemplatedElementView( {
164
173
  } );
165
174
 
166
175
  this.model.trigger( 'render:complete' );
176
+ this._notifyStylesChanged();
177
+ },
178
+
179
+ // `document/elements/create` fires before the nested template has fully painted, so the
180
+ // styles provider rebuilds CSS too early and misses the new local class IDs. Dispatching
181
+ // ELEMENT_STYLE_CHANGE_EVENT here — after the template and all children are in the DOM —
182
+ // gives the provider a second chance to regenerate CSS against the live class names.
183
+ //
184
+ // The reference-equality guard prevents a parent re-render from emitting one event per
185
+ // unchanged descendant. Use a sentinel for the initial state — otherwise elements with no
186
+ // local styles (undefined === undefined) would never notify, and descendants like e-heading
187
+ // that rely on this post-paint refresh would lose their CSS after detach.
188
+ _notifyStylesChanged() {
189
+ const styles = this.model.get( 'styles' );
190
+
191
+ if ( this._lastRenderedStyles !== STYLES_REFERENCE_UNTRACKED && styles === this._lastRenderedStyles ) {
192
+ return;
193
+ }
194
+
195
+ this._lastRenderedStyles = styles;
196
+
167
197
  window.dispatchEvent( new CustomEvent( ELEMENT_STYLE_CHANGE_EVENT ) );
168
198
  },
169
199
 
@@ -229,6 +229,7 @@ export type ElementModel = {
229
229
  elType: string;
230
230
  settings: BackboneModel< Props >;
231
231
  editor_settings: Record< string, unknown >;
232
+ styles?: V1ElementModelProps[ 'styles' ];
232
233
  widgetType: string;
233
234
  editSettings?: BackboneModel< { inactive?: boolean } >;
234
235
  elements?: BackboneCollection< ElementModel >;
@@ -1,10 +1,10 @@
1
+ import { resetFontAwesome7IconsCache } from '@elementor/editor-controls';
1
2
  import { iconPropTypeUtil, stringPropTypeUtil } from '@elementor/editor-props';
2
3
 
3
4
  import { iconPropType } from '../../../__tests__/prop-types';
4
5
  import { initSettingsTransformers } from '../../../init-settings-transformers';
5
6
  import { createPropsResolver } from '../../../renderers/create-props-resolver';
6
7
  import { settingsTransformersRegistry } from '../../../settings-transformers-registry';
7
- import { resetFontAwesomeIconsCache } from '../icon-transformer';
8
8
 
9
9
  const STAR_PATH = 'M0 0h100v100H0z';
10
10
  const STAR_WIDTH = 576;
@@ -44,7 +44,7 @@ describe( 'iconTransformer', () => {
44
44
 
45
45
  beforeEach( () => {
46
46
  jest.clearAllMocks();
47
- resetFontAwesomeIconsCache();
47
+ resetFontAwesome7IconsCache();
48
48
  initSettingsTransformers();
49
49
  window.elementorCommon = {
50
50
  config: {
@@ -1,55 +1,46 @@
1
+ import {
2
+ type FontAwesome7IconDefinition,
3
+ getFontAwesome7IconName,
4
+ resolveFontAwesome7Icon,
5
+ } from '@elementor/editor-controls';
6
+
1
7
  import { createTransformer } from '../create-transformer';
2
8
  import type { TransformerOptions } from '../types';
3
9
  import { processSvgContent } from './process-svg-content';
4
10
 
11
+ const EMPTY_ICON_RESULT = { html: null, url: null };
12
+ const ICON_SVG_SIZE = '100%';
13
+ const ICON_SVG_OVERFLOW = 'visible';
14
+
5
15
  type IconValue = {
6
16
  value?: unknown;
7
17
  library?: unknown;
8
18
  };
9
19
 
10
- type FontAwesomeIconJson = [ number, number, unknown[], unknown, string | string[] ];
11
-
12
- type FontAwesome7EditorConfig = {
13
- jsonFiles: string[];
14
- jsonBaseUrl: string;
15
- };
16
-
17
- const FONT_AWESOME_JSON = {
18
- width: 0,
19
- height: 1,
20
- aliases: 2,
21
- unicode: 3,
22
- path: 4,
23
- } as const;
24
-
25
- const fontAwesomeJsonCache = new Map< string, Record< string, FontAwesomeIconJson > >();
26
-
27
20
  export const iconTransformer = createTransformer( async ( value: IconValue, { signal }: TransformerOptions ) => {
28
21
  const iconValue = typeof value.value === 'string' ? value.value : null;
29
22
  const library = typeof value.library === 'string' ? value.library : null;
30
23
 
31
24
  if ( ! iconValue || ! library ) {
32
- return { html: null, url: null };
25
+ return EMPTY_ICON_RESULT;
33
26
  }
34
27
 
35
- const iconName = getFontAwesomeIconName( iconValue );
36
- const jsonFileName = getFontAwesomeJsonFileName( library );
28
+ const iconName = getFontAwesome7IconName( iconValue );
37
29
 
38
- if ( ! iconName || ! jsonFileName ) {
39
- return { html: null, url: null };
30
+ if ( ! iconName ) {
31
+ return EMPTY_ICON_RESULT;
40
32
  }
41
33
 
42
- const icons = await fetchFontAwesomeIcons( jsonFileName, signal );
43
- const iconData = icons?.[ iconName ];
34
+ const iconData = await resolveFontAwesome7Icon( library, iconName, signal );
44
35
 
45
36
  if ( ! iconData ) {
46
- return { html: null, url: null };
37
+ return EMPTY_ICON_RESULT;
47
38
  }
48
39
 
49
40
  const svgText = buildFontAwesomeSvg( iconData );
50
41
 
51
42
  if ( ! svgText ) {
52
- return { html: null, url: null };
43
+ return EMPTY_ICON_RESULT;
53
44
  }
54
45
 
55
46
  const html = processIconSvgContent( svgText );
@@ -57,146 +48,14 @@ export const iconTransformer = createTransformer( async ( value: IconValue, { si
57
48
  return { html, url: null };
58
49
  } );
59
50
 
60
- function getFontAwesomeIconName( iconValue: string ): string | null {
61
- const match = iconValue.match( /^fa\S*\s+fa-(.+)$/ );
62
-
63
- return match?.[ 1 ] ?? null;
64
- }
65
-
66
- function getFontAwesomeJsonFileName( library: string ): string | null {
67
- const fileName = library.replace( /^fa-/, '' );
68
- const config = getFontAwesome7EditorConfig();
69
-
70
- if ( ! config?.jsonFiles.includes( fileName ) ) {
71
- return null;
72
- }
73
-
74
- return fileName;
75
- }
76
-
77
- function getFontAwesome7EditorConfig(): FontAwesome7EditorConfig | null {
78
- const config = window.elementorCommon?.config?.fontAwesome?.v7;
79
-
80
- if (
81
- ! config ||
82
- ! Array.isArray( config.jsonFiles ) ||
83
- typeof config.jsonBaseUrl !== 'string' ||
84
- config.jsonBaseUrl === ''
85
- ) {
86
- return null;
87
- }
88
-
89
- return {
90
- jsonFiles: config.jsonFiles,
91
- jsonBaseUrl: config.jsonBaseUrl,
92
- };
93
- }
94
-
95
- async function fetchFontAwesomeIcons(
96
- jsonFileName: string,
97
- signal?: AbortSignal
98
- ): Promise< Record< string, FontAwesomeIconJson > | null > {
99
- const cached = fontAwesomeJsonCache.get( jsonFileName );
100
-
101
- if ( cached ) {
102
- return cached;
103
- }
104
-
105
- const icons = await loadFontAwesomeIcons( jsonFileName, signal );
106
-
107
- if ( icons ) {
108
- fontAwesomeJsonCache.set( jsonFileName, icons );
109
- }
110
-
111
- return icons;
112
- }
113
-
114
- async function loadFontAwesomeIcons(
115
- jsonFileName: string,
116
- signal?: AbortSignal
117
- ): Promise< Record< string, FontAwesomeIconJson > | null > {
118
- const config = getFontAwesome7EditorConfig();
119
-
120
- if ( ! config?.jsonFiles.includes( jsonFileName ) ) {
121
- return null;
122
- }
123
-
124
- try {
125
- const response = await fetch( `${ config.jsonBaseUrl }${ jsonFileName }.json`, {
126
- signal,
127
- } );
128
-
129
- if ( ! response.ok ) {
130
- return null;
131
- }
132
-
133
- const data = ( await response.json() ) as { icons?: Record< string, FontAwesomeIconJson > };
134
- const icons = data.icons;
135
-
136
- if ( ! icons || typeof icons !== 'object' ) {
137
- return null;
138
- }
139
-
140
- return indexFontAwesomeIcons( icons );
141
- } catch {
142
- return null;
143
- }
144
- }
145
-
146
- function indexFontAwesomeIcons( icons: Record< string, FontAwesomeIconJson > ): Record< string, FontAwesomeIconJson > {
147
- const index: Record< string, FontAwesomeIconJson > = {};
148
-
149
- for ( const [ name, iconData ] of Object.entries( icons ) ) {
150
- if ( ! isValidIconTuple( iconData ) ) {
151
- continue;
152
- }
153
-
154
- index[ name ] = iconData;
155
-
156
- for ( const alias of iconData[ FONT_AWESOME_JSON.aliases ] ) {
157
- if ( typeof alias === 'string' && alias !== '' && ! index[ alias ] ) {
158
- index[ alias ] = iconData;
159
- }
160
- }
161
- }
162
-
163
- return index;
164
- }
165
-
166
- function isValidIconTuple( iconData: unknown ): iconData is FontAwesomeIconJson {
167
- return (
168
- Array.isArray( iconData ) &&
169
- iconData.length >= 5 &&
170
- typeof iconData[ FONT_AWESOME_JSON.width ] === 'number' &&
171
- typeof iconData[ FONT_AWESOME_JSON.height ] === 'number' &&
172
- Array.isArray( iconData[ FONT_AWESOME_JSON.aliases ] )
173
- );
174
- }
175
-
176
- function buildFontAwesomeSvg( iconData: FontAwesomeIconJson ): string | null {
177
- const width = iconData[ FONT_AWESOME_JSON.width ];
178
- const height = iconData[ FONT_AWESOME_JSON.height ];
179
- const paths = normalizePaths( iconData[ FONT_AWESOME_JSON.path ] );
180
-
181
- if ( paths.length === 0 ) {
51
+ function buildFontAwesomeSvg( iconData: FontAwesome7IconDefinition ): string | null {
52
+ if ( iconData.paths.length === 0 ) {
182
53
  return null;
183
54
  }
184
55
 
185
- const pathMarkup = paths.map( ( path ) => `<path d="${ escapeSvgPath( path ) }"></path>` ).join( '' );
56
+ const pathMarkup = iconData.paths.map( ( path ) => `<path d="${ escapeSvgPath( path ) }"></path>` ).join( '' );
186
57
 
187
- return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${ width } ${ height }">${ pathMarkup }</svg>`;
188
- }
189
-
190
- function normalizePaths( pathData: string | string[] ): string[] {
191
- if ( typeof pathData === 'string' && pathData !== '' ) {
192
- return [ pathData ];
193
- }
194
-
195
- if ( ! Array.isArray( pathData ) ) {
196
- return [];
197
- }
198
-
199
- return pathData.filter( ( path ): path is string => typeof path === 'string' && path !== '' );
58
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${ iconData.width } ${ iconData.height }">${ pathMarkup }</svg>`;
200
59
  }
201
60
 
202
61
  function escapeSvgPath( path: string ): string {
@@ -219,13 +78,9 @@ function processIconSvgContent( svgText: string ): string | null {
219
78
  }
220
79
 
221
80
  svgElement.setAttribute( 'aria-hidden', 'true' );
222
- svgElement.style.setProperty( 'width', '100%' );
223
- svgElement.style.setProperty( 'height', '100%' );
224
- svgElement.style.setProperty( 'overflow', 'visible' );
81
+ svgElement.style.setProperty( 'width', ICON_SVG_SIZE );
82
+ svgElement.style.setProperty( 'height', ICON_SVG_SIZE );
83
+ svgElement.style.setProperty( 'overflow', ICON_SVG_OVERFLOW );
225
84
 
226
85
  return svgElement.outerHTML;
227
86
  }
228
-
229
- export function resetFontAwesomeIconsCache() {
230
- fontAwesomeJsonCache.clear();
231
- }