@elementor/editor-canvas 4.1.0-697 → 4.1.0-699

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
@@ -164,6 +164,7 @@ type BackboneCollection<Model extends object> = {
164
164
  };
165
165
  type ElementModel = {
166
166
  id: string;
167
+ originId?: string;
167
168
  elType: string;
168
169
  settings: BackboneModel<Props>;
169
170
  editor_settings: Record<string, unknown>;
package/dist/index.d.ts CHANGED
@@ -164,6 +164,7 @@ type BackboneCollection<Model extends object> = {
164
164
  };
165
165
  type ElementModel = {
166
166
  id: string;
167
+ originId?: string;
167
168
  elType: string;
168
169
  settings: BackboneModel<Props>;
169
170
  editor_settings: Record<string, unknown>;
package/dist/index.js CHANGED
@@ -833,14 +833,22 @@ var UnknownStyleStateError = (0, import_utils2.createError)({
833
833
  var SELECTORS_MAP = {
834
834
  class: "."
835
835
  };
836
+ var DEFAULT_BREAKPOINT = "desktop";
837
+ var DEFAULT_STATE = "normal";
838
+ function getStyleUniqueKey(style) {
839
+ const breakpoint = style.variants[0]?.meta?.breakpoint ?? DEFAULT_BREAKPOINT;
840
+ const state = style.variants[0]?.meta?.state ?? DEFAULT_STATE;
841
+ return `${style.id}-${breakpoint}-${state}`;
842
+ }
836
843
  function createStylesRenderer({ resolve, breakpoints, selectorPrefix = "" }) {
837
844
  return async ({ styles, signal }) => {
838
- const seenIds = /* @__PURE__ */ new Set();
845
+ const seenKeys = /* @__PURE__ */ new Set();
839
846
  const uniqueStyles = styles.filter((style) => {
840
- if (seenIds.has(style.id)) {
847
+ const key = getStyleUniqueKey(style);
848
+ if (seenKeys.has(key)) {
841
849
  return false;
842
850
  }
843
- seenIds.add(style.id);
851
+ seenKeys.add(key);
844
852
  return true;
845
853
  });
846
854
  const stylesCssPromises = uniqueStyles.map(async (style) => {
@@ -1025,7 +1033,7 @@ function createProviderSubscriber2({ provider, renderStyles, setStyleItems, cach
1025
1033
  });
1026
1034
  return renderStyles({ styles: breakToBreakpoints(styles), signal }).then((rendered) => {
1027
1035
  rebuildCache(cache, allStyles, rendered);
1028
- return rendered;
1036
+ return getOrderedItems(cache);
1029
1037
  });
1030
1038
  }
1031
1039
  function breakToBreakpoints(styles) {
@@ -2008,6 +2016,7 @@ function createTemplatedElementView({
2008
2016
  this._lastResolvedSettingsHash = settingsHash;
2009
2017
  const context = {
2010
2018
  id: this.model.get("id"),
2019
+ interaction_id: this.getInteractionId(),
2011
2020
  type,
2012
2021
  settings,
2013
2022
  base_styles: baseStylesDictionary
@@ -2042,6 +2051,11 @@ function createTemplatedElementView({
2042
2051
  _openEditingPanel(options) {
2043
2052
  this._doAfterRender(() => super._openEditingPanel(options));
2044
2053
  }
2054
+ getInteractionId() {
2055
+ const originId = this.model.get("originId");
2056
+ const id = this.model.get("id");
2057
+ return originId ?? id;
2058
+ }
2045
2059
  };
2046
2060
  }
2047
2061
 
@@ -2074,10 +2088,11 @@ function createNestedTemplatedElementType({
2074
2088
  }
2075
2089
  function buildEditorAttributes(model) {
2076
2090
  const id = model.get("id");
2091
+ const originId = model.get("originId");
2077
2092
  const cid = model.cid ?? "";
2078
2093
  const attrs = {
2079
2094
  "data-model-cid": cid,
2080
- "data-interaction-id": id,
2095
+ "data-interaction-id": originId ?? id,
2081
2096
  "x-ignore": "true"
2082
2097
  };
2083
2098
  return Object.entries(attrs).map(([key, value]) => `${key}="${value}"`).join(" ");
@@ -2154,6 +2169,7 @@ function createNestedTemplatedElementView({
2154
2169
  this._lastResolvedSettingsHash = settingsHash;
2155
2170
  const context = {
2156
2171
  id: model.get("id"),
2172
+ interaction_id: this.getInteractionId(),
2157
2173
  type,
2158
2174
  settings,
2159
2175
  base_styles: baseStylesDictionary,
@@ -2283,6 +2299,11 @@ function createNestedTemplatedElementView({
2283
2299
  },
2284
2300
  _openEditingPanel(options) {
2285
2301
  this._doAfterRender(() => parentOpenEditingPanel.call(this, options));
2302
+ },
2303
+ getInteractionId() {
2304
+ const originId = this.model.get("originId");
2305
+ const id = this.model.get("id");
2306
+ return originId ?? id;
2286
2307
  }
2287
2308
  });
2288
2309
  }
package/dist/index.mjs CHANGED
@@ -799,14 +799,22 @@ var UnknownStyleStateError = createError({
799
799
  var SELECTORS_MAP = {
800
800
  class: "."
801
801
  };
802
+ var DEFAULT_BREAKPOINT = "desktop";
803
+ var DEFAULT_STATE = "normal";
804
+ function getStyleUniqueKey(style) {
805
+ const breakpoint = style.variants[0]?.meta?.breakpoint ?? DEFAULT_BREAKPOINT;
806
+ const state = style.variants[0]?.meta?.state ?? DEFAULT_STATE;
807
+ return `${style.id}-${breakpoint}-${state}`;
808
+ }
802
809
  function createStylesRenderer({ resolve, breakpoints, selectorPrefix = "" }) {
803
810
  return async ({ styles, signal }) => {
804
- const seenIds = /* @__PURE__ */ new Set();
811
+ const seenKeys = /* @__PURE__ */ new Set();
805
812
  const uniqueStyles = styles.filter((style) => {
806
- if (seenIds.has(style.id)) {
813
+ const key = getStyleUniqueKey(style);
814
+ if (seenKeys.has(key)) {
807
815
  return false;
808
816
  }
809
- seenIds.add(style.id);
817
+ seenKeys.add(key);
810
818
  return true;
811
819
  });
812
820
  const stylesCssPromises = uniqueStyles.map(async (style) => {
@@ -991,7 +999,7 @@ function createProviderSubscriber2({ provider, renderStyles, setStyleItems, cach
991
999
  });
992
1000
  return renderStyles({ styles: breakToBreakpoints(styles), signal }).then((rendered) => {
993
1001
  rebuildCache(cache, allStyles, rendered);
994
- return rendered;
1002
+ return getOrderedItems(cache);
995
1003
  });
996
1004
  }
997
1005
  function breakToBreakpoints(styles) {
@@ -1974,6 +1982,7 @@ function createTemplatedElementView({
1974
1982
  this._lastResolvedSettingsHash = settingsHash;
1975
1983
  const context = {
1976
1984
  id: this.model.get("id"),
1985
+ interaction_id: this.getInteractionId(),
1977
1986
  type,
1978
1987
  settings,
1979
1988
  base_styles: baseStylesDictionary
@@ -2008,6 +2017,11 @@ function createTemplatedElementView({
2008
2017
  _openEditingPanel(options) {
2009
2018
  this._doAfterRender(() => super._openEditingPanel(options));
2010
2019
  }
2020
+ getInteractionId() {
2021
+ const originId = this.model.get("originId");
2022
+ const id = this.model.get("id");
2023
+ return originId ?? id;
2024
+ }
2011
2025
  };
2012
2026
  }
2013
2027
 
@@ -2040,10 +2054,11 @@ function createNestedTemplatedElementType({
2040
2054
  }
2041
2055
  function buildEditorAttributes(model) {
2042
2056
  const id = model.get("id");
2057
+ const originId = model.get("originId");
2043
2058
  const cid = model.cid ?? "";
2044
2059
  const attrs = {
2045
2060
  "data-model-cid": cid,
2046
- "data-interaction-id": id,
2061
+ "data-interaction-id": originId ?? id,
2047
2062
  "x-ignore": "true"
2048
2063
  };
2049
2064
  return Object.entries(attrs).map(([key, value]) => `${key}="${value}"`).join(" ");
@@ -2120,6 +2135,7 @@ function createNestedTemplatedElementView({
2120
2135
  this._lastResolvedSettingsHash = settingsHash;
2121
2136
  const context = {
2122
2137
  id: model.get("id"),
2138
+ interaction_id: this.getInteractionId(),
2123
2139
  type,
2124
2140
  settings,
2125
2141
  base_styles: baseStylesDictionary,
@@ -2249,6 +2265,11 @@ function createNestedTemplatedElementView({
2249
2265
  },
2250
2266
  _openEditingPanel(options) {
2251
2267
  this._doAfterRender(() => parentOpenEditingPanel.call(this, options));
2268
+ },
2269
+ getInteractionId() {
2270
+ const originId = this.model.get("originId");
2271
+ const id = this.model.get("id");
2272
+ return originId ?? id;
2252
2273
  }
2253
2274
  });
2254
2275
  }
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.1.0-697",
4
+ "version": "4.1.0-699",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -37,24 +37,24 @@
37
37
  "react-dom": "^18.3.1"
38
38
  },
39
39
  "dependencies": {
40
- "@elementor/editor": "4.1.0-697",
41
- "@elementor/editor-controls": "4.1.0-697",
42
- "@elementor/editor-documents": "4.1.0-697",
43
- "@elementor/editor-elements": "4.1.0-697",
44
- "@elementor/editor-interactions": "4.1.0-697",
45
- "@elementor/editor-mcp": "4.1.0-697",
46
- "@elementor/editor-notifications": "4.1.0-697",
47
- "@elementor/editor-props": "4.1.0-697",
48
- "@elementor/editor-responsive": "4.1.0-697",
49
- "@elementor/editor-styles": "4.1.0-697",
50
- "@elementor/editor-styles-repository": "4.1.0-697",
51
- "@elementor/editor-ui": "4.1.0-697",
52
- "@elementor/editor-v1-adapters": "4.1.0-697",
53
- "@elementor/schema": "4.1.0-697",
54
- "@elementor/twing": "4.1.0-697",
40
+ "@elementor/editor": "4.1.0-699",
41
+ "@elementor/editor-controls": "4.1.0-699",
42
+ "@elementor/editor-documents": "4.1.0-699",
43
+ "@elementor/editor-elements": "4.1.0-699",
44
+ "@elementor/editor-interactions": "4.1.0-699",
45
+ "@elementor/editor-mcp": "4.1.0-699",
46
+ "@elementor/editor-notifications": "4.1.0-699",
47
+ "@elementor/editor-props": "4.1.0-699",
48
+ "@elementor/editor-responsive": "4.1.0-699",
49
+ "@elementor/editor-styles": "4.1.0-699",
50
+ "@elementor/editor-styles-repository": "4.1.0-699",
51
+ "@elementor/editor-ui": "4.1.0-699",
52
+ "@elementor/editor-v1-adapters": "4.1.0-699",
53
+ "@elementor/schema": "4.1.0-699",
54
+ "@elementor/twing": "4.1.0-699",
55
55
  "@elementor/ui": "1.36.17",
56
- "@elementor/utils": "4.1.0-697",
57
- "@elementor/wp-media": "4.1.0-697",
56
+ "@elementor/utils": "4.1.0-699",
57
+ "@elementor/wp-media": "4.1.0-699",
58
58
  "@floating-ui/react": "^0.27.5",
59
59
  "@wordpress/i18n": "^5.13.0"
60
60
  },
@@ -289,6 +289,57 @@ describe( 'useStyleItems', () => {
289
289
  expect( result.current ).toHaveLength( 2 );
290
290
  } );
291
291
 
292
+ it( 'should maintain breakpoint order after style update', async () => {
293
+ // Arrange.
294
+ const renderStylesMock = jest.fn().mockImplementation( ( { styles } ) =>
295
+ Promise.resolve(
296
+ styles.map( ( style: StyleDefinition ) => ( {
297
+ id: style.id,
298
+ breakpoint: style?.variants[ 0 ]?.meta.breakpoint || 'desktop',
299
+ } ) )
300
+ )
301
+ );
302
+
303
+ jest.mocked( useStyleRenderer ).mockReturnValue( renderStylesMock );
304
+
305
+ const mockProvider = createMockStylesProvider( { key: 'provider1', priority: 1 }, [
306
+ createMockStyleDefinitionWithVariants( {
307
+ id: 'style1',
308
+ variants: [
309
+ { meta: { breakpoint: null, state: null }, props: { padding: '10px' }, custom_css: null },
310
+ { meta: { breakpoint: 'tablet', state: null }, props: { padding: '8px' }, custom_css: null },
311
+ { meta: { breakpoint: 'mobile', state: null }, props: { padding: '5px' }, custom_css: null },
312
+ ],
313
+ } ),
314
+ ] );
315
+
316
+ jest.mocked( stylesRepository ).getProviders.mockReturnValue( [ mockProvider ] );
317
+
318
+ // Act - initial render.
319
+ const { result } = renderHook( () => useStyleItems() );
320
+
321
+ await act( async () => {
322
+ mockProvider.actions.updateProps?.( {
323
+ id: 'style1',
324
+ meta: { breakpoint: 'tablet', state: null },
325
+ props: { padding: '12px' },
326
+ } );
327
+ } );
328
+
329
+ // Assert - items should be ordered by breakpoint (desktop, tablet, mobile).
330
+ const breakpointOrder = result.current.map( ( item ) => item.breakpoint );
331
+ expect( breakpointOrder ).toEqual( [ 'desktop', 'tablet', 'mobile' ] );
332
+
333
+ // Act - update again (should maintain order).
334
+ await act( async () => {
335
+ mockProvider.actions.update?.( { id: 'style1', label: 'Updated' } );
336
+ } );
337
+
338
+ // Assert - order should still be maintained.
339
+ const breakpointOrderAfterUpdate = result.current.map( ( item ) => item.breakpoint );
340
+ expect( breakpointOrderAfterUpdate ).toEqual( [ 'desktop', 'tablet', 'mobile' ] );
341
+ } );
342
+
292
343
  it( 'should only re-render changed styles on differential update', async () => {
293
344
  // Arrange.
294
345
  const renderStylesMock = jest.fn().mockImplementation( ( { styles } ) =>
@@ -191,7 +191,7 @@ function createProviderSubscriber( { provider, renderStyles, setStyleItems, cach
191
191
  return renderStyles( { styles: breakToBreakpoints( styles ), signal } ).then( ( rendered ) => {
192
192
  rebuildCache( cache, allStyles, rendered );
193
193
 
194
- return rendered;
194
+ return getOrderedItems( cache );
195
195
  } );
196
196
  }
197
197
 
@@ -61,13 +61,14 @@ export function createNestedTemplatedElementType( {
61
61
  };
62
62
  }
63
63
 
64
- function buildEditorAttributes( model: { get: ( key: 'id' ) => string; cid?: string } ): string {
64
+ function buildEditorAttributes( model: ElementView[ 'model' ] ): string {
65
65
  const id = model.get( 'id' );
66
+ const originId = model.get( 'originId' );
66
67
  const cid = model.cid ?? '';
67
68
 
68
69
  const attrs: Record< string, string > = {
69
70
  'data-model-cid': cid,
70
- 'data-interaction-id': id,
71
+ 'data-interaction-id': originId ?? id,
71
72
  'x-ignore': 'true',
72
73
  };
73
74
 
@@ -181,6 +182,7 @@ export function createNestedTemplatedElementView( {
181
182
 
182
183
  const context = {
183
184
  id: model.get( 'id' ),
185
+ interaction_id: this.getInteractionId(),
184
186
  type,
185
187
  settings,
186
188
  base_styles: baseStylesDictionary,
@@ -361,5 +363,12 @@ export function createNestedTemplatedElementView( {
361
363
  _openEditingPanel( options?: { scrollIntoView: boolean } ) {
362
364
  this._doAfterRender( () => parentOpenEditingPanel.call( this, options ) );
363
365
  },
366
+
367
+ getInteractionId() {
368
+ const originId = this.model.get( 'originId' );
369
+ const id = this.model.get( 'id' );
370
+
371
+ return originId ?? id;
372
+ },
364
373
  } ) as unknown as typeof ElementView;
365
374
  }
@@ -162,6 +162,7 @@ export function createTemplatedElementView( {
162
162
 
163
163
  const context = {
164
164
  id: this.model.get( 'id' ),
165
+ interaction_id: this.getInteractionId(),
165
166
  type,
166
167
  settings,
167
168
  base_styles: baseStylesDictionary,
@@ -206,5 +207,12 @@ export function createTemplatedElementView( {
206
207
  _openEditingPanel( options?: { scrollIntoView: boolean } ) {
207
208
  this._doAfterRender( () => super._openEditingPanel( options ) );
208
209
  }
210
+
211
+ getInteractionId() {
212
+ const originId = this.model.get( 'originId' );
213
+ const id = this.model.get( 'id' );
214
+
215
+ return originId ?? id;
216
+ }
209
217
  };
210
218
  }
@@ -203,6 +203,7 @@ type BackboneCollection< Model extends object > = {
203
203
 
204
204
  export type ElementModel = {
205
205
  id: string;
206
+ originId?: string;
206
207
  elType: string;
207
208
  settings: BackboneModel< Props >;
208
209
  editor_settings: Record< string, unknown >;
@@ -118,6 +118,123 @@ describe( 'renderStyles', () => {
118
118
  } );
119
119
  } );
120
120
 
121
+ describe( 'breakpoint deduplication', () => {
122
+ it( 'should render all breakpoints when same id has multiple breakpoint variants', async () => {
123
+ // Arrange - simulates output from breakToBreakpoints in use-style-items.
124
+ const desktopStyle: RendererStyleDefinition = {
125
+ id: 'button-style',
126
+ type: 'class',
127
+ cssName: 'e-button',
128
+ label: 'Button',
129
+ variants: [ { meta: { breakpoint: null, state: null }, props: { 'font-size': '16px' }, custom_css: null } ],
130
+ };
131
+ const tabletStyle: RendererStyleDefinition = {
132
+ ...desktopStyle,
133
+ variants: [
134
+ { meta: { breakpoint: 'tablet', state: null }, props: { 'font-size': '14px' }, custom_css: null },
135
+ ],
136
+ };
137
+ const mobileStyle: RendererStyleDefinition = {
138
+ ...desktopStyle,
139
+ variants: [
140
+ { meta: { breakpoint: 'mobile', state: null }, props: { 'font-size': '12px' }, custom_css: null },
141
+ ],
142
+ };
143
+
144
+ const resolve = jest.fn( ( { props } ) => props );
145
+ const renderStyles = createStylesRenderer( {
146
+ breakpoints: {
147
+ tablet: { width: 992, type: 'max-width' },
148
+ mobile: { width: 768, type: 'max-width' },
149
+ } as BreakpointsMap,
150
+ resolve,
151
+ } );
152
+
153
+ // Act.
154
+ const result = await renderStyles( { styles: [ desktopStyle, tabletStyle, mobileStyle ] } );
155
+
156
+ // Assert - all three breakpoints must be rendered (previously tablet/mobile were dropped).
157
+ expect( result ).toHaveLength( 3 );
158
+ expect( result.map( ( r ) => r.breakpoint ) ).toEqual( [ 'desktop', 'tablet', 'mobile' ] );
159
+ expect( result[ 0 ].value ).toContain( 'font-size:16px' );
160
+ expect( result[ 1 ].value ).toContain( '@media(max-width:992px)' );
161
+ expect( result[ 1 ].value ).toContain( 'font-size:14px' );
162
+ expect( result[ 2 ].value ).toContain( '@media(max-width:768px)' );
163
+ expect( result[ 2 ].value ).toContain( 'font-size:12px' );
164
+ } );
165
+
166
+ it( 'should deduplicate same id + breakpoint + state combinations', async () => {
167
+ // Arrange - two styles with same id, breakpoint, and state should dedupe.
168
+ const style1: RendererStyleDefinition = {
169
+ id: 'button-style',
170
+ type: 'class',
171
+ cssName: 'e-button',
172
+ label: 'Button',
173
+ variants: [
174
+ { meta: { breakpoint: 'tablet', state: null }, props: { 'font-size': '14px' }, custom_css: null },
175
+ ],
176
+ };
177
+ const style2: RendererStyleDefinition = {
178
+ ...style1,
179
+ variants: [
180
+ { meta: { breakpoint: 'tablet', state: null }, props: { 'font-size': '16px' }, custom_css: null },
181
+ ],
182
+ };
183
+
184
+ const resolve = jest.fn( ( { props } ) => props );
185
+ const renderStyles = createStylesRenderer( {
186
+ breakpoints: {
187
+ tablet: { width: 992, type: 'max-width' },
188
+ } as BreakpointsMap,
189
+ resolve,
190
+ } );
191
+
192
+ // Act.
193
+ const result = await renderStyles( { styles: [ style1, style2 ] } );
194
+
195
+ // Assert - should only render first occurrence.
196
+ expect( result ).toHaveLength( 1 );
197
+ expect( result[ 0 ].value ).toContain( 'font-size:14px' );
198
+ } );
199
+
200
+ it( 'should render separately when same id + breakpoint have different states', async () => {
201
+ // Arrange - same id and breakpoint but different states should NOT dedupe.
202
+ const normalStyle: RendererStyleDefinition = {
203
+ id: 'button-style',
204
+ type: 'class',
205
+ cssName: 'e-button',
206
+ label: 'Button',
207
+ variants: [
208
+ { meta: { breakpoint: 'tablet', state: null }, props: { 'font-size': '14px' }, custom_css: null },
209
+ ],
210
+ };
211
+ const hoverStyle: RendererStyleDefinition = {
212
+ ...normalStyle,
213
+ variants: [
214
+ { meta: { breakpoint: 'tablet', state: 'hover' }, props: { 'font-size': '16px' }, custom_css: null },
215
+ ],
216
+ };
217
+
218
+ const resolve = jest.fn( ( { props } ) => props );
219
+ const renderStyles = createStylesRenderer( {
220
+ breakpoints: {
221
+ tablet: { width: 992, type: 'max-width' },
222
+ } as BreakpointsMap,
223
+ resolve,
224
+ } );
225
+
226
+ // Act.
227
+ const result = await renderStyles( { styles: [ normalStyle, hoverStyle ] } );
228
+
229
+ // Assert - both should be rendered since states differ.
230
+ expect( result ).toHaveLength( 2 );
231
+ expect( result[ 0 ].state ).toBeNull();
232
+ expect( result[ 0 ].value ).toContain( 'font-size:14px' );
233
+ expect( result[ 1 ].state ).toBe( 'hover' );
234
+ expect( result[ 1 ].value ).toContain( 'font-size:16px' );
235
+ } );
236
+ } );
237
+
121
238
  describe( 'custom_css rendering', () => {
122
239
  it( 'should not render custom_css if raw is empty', async () => {
123
240
  // Arrange.
@@ -46,14 +46,24 @@ const SELECTORS_MAP: Record< StyleDefinitionType, string > = {
46
46
  class: '.',
47
47
  };
48
48
 
49
+ const DEFAULT_BREAKPOINT = 'desktop';
50
+ const DEFAULT_STATE = 'normal';
51
+
52
+ function getStyleUniqueKey( style: RendererStyleDefinition ): string {
53
+ const breakpoint = style.variants[ 0 ]?.meta?.breakpoint ?? DEFAULT_BREAKPOINT;
54
+ const state = style.variants[ 0 ]?.meta?.state ?? DEFAULT_STATE;
55
+ return `${ style.id }-${ breakpoint }-${ state }`;
56
+ }
57
+
49
58
  export function createStylesRenderer( { resolve, breakpoints, selectorPrefix = '' }: CreateStyleRendererArgs ) {
50
59
  return async ( { styles, signal }: StyleRendererArgs ): Promise< StyleItem[] > => {
51
- const seenIds = new Set< string >();
60
+ const seenKeys = new Set< string >();
52
61
  const uniqueStyles = styles.filter( ( style ) => {
53
- if ( seenIds.has( style.id ) ) {
62
+ const key = getStyleUniqueKey( style );
63
+ if ( seenKeys.has( key ) ) {
54
64
  return false;
55
65
  }
56
- seenIds.add( style.id );
66
+ seenKeys.add( key );
57
67
  return true;
58
68
  } );
59
69