@elementor/editor-canvas 4.3.0-beta1 → 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,78 +2132,56 @@ 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
- path: 4
2136
- };
2137
- 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";
2138
2138
  var iconTransformer = createTransformer(async (value, { signal }) => {
2139
2139
  const iconValue = typeof value.value === "string" ? value.value : null;
2140
2140
  const library = typeof value.library === "string" ? value.library : null;
2141
2141
  if (!iconValue || !library) {
2142
- return { html: null, url: null };
2142
+ return EMPTY_ICON_RESULT;
2143
2143
  }
2144
- const iconName = getFontAwesomeIconName(iconValue);
2145
- const jsonFileName = getFontAwesomeJsonFileName(library);
2146
- if (!iconName || !jsonFileName) {
2147
- return { html: null, url: null };
2144
+ const iconName = (0, import_editor_controls.getFontAwesome7IconName)(iconValue);
2145
+ if (!iconName) {
2146
+ return EMPTY_ICON_RESULT;
2148
2147
  }
2149
- const icons = await fetchFontAwesomeIcons(jsonFileName, signal);
2150
- const iconData = icons?.[iconName];
2148
+ const iconData = await (0, import_editor_controls.resolveFontAwesome7Icon)(library, iconName, signal);
2151
2149
  if (!iconData) {
2152
- return { html: null, url: null };
2150
+ return EMPTY_ICON_RESULT;
2153
2151
  }
2154
2152
  const svgText = buildFontAwesomeSvg(iconData);
2155
- const html = processSvgContent(svgText);
2153
+ if (!svgText) {
2154
+ return EMPTY_ICON_RESULT;
2155
+ }
2156
+ const html = processIconSvgContent(svgText);
2156
2157
  return { html, url: null };
2157
2158
  });
2158
- function getFontAwesomeIconName(iconValue) {
2159
- const match = iconValue.match(/^fa\S*\s+fa-(.+)$/);
2160
- return match?.[1] ?? null;
2161
- }
2162
- function getFontAwesomeJsonFileName(library) {
2163
- if (!library.startsWith("fa-")) {
2159
+ function buildFontAwesomeSvg(iconData) {
2160
+ if (iconData.paths.length === 0) {
2164
2161
  return null;
2165
2162
  }
2166
- return library.replace(/^fa-/, "");
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>`;
2167
2165
  }
2168
- function getAssetsBaseUrl() {
2169
- const assetsUrl = window.elementorCommon?.config?.urls?.assets;
2170
- return typeof assetsUrl === "string" && assetsUrl !== "" ? assetsUrl : null;
2166
+ function escapeSvgPath(path) {
2167
+ return path.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2171
2168
  }
2172
- async function fetchFontAwesomeIcons(jsonFileName, signal) {
2173
- const cached = fontAwesomeJsonCache.get(jsonFileName);
2174
- if (cached) {
2175
- return cached;
2176
- }
2177
- const icons = await loadFontAwesomeIcons(jsonFileName, signal);
2178
- if (icons) {
2179
- fontAwesomeJsonCache.set(jsonFileName, icons);
2180
- }
2181
- return icons;
2182
- }
2183
- async function loadFontAwesomeIcons(jsonFileName, signal) {
2184
- const assetsUrl = getAssetsBaseUrl();
2185
- if (!assetsUrl) {
2169
+ function processIconSvgContent(svgText) {
2170
+ const html = processSvgContent(svgText);
2171
+ if (!html) {
2186
2172
  return null;
2187
2173
  }
2188
- try {
2189
- const response = await fetch(`${assetsUrl}lib/font-awesome/json/${jsonFileName}.json`, { signal });
2190
- if (!response.ok) {
2191
- return null;
2192
- }
2193
- const data = await response.json();
2194
- return data.icons ?? null;
2195
- } catch {
2174
+ const parser = new DOMParser();
2175
+ const doc = parser.parseFromString(html, "image/svg+xml");
2176
+ const svgElement = doc.querySelector("svg");
2177
+ if (!svgElement) {
2196
2178
  return null;
2197
2179
  }
2198
- }
2199
- function buildFontAwesomeSvg(iconData) {
2200
- const width = iconData[FONT_AWESOME_JSON.width];
2201
- const height = iconData[FONT_AWESOME_JSON.height];
2202
- const path = iconData[FONT_AWESOME_JSON.path];
2203
- return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}"><path d="${path}"></path></svg>`;
2180
+ svgElement.setAttribute("aria-hidden", "true");
2181
+ svgElement.style.setProperty("width", ICON_SVG_SIZE);
2182
+ svgElement.style.setProperty("height", ICON_SVG_SIZE);
2183
+ svgElement.style.setProperty("overflow", ICON_SVG_OVERFLOW);
2184
+ return svgElement.outerHTML;
2204
2185
  }
2205
2186
 
2206
2187
  // src/transformers/shared/image-src-transformer.ts
@@ -2446,6 +2427,10 @@ var mapToFilterFunctionString = (value) => {
2446
2427
  };
2447
2428
 
2448
2429
  // src/transformers/styles/flex-transformer.ts
2430
+ var DEFAULT_FLEX_GROW = 0;
2431
+ var DEFAULT_FLEX_SHRINK = 1;
2432
+ var DEFAULT_FLEX_BASIS = "auto";
2433
+ var formatBasis = (basis) => typeof basis === "object" && basis.size !== void 0 ? `${basis.size}${basis.unit || ""}` : basis;
2449
2434
  var flexTransformer = createTransformer((value) => {
2450
2435
  const grow = value.flexGrow;
2451
2436
  const shrink = value.flexShrink;
@@ -2456,28 +2441,10 @@ var flexTransformer = createTransformer((value) => {
2456
2441
  if (!hasGrow && !hasShrink && !hasBasis) {
2457
2442
  return null;
2458
2443
  }
2459
- if (hasGrow && hasShrink && hasBasis) {
2460
- return `${grow} ${shrink} ${typeof basis === "object" && basis.size !== void 0 ? `${basis.size}${basis.unit || ""}` : basis}`;
2461
- }
2462
- if (hasGrow && hasShrink && !hasBasis) {
2463
- return `${grow} ${shrink}`;
2464
- }
2465
- if (hasGrow && !hasShrink && hasBasis) {
2466
- return `${grow} 1 ${typeof basis === "object" && basis.size !== void 0 ? `${basis.size}${basis.unit || ""}` : basis}`;
2467
- }
2468
- if (!hasGrow && hasShrink && hasBasis) {
2469
- return `0 ${shrink} ${typeof basis === "object" && basis.size !== void 0 ? `${basis.size}${basis.unit || ""}` : basis}`;
2470
- }
2471
- if (hasGrow && !hasShrink && !hasBasis) {
2472
- return `${grow}`;
2473
- }
2474
- if (!hasGrow && hasShrink && !hasBasis) {
2475
- return `0 ${shrink}`;
2476
- }
2477
- if (!hasGrow && !hasShrink && hasBasis) {
2478
- return `0 1 ${typeof basis === "object" && basis.size !== void 0 ? `${basis.size}${basis.unit || ""}` : basis}`;
2479
- }
2480
- return null;
2444
+ const growOut = hasGrow ? grow : DEFAULT_FLEX_GROW;
2445
+ const shrinkOut = hasShrink ? shrink : DEFAULT_FLEX_SHRINK;
2446
+ const basisOut = hasBasis ? formatBasis(basis) : DEFAULT_FLEX_BASIS;
2447
+ return `${growOut} ${shrinkOut} ${basisOut}`;
2481
2448
  });
2482
2449
 
2483
2450
  // src/transformers/styles/font-family-transformer.ts
@@ -2609,10 +2576,10 @@ var transformSkewTransformer = createTransformer((value) => {
2609
2576
  });
2610
2577
 
2611
2578
  // src/transformers/styles/transition-transformer.ts
2612
- var import_editor_controls = require("@elementor/editor-controls");
2579
+ var import_editor_controls2 = require("@elementor/editor-controls");
2613
2580
  var getAllowedProperties = () => {
2614
2581
  const allowedProperties = /* @__PURE__ */ new Set();
2615
- import_editor_controls.transitionProperties.forEach((category) => {
2582
+ import_editor_controls2.transitionProperties.forEach((category) => {
2616
2583
  category.properties.forEach((property) => {
2617
2584
  allowedProperties.add(property.value);
2618
2585
  });
@@ -3055,6 +3022,7 @@ function createTemplatedElementView({
3055
3022
  }
3056
3023
 
3057
3024
  // src/legacy/create-nested-templated-element-type.ts
3025
+ var STYLES_REFERENCE_UNTRACKED = /* @__PURE__ */ Symbol("styles-reference-untracked");
3058
3026
  function canBeNestedTemplated(element) {
3059
3027
  return canBeTemplated(element) && "support_nesting" in element && !!element.support_nesting;
3060
3028
  }
@@ -3114,6 +3082,7 @@ function createNestedTemplatedElementView({
3114
3082
  return AtomicElementBaseView.extend({
3115
3083
  _abortController: null,
3116
3084
  _lastResolvedSettingsHash: null,
3085
+ _lastRenderedStyles: STYLES_REFERENCE_UNTRACKED,
3117
3086
  _domUpdateWasSkipped: false,
3118
3087
  template: false,
3119
3088
  attributes() {
@@ -3150,6 +3119,23 @@ function createNestedTemplatedElementView({
3150
3119
  this._initAlpine();
3151
3120
  });
3152
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;
3153
3139
  window.dispatchEvent(new CustomEvent(import_editor_elements6.ELEMENT_STYLE_CHANGE_EVENT));
3154
3140
  },
3155
3141
  async _renderTemplate() {
@@ -3449,7 +3435,7 @@ var ReplacementBase = class {
3449
3435
  // src/legacy/replacements/inline-editing/canvas-inline-editor.tsx
3450
3436
  var React10 = __toESM(require("react"));
3451
3437
  var import_react18 = require("react");
3452
- var import_editor_controls2 = require("@elementor/editor-controls");
3438
+ var import_editor_controls3 = require("@elementor/editor-controls");
3453
3439
  var import_ui5 = require("@elementor/ui");
3454
3440
  var import_react19 = require("@floating-ui/react");
3455
3441
 
@@ -3620,7 +3606,7 @@ var CanvasInlineEditor = ({
3620
3606
  return null;
3621
3607
  }
3622
3608
  return /* @__PURE__ */ React10.createElement(import_ui5.ThemeProvider, null, /* @__PURE__ */ React10.createElement(InlineEditingOverlay, { expectedTag, rootElement, id }), /* @__PURE__ */ React10.createElement(
3623
- import_editor_controls2.InlineEditor,
3609
+ import_editor_controls3.InlineEditor,
3624
3610
  {
3625
3611
  onEditorCreate: setEditor,
3626
3612
  mountElement: contentElement,
@@ -3672,7 +3658,7 @@ var InlineEditingToolbar = ({ anchor, editor, id }) => {
3672
3658
  pointerEvents: "none"
3673
3659
  }
3674
3660
  },
3675
- /* @__PURE__ */ React10.createElement(import_editor_controls2.InlineEditorToolbar, { editor, elementId: id })
3661
+ /* @__PURE__ */ React10.createElement(import_editor_controls3.InlineEditorToolbar, { editor, elementId: id })
3676
3662
  ));
3677
3663
  };
3678
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,78 +2087,56 @@ 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
- path: 4
2088
- };
2089
- 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";
2090
2093
  var iconTransformer = createTransformer(async (value, { signal }) => {
2091
2094
  const iconValue = typeof value.value === "string" ? value.value : null;
2092
2095
  const library = typeof value.library === "string" ? value.library : null;
2093
2096
  if (!iconValue || !library) {
2094
- return { html: null, url: null };
2097
+ return EMPTY_ICON_RESULT;
2095
2098
  }
2096
- const iconName = getFontAwesomeIconName(iconValue);
2097
- const jsonFileName = getFontAwesomeJsonFileName(library);
2098
- if (!iconName || !jsonFileName) {
2099
- return { html: null, url: null };
2099
+ const iconName = getFontAwesome7IconName(iconValue);
2100
+ if (!iconName) {
2101
+ return EMPTY_ICON_RESULT;
2100
2102
  }
2101
- const icons = await fetchFontAwesomeIcons(jsonFileName, signal);
2102
- const iconData = icons?.[iconName];
2103
+ const iconData = await resolveFontAwesome7Icon(library, iconName, signal);
2103
2104
  if (!iconData) {
2104
- return { html: null, url: null };
2105
+ return EMPTY_ICON_RESULT;
2105
2106
  }
2106
2107
  const svgText = buildFontAwesomeSvg(iconData);
2107
- const html = processSvgContent(svgText);
2108
+ if (!svgText) {
2109
+ return EMPTY_ICON_RESULT;
2110
+ }
2111
+ const html = processIconSvgContent(svgText);
2108
2112
  return { html, url: null };
2109
2113
  });
2110
- function getFontAwesomeIconName(iconValue) {
2111
- const match = iconValue.match(/^fa\S*\s+fa-(.+)$/);
2112
- return match?.[1] ?? null;
2113
- }
2114
- function getFontAwesomeJsonFileName(library) {
2115
- if (!library.startsWith("fa-")) {
2114
+ function buildFontAwesomeSvg(iconData) {
2115
+ if (iconData.paths.length === 0) {
2116
2116
  return null;
2117
2117
  }
2118
- return library.replace(/^fa-/, "");
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>`;
2119
2120
  }
2120
- function getAssetsBaseUrl() {
2121
- const assetsUrl = window.elementorCommon?.config?.urls?.assets;
2122
- return typeof assetsUrl === "string" && assetsUrl !== "" ? assetsUrl : null;
2121
+ function escapeSvgPath(path) {
2122
+ return path.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2123
2123
  }
2124
- async function fetchFontAwesomeIcons(jsonFileName, signal) {
2125
- const cached = fontAwesomeJsonCache.get(jsonFileName);
2126
- if (cached) {
2127
- return cached;
2128
- }
2129
- const icons = await loadFontAwesomeIcons(jsonFileName, signal);
2130
- if (icons) {
2131
- fontAwesomeJsonCache.set(jsonFileName, icons);
2132
- }
2133
- return icons;
2134
- }
2135
- async function loadFontAwesomeIcons(jsonFileName, signal) {
2136
- const assetsUrl = getAssetsBaseUrl();
2137
- if (!assetsUrl) {
2124
+ function processIconSvgContent(svgText) {
2125
+ const html = processSvgContent(svgText);
2126
+ if (!html) {
2138
2127
  return null;
2139
2128
  }
2140
- try {
2141
- const response = await fetch(`${assetsUrl}lib/font-awesome/json/${jsonFileName}.json`, { signal });
2142
- if (!response.ok) {
2143
- return null;
2144
- }
2145
- const data = await response.json();
2146
- return data.icons ?? null;
2147
- } catch {
2129
+ const parser = new DOMParser();
2130
+ const doc = parser.parseFromString(html, "image/svg+xml");
2131
+ const svgElement = doc.querySelector("svg");
2132
+ if (!svgElement) {
2148
2133
  return null;
2149
2134
  }
2150
- }
2151
- function buildFontAwesomeSvg(iconData) {
2152
- const width = iconData[FONT_AWESOME_JSON.width];
2153
- const height = iconData[FONT_AWESOME_JSON.height];
2154
- const path = iconData[FONT_AWESOME_JSON.path];
2155
- return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}"><path d="${path}"></path></svg>`;
2135
+ svgElement.setAttribute("aria-hidden", "true");
2136
+ svgElement.style.setProperty("width", ICON_SVG_SIZE);
2137
+ svgElement.style.setProperty("height", ICON_SVG_SIZE);
2138
+ svgElement.style.setProperty("overflow", ICON_SVG_OVERFLOW);
2139
+ return svgElement.outerHTML;
2156
2140
  }
2157
2141
 
2158
2142
  // src/transformers/shared/image-src-transformer.ts
@@ -2398,6 +2382,10 @@ var mapToFilterFunctionString = (value) => {
2398
2382
  };
2399
2383
 
2400
2384
  // src/transformers/styles/flex-transformer.ts
2385
+ var DEFAULT_FLEX_GROW = 0;
2386
+ var DEFAULT_FLEX_SHRINK = 1;
2387
+ var DEFAULT_FLEX_BASIS = "auto";
2388
+ var formatBasis = (basis) => typeof basis === "object" && basis.size !== void 0 ? `${basis.size}${basis.unit || ""}` : basis;
2401
2389
  var flexTransformer = createTransformer((value) => {
2402
2390
  const grow = value.flexGrow;
2403
2391
  const shrink = value.flexShrink;
@@ -2408,28 +2396,10 @@ var flexTransformer = createTransformer((value) => {
2408
2396
  if (!hasGrow && !hasShrink && !hasBasis) {
2409
2397
  return null;
2410
2398
  }
2411
- if (hasGrow && hasShrink && hasBasis) {
2412
- return `${grow} ${shrink} ${typeof basis === "object" && basis.size !== void 0 ? `${basis.size}${basis.unit || ""}` : basis}`;
2413
- }
2414
- if (hasGrow && hasShrink && !hasBasis) {
2415
- return `${grow} ${shrink}`;
2416
- }
2417
- if (hasGrow && !hasShrink && hasBasis) {
2418
- return `${grow} 1 ${typeof basis === "object" && basis.size !== void 0 ? `${basis.size}${basis.unit || ""}` : basis}`;
2419
- }
2420
- if (!hasGrow && hasShrink && hasBasis) {
2421
- return `0 ${shrink} ${typeof basis === "object" && basis.size !== void 0 ? `${basis.size}${basis.unit || ""}` : basis}`;
2422
- }
2423
- if (hasGrow && !hasShrink && !hasBasis) {
2424
- return `${grow}`;
2425
- }
2426
- if (!hasGrow && hasShrink && !hasBasis) {
2427
- return `0 ${shrink}`;
2428
- }
2429
- if (!hasGrow && !hasShrink && hasBasis) {
2430
- return `0 1 ${typeof basis === "object" && basis.size !== void 0 ? `${basis.size}${basis.unit || ""}` : basis}`;
2431
- }
2432
- return null;
2399
+ const growOut = hasGrow ? grow : DEFAULT_FLEX_GROW;
2400
+ const shrinkOut = hasShrink ? shrink : DEFAULT_FLEX_SHRINK;
2401
+ const basisOut = hasBasis ? formatBasis(basis) : DEFAULT_FLEX_BASIS;
2402
+ return `${growOut} ${shrinkOut} ${basisOut}`;
2433
2403
  });
2434
2404
 
2435
2405
  // src/transformers/styles/font-family-transformer.ts
@@ -3012,6 +2982,7 @@ function createTemplatedElementView({
3012
2982
  }
3013
2983
 
3014
2984
  // src/legacy/create-nested-templated-element-type.ts
2985
+ var STYLES_REFERENCE_UNTRACKED = /* @__PURE__ */ Symbol("styles-reference-untracked");
3015
2986
  function canBeNestedTemplated(element) {
3016
2987
  return canBeTemplated(element) && "support_nesting" in element && !!element.support_nesting;
3017
2988
  }
@@ -3071,6 +3042,7 @@ function createNestedTemplatedElementView({
3071
3042
  return AtomicElementBaseView.extend({
3072
3043
  _abortController: null,
3073
3044
  _lastResolvedSettingsHash: null,
3045
+ _lastRenderedStyles: STYLES_REFERENCE_UNTRACKED,
3074
3046
  _domUpdateWasSkipped: false,
3075
3047
  template: false,
3076
3048
  attributes() {
@@ -3107,6 +3079,23 @@ function createNestedTemplatedElementView({
3107
3079
  this._initAlpine();
3108
3080
  });
3109
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;
3110
3099
  window.dispatchEvent(new CustomEvent(ELEMENT_STYLE_CHANGE_EVENT2));
3111
3100
  },
3112
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-beta1",
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-beta1",
41
- "@elementor/editor-controls": "4.3.0-beta1",
42
- "@elementor/editor-documents": "4.3.0-beta1",
43
- "@elementor/editor-elements": "4.3.0-beta1",
44
- "@elementor/editor-interactions": "4.3.0-beta1",
45
- "@elementor/editor-mcp": "4.3.0-beta1",
46
- "@elementor/editor-notifications": "4.3.0-beta1",
47
- "@elementor/editor-props": "4.3.0-beta1",
48
- "@elementor/editor-responsive": "4.3.0-beta1",
49
- "@elementor/editor-styles": "4.3.0-beta1",
50
- "@elementor/editor-styles-repository": "4.3.0-beta1",
51
- "@elementor/editor-ui": "4.3.0-beta1",
52
- "@elementor/editor-v1-adapters": "4.3.0-beta1",
53
- "@elementor/events": "4.3.0-beta1",
54
- "@elementor/http-client": "4.3.0-beta1",
55
- "@elementor/schema": "4.3.0-beta1",
56
- "@elementor/twing": "4.3.0-beta1",
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-beta1",
59
- "@elementor/wp-media": "4.3.0-beta1",
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"
@@ -68,7 +68,7 @@ describe( 'flexTransformer', () => {
68
68
  } );
69
69
 
70
70
  describe( 'when only two values are provided', () => {
71
- it( 'should return "grow shrink" when grow and shrink are set', () => {
71
+ it( 'should return "grow shrink auto" when grow and shrink are set', () => {
72
72
  // Arrange.
73
73
  const value: Flex = {
74
74
  flexGrow: 1,
@@ -80,7 +80,7 @@ describe( 'flexTransformer', () => {
80
80
  const result = flexTransformer( value, { key: 'flex' } );
81
81
 
82
82
  // Assert.
83
- expect( result ).toBe( '1 2' );
83
+ expect( result ).toBe( '1 2 auto' );
84
84
  } );
85
85
 
86
86
  it( 'should return "grow 1 basis" when grow and basis are set', () => {
@@ -115,7 +115,7 @@ describe( 'flexTransformer', () => {
115
115
  } );
116
116
 
117
117
  describe( 'when only one value is provided', () => {
118
- it( 'should return "grow" when only grow is set', () => {
118
+ it( 'should return "grow 1 auto" when only grow is set', () => {
119
119
  // Arrange.
120
120
  const value: Flex = {
121
121
  flexGrow: 1,
@@ -127,10 +127,10 @@ describe( 'flexTransformer', () => {
127
127
  const result = flexTransformer( value, { key: 'flex' } );
128
128
 
129
129
  // Assert.
130
- expect( result ).toBe( '1' );
130
+ expect( result ).toBe( '1 1 auto' );
131
131
  } );
132
132
 
133
- it( 'should return "0 shrink" when only shrink is set', () => {
133
+ it( 'should return "0 shrink auto" when only shrink is set', () => {
134
134
  // Arrange.
135
135
  const value: Flex = {
136
136
  flexGrow: null,
@@ -142,7 +142,7 @@ describe( 'flexTransformer', () => {
142
142
  const result = flexTransformer( value, { key: 'flex' } );
143
143
 
144
144
  // Assert.
145
- expect( result ).toBe( '0 2' );
145
+ expect( result ).toBe( '0 2 auto' );
146
146
  } );
147
147
 
148
148
  it( 'should return "0 1 basis" when only basis is set', () => {
@@ -238,8 +238,8 @@ describe( 'flexTransformer', () => {
238
238
  } );
239
239
  } );
240
240
 
241
- describe( 'CSS shorthand behavior', () => {
242
- it( 'should follow CSS flex shorthand rules for single value', () => {
241
+ describe( 'CSS initial values for omitted longhands', () => {
242
+ it( 'should default shrink and basis when only grow is set', () => {
243
243
  // Arrange.
244
244
  const value: Flex = {
245
245
  flexGrow: 2,
@@ -251,10 +251,10 @@ describe( 'flexTransformer', () => {
251
251
  const result = flexTransformer( value, { key: 'flex' } );
252
252
 
253
253
  // Assert.
254
- expect( result ).toBe( '2' );
254
+ expect( result ).toBe( '2 1 auto' );
255
255
  } );
256
256
 
257
- it( 'should follow CSS flex shorthand rules for two values', () => {
257
+ it( 'should default basis when grow and shrink are set', () => {
258
258
  // Arrange.
259
259
  const value: Flex = {
260
260
  flexGrow: 1,
@@ -266,7 +266,7 @@ describe( 'flexTransformer', () => {
266
266
  const result = flexTransformer( value, { key: 'flex' } );
267
267
 
268
268
  // Assert.
269
- expect( result ).toBe( '1 2' );
269
+ expect( result ).toBe( '1 2 auto' );
270
270
  } );
271
271
  } );
272
272
  } );
@@ -36,6 +36,17 @@ export function linkPropType() {
36
36
  } );
37
37
  }
38
38
 
39
+ export function iconPropType() {
40
+ return createMockPropType( {
41
+ kind: 'object',
42
+ key: 'icon',
43
+ shape: {
44
+ value: stringPropType(),
45
+ library: stringPropType(),
46
+ },
47
+ } );
48
+ }
49
+
39
50
  export function imagePropType() {
40
51
  return createMockPropType( {
41
52
  kind: 'object',
@@ -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,7 +1,13 @@
1
- import { iconTransformer, resetFontAwesomeIconsCache } from '../icon-transformer';
1
+ import { resetFontAwesome7IconsCache } from '@elementor/editor-controls';
2
+ import { iconPropTypeUtil, stringPropTypeUtil } from '@elementor/editor-props';
3
+
4
+ import { iconPropType } from '../../../__tests__/prop-types';
5
+ import { initSettingsTransformers } from '../../../init-settings-transformers';
6
+ import { createPropsResolver } from '../../../renderers/create-props-resolver';
7
+ import { settingsTransformersRegistry } from '../../../settings-transformers-registry';
2
8
 
3
9
  const STAR_PATH = 'M0 0h100v100H0z';
4
- const STAR_WIDTH = 512;
10
+ const STAR_WIDTH = 576;
5
11
  const STAR_HEIGHT = 512;
6
12
 
7
13
  const mockFetch = ( body: unknown, ok = true ) => {
@@ -11,17 +17,46 @@ const mockFetch = ( body: unknown, ok = true ) => {
11
17
  } );
12
18
  };
13
19
 
20
+ function createSavedIcon( iconClass: string, library: string ) {
21
+ return iconPropTypeUtil.create( {
22
+ value: stringPropTypeUtil.create( iconClass ),
23
+ library: stringPropTypeUtil.create( library ),
24
+ } );
25
+ }
26
+
27
+ async function resolveSavedIcon( iconClass: string, library: string ) {
28
+ const resolve = createPropsResolver( {
29
+ transformers: settingsTransformersRegistry,
30
+ schema: { svg: iconPropType() },
31
+ } );
32
+
33
+ const result = await resolve( {
34
+ props: {
35
+ svg: createSavedIcon( iconClass, library ),
36
+ },
37
+ } );
38
+
39
+ return result.svg;
40
+ }
41
+
14
42
  describe( 'iconTransformer', () => {
15
43
  const originalElementorCommon = window.elementorCommon;
16
44
 
17
45
  beforeEach( () => {
18
46
  jest.clearAllMocks();
19
- resetFontAwesomeIconsCache();
47
+ resetFontAwesome7IconsCache();
48
+ initSettingsTransformers();
20
49
  window.elementorCommon = {
21
50
  config: {
22
51
  urls: {
23
52
  assets: 'https://example.com/assets/',
24
53
  },
54
+ fontAwesome: {
55
+ v7: {
56
+ jsonFiles: [ 'solid', 'regular', 'brands' ],
57
+ jsonBaseUrl: 'https://example.com/assets/lib/font-awesome-7/json/',
58
+ },
59
+ },
25
60
  },
26
61
  };
27
62
  } );
@@ -40,10 +75,10 @@ describe( 'iconTransformer', () => {
40
75
  } );
41
76
 
42
77
  // Act.
43
- const result = await iconTransformer( { value: 'fas fa-star', library: 'fa-solid' }, { key: 'svg' } );
78
+ const result = await resolveSavedIcon( 'fas fa-star', 'fa-solid' );
44
79
 
45
80
  // Assert.
46
- expect( global.fetch ).toHaveBeenCalledWith( 'https://example.com/assets/lib/font-awesome/json/solid.json', {
81
+ expect( global.fetch ).toHaveBeenCalledWith( 'https://example.com/assets/lib/font-awesome-7/json/solid.json', {
47
82
  signal: undefined,
48
83
  } );
49
84
  expect( result ).toEqual( {
@@ -51,6 +86,45 @@ describe( 'iconTransformer', () => {
51
86
  url: null,
52
87
  } );
53
88
  expect( ( result as { html: string } ).html ).toContain( 'fill="currentColor"' );
89
+ expect( ( result as { html: string } ).html ).toContain( `viewBox="0 0 ${ STAR_WIDTH } ${ STAR_HEIGHT }"` );
90
+ expect( ( result as { html: string } ).html ).toContain( 'aria-hidden="true"' );
91
+ expect( ( result as { html: string } ).html ).toContain( 'overflow: visible' );
92
+ } );
93
+
94
+ it( 'resolves icons by alias name', async () => {
95
+ // Arrange.
96
+ mockFetch( {
97
+ icons: {
98
+ headphones: [ 448, 512, [ 'headphones-simple' ], 'f025', 'M0 0' ],
99
+ },
100
+ } );
101
+
102
+ // Act.
103
+ const result = await resolveSavedIcon( 'fas fa-headphones-simple', 'fa-solid' );
104
+
105
+ // Assert.
106
+ expect( result ).toEqual( {
107
+ html: expect.stringContaining( 'M0 0' ),
108
+ url: null,
109
+ } );
110
+ } );
111
+
112
+ it( 'renders multiple paths when the icon definition contains an array', async () => {
113
+ // Arrange.
114
+ mockFetch( {
115
+ icons: {
116
+ slash: [ 640, 640, [], 'f715', [ 'M0 0', 'M10 10' ] ],
117
+ },
118
+ } );
119
+
120
+ // Act.
121
+ const result = await resolveSavedIcon( 'fas fa-slash', 'fa-solid' );
122
+
123
+ // Assert.
124
+ const html = ( result as { html: string } ).html;
125
+ expect( html.match( /<path/g ) ).toHaveLength( 2 );
126
+ expect( html ).toContain( 'M0 0' );
127
+ expect( html ).toContain( 'M10 10' );
54
128
  } );
55
129
 
56
130
  it( 'returns null html when the icon cannot be resolved', async () => {
@@ -58,17 +132,68 @@ describe( 'iconTransformer', () => {
58
132
  mockFetch( { icons: {} } );
59
133
 
60
134
  // Act.
61
- const result = await iconTransformer( { value: 'fas fa-missing', library: 'fa-solid' }, { key: 'svg' } );
135
+ const result = await resolveSavedIcon( 'fas fa-missing', 'fa-solid' );
62
136
 
63
137
  // Assert.
64
138
  expect( result ).toEqual( { html: null, url: null } );
65
139
  } );
66
140
 
67
- it( 'returns null html when value or library is missing', async () => {
141
+ it( 'fetches json from the localized font awesome 7 base url', async () => {
142
+ // Arrange.
143
+ const jsonBaseUrl = 'https://cdn.example.com/fa7/json/';
144
+ window.elementorCommon = {
145
+ config: {
146
+ fontAwesome: {
147
+ v7: {
148
+ jsonFiles: [ 'solid', 'regular', 'brands' ],
149
+ jsonBaseUrl,
150
+ },
151
+ },
152
+ },
153
+ };
154
+ mockFetch( {
155
+ icons: {
156
+ star: [ STAR_WIDTH, STAR_HEIGHT, [], 'f005', STAR_PATH ],
157
+ },
158
+ } );
159
+
68
160
  // Act.
69
- const result = await iconTransformer( { value: 'fas fa-star' }, { key: 'svg' } );
161
+ await resolveSavedIcon( 'fas fa-star', 'fa-solid' );
70
162
 
71
163
  // Assert.
164
+ expect( global.fetch ).toHaveBeenCalledWith( `${ jsonBaseUrl }solid.json`, {
165
+ signal: undefined,
166
+ } );
167
+ } );
168
+
169
+ it( 'does not fetch json when library is not an allowed fa7 file', async () => {
170
+ // Arrange.
171
+ mockFetch( { icons: {} } );
172
+
173
+ // Act.
174
+ const result = await resolveSavedIcon( 'fas fa-star', 'fa-../../wp-config' );
175
+
176
+ // Assert.
177
+ expect( global.fetch ).not.toHaveBeenCalled();
72
178
  expect( result ).toEqual( { html: null, url: null } );
73
179
  } );
180
+
181
+ it( 'returns null html when value or library is missing', async () => {
182
+ // Act.
183
+ const resolve = createPropsResolver( {
184
+ transformers: settingsTransformersRegistry,
185
+ schema: { svg: iconPropType() },
186
+ } );
187
+ const result = await resolve( {
188
+ props: {
189
+ svg: iconPropTypeUtil.create( {
190
+ value: stringPropTypeUtil.create( 'fas fa-star' ),
191
+ library: stringPropTypeUtil.create( '' ),
192
+ } ),
193
+ },
194
+ } );
195
+
196
+ // Assert.
197
+ expect( result.svg ).toEqual( { html: null, url: null } );
198
+ } );
74
199
  } );
@@ -1,122 +1,86 @@
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 ];
11
-
12
- const FONT_AWESOME_JSON = {
13
- width: 0,
14
- height: 1,
15
- path: 4,
16
- } as const;
17
-
18
- const fontAwesomeJsonCache = new Map< string, Record< string, FontAwesomeIconJson > >();
19
-
20
20
  export const iconTransformer = createTransformer( async ( value: IconValue, { signal }: TransformerOptions ) => {
21
21
  const iconValue = typeof value.value === 'string' ? value.value : null;
22
22
  const library = typeof value.library === 'string' ? value.library : null;
23
23
 
24
24
  if ( ! iconValue || ! library ) {
25
- return { html: null, url: null };
25
+ return EMPTY_ICON_RESULT;
26
26
  }
27
27
 
28
- const iconName = getFontAwesomeIconName( iconValue );
29
- const jsonFileName = getFontAwesomeJsonFileName( library );
28
+ const iconName = getFontAwesome7IconName( iconValue );
30
29
 
31
- if ( ! iconName || ! jsonFileName ) {
32
- return { html: null, url: null };
30
+ if ( ! iconName ) {
31
+ return EMPTY_ICON_RESULT;
33
32
  }
34
33
 
35
- const icons = await fetchFontAwesomeIcons( jsonFileName, signal );
36
- const iconData = icons?.[ iconName ];
34
+ const iconData = await resolveFontAwesome7Icon( library, iconName, signal );
37
35
 
38
36
  if ( ! iconData ) {
39
- return { html: null, url: null };
37
+ return EMPTY_ICON_RESULT;
40
38
  }
41
39
 
42
40
  const svgText = buildFontAwesomeSvg( iconData );
43
- const html = processSvgContent( svgText );
44
41
 
45
- return { html, url: null };
46
- } );
42
+ if ( ! svgText ) {
43
+ return EMPTY_ICON_RESULT;
44
+ }
47
45
 
48
- function getFontAwesomeIconName( iconValue: string ): string | null {
49
- const match = iconValue.match( /^fa\S*\s+fa-(.+)$/ );
46
+ const html = processIconSvgContent( svgText );
50
47
 
51
- return match?.[ 1 ] ?? null;
52
- }
48
+ return { html, url: null };
49
+ } );
53
50
 
54
- function getFontAwesomeJsonFileName( library: string ): string | null {
55
- if ( ! library.startsWith( 'fa-' ) ) {
51
+ function buildFontAwesomeSvg( iconData: FontAwesome7IconDefinition ): string | null {
52
+ if ( iconData.paths.length === 0 ) {
56
53
  return null;
57
54
  }
58
55
 
59
- return library.replace( /^fa-/, '' );
60
- }
61
-
62
- function getAssetsBaseUrl(): string | null {
63
- const assetsUrl = window.elementorCommon?.config?.urls?.assets;
56
+ const pathMarkup = iconData.paths.map( ( path ) => `<path d="${ escapeSvgPath( path ) }"></path>` ).join( '' );
64
57
 
65
- return typeof assetsUrl === 'string' && assetsUrl !== '' ? assetsUrl : null;
58
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${ iconData.width } ${ iconData.height }">${ pathMarkup }</svg>`;
66
59
  }
67
60
 
68
- async function fetchFontAwesomeIcons(
69
- jsonFileName: string,
70
- signal?: AbortSignal
71
- ): Promise< Record< string, FontAwesomeIconJson > | null > {
72
- const cached = fontAwesomeJsonCache.get( jsonFileName );
73
-
74
- if ( cached ) {
75
- return cached;
76
- }
77
-
78
- const icons = await loadFontAwesomeIcons( jsonFileName, signal );
79
-
80
- if ( icons ) {
81
- fontAwesomeJsonCache.set( jsonFileName, icons );
82
- }
83
-
84
- return icons;
61
+ function escapeSvgPath( path: string ): string {
62
+ return path.replace( /&/g, '&amp;' ).replace( /"/g, '&quot;' ).replace( /</g, '&lt;' ).replace( />/g, '&gt;' );
85
63
  }
86
64
 
87
- async function loadFontAwesomeIcons(
88
- jsonFileName: string,
89
- signal?: AbortSignal
90
- ): Promise< Record< string, FontAwesomeIconJson > | null > {
91
- const assetsUrl = getAssetsBaseUrl();
65
+ function processIconSvgContent( svgText: string ): string | null {
66
+ const html = processSvgContent( svgText );
92
67
 
93
- if ( ! assetsUrl ) {
68
+ if ( ! html ) {
94
69
  return null;
95
70
  }
96
71
 
97
- try {
98
- const response = await fetch( `${ assetsUrl }lib/font-awesome/json/${ jsonFileName }.json`, { signal } );
99
-
100
- if ( ! response.ok ) {
101
- return null;
102
- }
103
-
104
- const data = ( await response.json() ) as { icons?: Record< string, FontAwesomeIconJson > };
72
+ const parser = new DOMParser();
73
+ const doc = parser.parseFromString( html, 'image/svg+xml' );
74
+ const svgElement = doc.querySelector( 'svg' );
105
75
 
106
- return data.icons ?? null;
107
- } catch {
76
+ if ( ! svgElement ) {
108
77
  return null;
109
78
  }
110
- }
111
-
112
- function buildFontAwesomeSvg( iconData: FontAwesomeIconJson ): string {
113
- const width = iconData[ FONT_AWESOME_JSON.width ];
114
- const height = iconData[ FONT_AWESOME_JSON.height ];
115
- const path = iconData[ FONT_AWESOME_JSON.path ];
116
79
 
117
- return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${ width } ${ height }"><path d="${ path }"></path></svg>`;
118
- }
80
+ svgElement.setAttribute( 'aria-hidden', 'true' );
81
+ svgElement.style.setProperty( 'width', ICON_SVG_SIZE );
82
+ svgElement.style.setProperty( 'height', ICON_SVG_SIZE );
83
+ svgElement.style.setProperty( 'overflow', ICON_SVG_OVERFLOW );
119
84
 
120
- export function resetFontAwesomeIconsCache() {
121
- fontAwesomeJsonCache.clear();
85
+ return svgElement.outerHTML;
122
86
  }
@@ -6,6 +6,13 @@ type Flex = {
6
6
  flexBasis?: { size: number; unit: string } | string | null;
7
7
  };
8
8
 
9
+ const DEFAULT_FLEX_GROW = 0;
10
+ const DEFAULT_FLEX_SHRINK = 1;
11
+ const DEFAULT_FLEX_BASIS = 'auto';
12
+
13
+ const formatBasis = ( basis: NonNullable< Flex[ 'flexBasis' ] > ) =>
14
+ typeof basis === 'object' && basis.size !== undefined ? `${ basis.size }${ basis.unit || '' }` : basis;
15
+
9
16
  export const flexTransformer = createTransformer( ( value: Flex ) => {
10
17
  const grow = value.flexGrow;
11
18
  const shrink = value.flexShrink;
@@ -19,37 +26,9 @@ export const flexTransformer = createTransformer( ( value: Flex ) => {
19
26
  return null;
20
27
  }
21
28
 
22
- if ( hasGrow && hasShrink && hasBasis ) {
23
- return `${ grow } ${ shrink } ${
24
- typeof basis === 'object' && basis.size !== undefined ? `${ basis.size }${ basis.unit || '' }` : basis
25
- }`;
26
- }
27
-
28
- if ( hasGrow && hasShrink && ! hasBasis ) {
29
- return `${ grow } ${ shrink }`;
30
- }
31
- if ( hasGrow && ! hasShrink && hasBasis ) {
32
- return `${ grow } 1 ${
33
- typeof basis === 'object' && basis.size !== undefined ? `${ basis.size }${ basis.unit || '' }` : basis
34
- }`;
35
- }
36
- if ( ! hasGrow && hasShrink && hasBasis ) {
37
- return `0 ${ shrink } ${
38
- typeof basis === 'object' && basis.size !== undefined ? `${ basis.size }${ basis.unit || '' }` : basis
39
- }`;
40
- }
41
-
42
- if ( hasGrow && ! hasShrink && ! hasBasis ) {
43
- return `${ grow }`;
44
- }
45
- if ( ! hasGrow && hasShrink && ! hasBasis ) {
46
- return `0 ${ shrink }`;
47
- }
48
- if ( ! hasGrow && ! hasShrink && hasBasis ) {
49
- return `0 1 ${
50
- typeof basis === 'object' && basis.size !== undefined ? `${ basis.size }${ basis.unit || '' }` : basis
51
- }`;
52
- }
29
+ const growOut = hasGrow ? grow : DEFAULT_FLEX_GROW;
30
+ const shrinkOut = hasShrink ? shrink : DEFAULT_FLEX_SHRINK;
31
+ const basisOut = hasBasis ? formatBasis( basis ) : DEFAULT_FLEX_BASIS;
53
32
 
54
- return null;
33
+ return `${ growOut } ${ shrinkOut } ${ basisOut }`;
55
34
  } );