@configuratorware/configurator-frontendgui 1.57.13 → 1.57.15

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.
Files changed (24) hide show
  1. package/App/Modules/Designer/Components/DesignerSelectionToolbox/DesignerSelectionToolbox.js +5 -1
  2. package/App/Modules/Designer/Containers/DesignerSelectionToolbox.js +10 -2
  3. package/App/Reducers/Configurator/Reducer.js +35 -14
  4. package/App/Reducers/DesignArea/Selectors.js +39 -7
  5. package/App/Reducers/Fonts/Selectors.js +35 -10
  6. package/App/Services/DesignerService.js +23 -5
  7. package/App/Shared/Components/AmountInput/index.js +65 -44
  8. package/App/Shared/Components/ProductVariant/index.js +1 -1
  9. package/package.json +4 -4
  10. package/src/App/Modules/Designer/Components/DesignerSelectionToolbox/DesignerSelectionToolbox.js +4 -0
  11. package/src/App/Modules/Designer/Containers/DesignerSelectionToolbox.js +5 -1
  12. package/src/App/Reducers/Configurator/Reducer.js +67 -17
  13. package/src/App/Reducers/Configurator/__tests__/Reducer.test.js +230 -30
  14. package/src/App/Reducers/DesignArea/Selectors.js +26 -0
  15. package/src/App/Reducers/DesignArea/__tests__/Selectors.test.js +19 -0
  16. package/src/App/Reducers/Fonts/Selectors.js +25 -5
  17. package/src/App/Reducers/Fonts/__tests__/Selectors.test.js +14 -1
  18. package/src/App/Services/DesignerService.js +28 -9
  19. package/src/App/Services/__tests__/DesignerService.test.js +111 -0
  20. package/src/App/Shared/Components/AmountInput/__tests__/AmountInput.blur.test.js +249 -0
  21. package/src/App/Shared/Components/AmountInput/__tests__/AmountInput.retyp.test.js +57 -0
  22. package/src/App/Shared/Components/AmountInput/__tests__/index.test.js +16 -0
  23. package/src/App/Shared/Components/AmountInput/index.js +42 -38
  24. package/src/App/Shared/Components/ProductVariant/index.js +1 -1
@@ -9,7 +9,6 @@ import isArray from 'lodash/isArray';
9
9
  import find from 'lodash/find';
10
10
  import findIndex from 'lodash/findIndex';
11
11
  import omit from 'lodash/omit';
12
- import forEach from 'lodash/forEach';
13
12
  import reduce from 'lodash/reduce';
14
13
  import immutableSet from 'Utils/Immutable/set';
15
14
  import { designDataHandler } from '../DesignData/Reducer';
@@ -421,13 +420,21 @@ export function baseHandler(state, action) {
421
420
  case actionTypes.SET_SELECTED_AMOUNT: {
422
421
  const selectedAmountsPath = 'customdata.selectedAmounts';
423
422
  const selectedAmounts = get(state.configuration, selectedAmountsPath, {});
424
- const configuration = immutableSet(
423
+ let configuration = immutableSet(
425
424
  state.configuration,
426
425
  selectedAmountsPath,
427
426
  action.amount === 0
428
427
  ? omit(selectedAmounts, action.identifier)
429
428
  : { ...selectedAmounts, [action.identifier]: action.amount }
430
429
  );
430
+ if (action.amount === 0) {
431
+ const bulkNames = get(configuration, 'customdata.bulkNames', {});
432
+ configuration = immutableSet(
433
+ configuration,
434
+ 'customdata.bulkNames',
435
+ omit(bulkNames, action.identifier)
436
+ );
437
+ }
431
438
  return {
432
439
  ...state,
433
440
  selectedAmountsUpdated: !!action.isUpdatedByUser,
@@ -437,16 +444,33 @@ export function baseHandler(state, action) {
437
444
 
438
445
  case actionTypes.SWITCH_SELECTED_AMOUNT: {
439
446
  const { srcIdentifier, dstIdentifier } = action;
447
+ if (srcIdentifier === dstIdentifier) {
448
+ return state;
449
+ }
440
450
  const selectedAmountsPath = 'customdata.selectedAmounts';
441
451
  const selectedAmounts = get(state.configuration, selectedAmountsPath, {});
442
452
  if (!(srcIdentifier in selectedAmounts)) {
443
453
  return state;
444
454
  }
445
- const configuration = immutableSet(
455
+ let configuration = immutableSet(
446
456
  state.configuration,
447
457
  selectedAmountsPath,
448
458
  omit({ ...selectedAmounts, [dstIdentifier]: selectedAmounts[srcIdentifier] }, srcIdentifier)
449
459
  );
460
+ const bulkNames = get(configuration, 'customdata.bulkNames', {});
461
+ if (srcIdentifier in bulkNames) {
462
+ configuration = immutableSet(
463
+ configuration,
464
+ 'customdata.bulkNames',
465
+ omit({ ...bulkNames, [dstIdentifier]: bulkNames[srcIdentifier] }, srcIdentifier)
466
+ );
467
+ } else if (dstIdentifier in bulkNames) {
468
+ configuration = immutableSet(
469
+ configuration,
470
+ 'customdata.bulkNames',
471
+ omit(bulkNames, dstIdentifier)
472
+ );
473
+ }
450
474
  return {
451
475
  ...state,
452
476
  configuration,
@@ -455,9 +479,21 @@ export function baseHandler(state, action) {
455
479
 
456
480
  case actionTypes.SET_SELECTED_AMOUNTS: {
457
481
  const selectedAmountsPath = 'configuration.customdata.selectedAmounts';
482
+ const bulkNamesPath = 'configuration.customdata.bulkNames';
458
483
  const selectedAmounts = get(state, selectedAmountsPath, {});
459
484
  const nextAmounts = { ...selectedAmounts, ...action.sizes };
460
- const newState = { ...state, selectedAmountsUpdated: !!action.isUpdatedByUser };
485
+ const removedIdentifiers = filter(
486
+ Object.keys(action.sizes || {}),
487
+ identifier => action.sizes[identifier] === 0
488
+ );
489
+ let newState = { ...state, selectedAmountsUpdated: !!action.isUpdatedByUser };
490
+ if (removedIdentifiers.length > 0) {
491
+ newState = immutableSet(
492
+ newState,
493
+ bulkNamesPath,
494
+ omit(get(state, bulkNamesPath, {}), removedIdentifiers)
495
+ );
496
+ }
461
497
  return immutableSet(newState, selectedAmountsPath, {
462
498
  ...reduce(
463
499
  nextAmounts,
@@ -474,14 +510,22 @@ export function baseHandler(state, action) {
474
510
  }
475
511
  case actionTypes.DELETE_SELECTED_AMOUNTS: {
476
512
  const selectedAmountsPath = 'configuration.customdata.selectedAmounts';
477
- const selectedAmounts = cloneDeep(get(state, selectedAmountsPath, {}));
478
- forEach(selectedAmounts, (selectedAmount, identifier) => {
479
- const variantIdentifier = find(action.variant.sizes, { identifier: identifier });
480
- if (variantIdentifier) {
481
- delete selectedAmounts[variantIdentifier.identifier];
482
- }
483
- });
484
- return immutableSet(state, selectedAmountsPath, selectedAmounts);
513
+ const bulkNamesPath = 'configuration.customdata.bulkNames';
514
+ const sizeIdentifiers = reduce(
515
+ get(action, 'variant.sizes', []),
516
+ (identifiers, size) => (size?.identifier ? [...identifiers, size.identifier] : identifiers),
517
+ []
518
+ );
519
+ const nextState = immutableSet(
520
+ state,
521
+ selectedAmountsPath,
522
+ omit(get(state, selectedAmountsPath, {}), sizeIdentifiers)
523
+ );
524
+ return immutableSet(
525
+ nextState,
526
+ bulkNamesPath,
527
+ omit(get(state, bulkNamesPath, {}), sizeIdentifiers)
528
+ );
485
529
  }
486
530
  default:
487
531
  return state;
@@ -530,11 +574,17 @@ export function configurationModifierHandler(state, action) {
530
574
  }
531
575
  case actionTypes.DELETE_SELECTED_AMOUNT: {
532
576
  const selectedAmountsPath = 'configuration.customdata.selectedAmounts';
533
- const selectedAmounts = cloneDeep(get(state, selectedAmountsPath, {}));
534
-
535
- delete selectedAmounts[action.identifier];
536
-
537
- return immutableSet(state, selectedAmountsPath, selectedAmounts);
577
+ const bulkNamesPath = 'configuration.customdata.bulkNames';
578
+ const nextState = immutableSet(
579
+ state,
580
+ selectedAmountsPath,
581
+ omit(get(state, selectedAmountsPath, {}), action.identifier)
582
+ );
583
+ return immutableSet(
584
+ nextState,
585
+ bulkNamesPath,
586
+ omit(get(state, bulkNamesPath, {}), action.identifier)
587
+ );
538
588
  }
539
589
  case actionTypes.SET_SELECTED_CALCULATION: {
540
590
  return immutableSet(
@@ -7,6 +7,7 @@ import {
7
7
  CLEAR_BULKNAMES,
8
8
  SAVE_BULKNAMES,
9
9
  SET_SELECTED_AMOUNTS,
10
+ DELETE_SELECTED_AMOUNT,
10
11
  DELETE_SELECTED_AMOUNTS,
11
12
  skipNextAutoResolve,
12
13
  RECEIVE_CALCULATION,
@@ -145,37 +146,236 @@ describe('Reducers/Configurator/Reducer', () => {
145
146
  hoodie_blue_s: 2,
146
147
  });
147
148
  });
148
- test('returns the correct state for DELETE_SELECTED_AMOUNTS action', () => {
149
- const variant = {
150
- identifier: 'hoodie_blue_s',
151
- title: 'blau',
152
- url: 'http://localhost:10030/images/item/thumb/hoodie_blue_s.jpg',
153
- sizes: [
154
- {
155
- identifier: 'hoodie_blue_s',
156
- amount: 1,
157
- size: 'Größe S',
158
- key: 'hoodie_blue_s',
159
- label: 'Größe S',
160
- url: 'http://localhost:10030/images/item/thumb/hoodie_blue_s.jpg',
161
- },
162
- {
163
- identifier: 'hoodie_green_m',
164
- amount: 1,
165
- size: 'Größe M',
166
- key: 'hoodie_green_m',
167
- label: 'Größe M',
168
- url: 'http://localhost:10030/images/item/thumb/hoodie_green_m.jpg',
169
- },
170
- ],
171
- };
172
- const sizes = { hoodie_blue_s: 0 };
173
- stateWithSelectedAmounts = configuratorReducer(stateWithSelectedAmounts, {
174
- type: DELETE_SELECTED_AMOUNTS,
175
- variant: variant,
176
- sizes: sizes,
149
+ describe('bulkNames stay in sync with selectedAmounts', () => {
150
+ test('SET_SELECTED_AMOUNT with amount 0 removes bulkNames for that identifier', () => {
151
+ let state = configuratorReducer(initialState, {
152
+ type: SET_SELECTED_AMOUNT,
153
+ identifier: 'hoodie_blue_s',
154
+ amount: 2,
155
+ });
156
+ state = configuratorReducer(state, {
157
+ type: SAVE_BULKNAMES,
158
+ identifier: 'hoodie_blue_s',
159
+ bulkNamesData: ['Alex', 'Leon'],
160
+ });
161
+ state = configuratorReducer(state, {
162
+ type: SET_SELECTED_AMOUNT,
163
+ identifier: 'hoodie_blue_s',
164
+ amount: 0,
165
+ });
166
+ expect(get(state, 'configuration.customdata.selectedAmounts')).toEqual({});
167
+ expect(get(state, 'configuration.customdata.bulkNames')).toEqual({});
168
+ });
169
+
170
+ test('DELETE_SELECTED_AMOUNT removes bulkNames for that identifier', () => {
171
+ let state = configuratorReducer(initialState, {
172
+ type: SET_SELECTED_AMOUNT,
173
+ identifier: 'hoodie_blue_s',
174
+ amount: 2,
175
+ });
176
+ state = configuratorReducer(state, {
177
+ type: SAVE_BULKNAMES,
178
+ identifier: 'hoodie_blue_s',
179
+ bulkNamesData: ['Alex', 'Leon'],
180
+ });
181
+ state = configuratorReducer(state, {
182
+ type: SAVE_BULKNAMES,
183
+ identifier: 'hoodie_green_m',
184
+ bulkNamesData: ['Lea', 'Martin'],
185
+ });
186
+ state = configuratorReducer(state, {
187
+ type: DELETE_SELECTED_AMOUNT,
188
+ identifier: 'hoodie_blue_s',
189
+ });
190
+ expect(get(state, 'configuration.customdata.selectedAmounts')).toEqual({});
191
+ expect(get(state, 'configuration.customdata.bulkNames')).toEqual({
192
+ hoodie_green_m: ['Lea', 'Martin'],
193
+ });
194
+ });
195
+
196
+ test('DELETE_SELECTED_AMOUNTS clears bulkNames for all variant sizes', () => {
197
+ const variant = {
198
+ identifier: 'hoodie_blue_s',
199
+ title: 'blau',
200
+ url: 'http://localhost:10030/images/item/thumb/hoodie_blue_s.jpg',
201
+ sizes: [
202
+ {
203
+ identifier: 'hoodie_blue_s',
204
+ amount: 1,
205
+ size: 'Größe S',
206
+ key: 'hoodie_blue_s',
207
+ label: 'Größe S',
208
+ url: 'http://localhost:10030/images/item/thumb/hoodie_blue_s.jpg',
209
+ },
210
+ {
211
+ identifier: 'hoodie_green_m',
212
+ amount: 1,
213
+ size: 'Größe M',
214
+ key: 'hoodie_green_m',
215
+ label: 'Größe M',
216
+ url: 'http://localhost:10030/images/item/thumb/hoodie_green_m.jpg',
217
+ },
218
+ ],
219
+ };
220
+ const sizes = { hoodie_blue_s: 0 };
221
+ let state = configuratorReducer(stateWithSelectedAmounts, {
222
+ type: SAVE_BULKNAMES,
223
+ identifier: 'hoodie_blue_s',
224
+ bulkNamesData: ['Alex', 'Leon'],
225
+ });
226
+ state = configuratorReducer(state, {
227
+ type: SAVE_BULKNAMES,
228
+ identifier: 'hoodie_green_m',
229
+ bulkNamesData: ['Lea', 'Martin'],
230
+ });
231
+ state = configuratorReducer(state, {
232
+ type: DELETE_SELECTED_AMOUNTS,
233
+ variant: variant,
234
+ sizes: sizes,
235
+ });
236
+ expect(get(state, 'configuration.customdata.selectedAmounts')).toEqual({});
237
+ expect(get(state, 'configuration.customdata.bulkNames')).toEqual({});
238
+ });
239
+
240
+ test('SET_SELECTED_AMOUNTS removes bulkNames for sizes set to 0', () => {
241
+ const variant = {
242
+ identifier: 'hoodie_blue_s',
243
+ sizes: [{ identifier: 'hoodie_blue_s' }, { identifier: 'hoodie_green_m' }],
244
+ };
245
+ let state = configuratorReducer(initialState, {
246
+ type: SET_SELECTED_AMOUNTS,
247
+ variant,
248
+ sizes: { hoodie_green_m: 5, hoodie_blue_s: 2 },
249
+ });
250
+ state = configuratorReducer(state, {
251
+ type: SAVE_BULKNAMES,
252
+ identifier: 'hoodie_blue_s',
253
+ bulkNamesData: ['Alex', 'Leon'],
254
+ });
255
+ state = configuratorReducer(state, {
256
+ type: SAVE_BULKNAMES,
257
+ identifier: 'hoodie_green_m',
258
+ bulkNamesData: ['Lea', 'Martin'],
259
+ });
260
+ state = configuratorReducer(state, {
261
+ type: SET_SELECTED_AMOUNTS,
262
+ variant,
263
+ sizes: { hoodie_blue_s: 0 },
264
+ });
265
+ expect(get(state, 'configuration.customdata.selectedAmounts')).toEqual({ hoodie_green_m: 5 });
266
+ expect(get(state, 'configuration.customdata.bulkNames')).toEqual({
267
+ hoodie_green_m: ['Lea', 'Martin'],
268
+ });
269
+ });
270
+
271
+ test('SWITCH_SELECTED_AMOUNT moves bulkNames from src to dst identifier', () => {
272
+ let state = configuratorReducer(initialState, {
273
+ type: SET_SELECTED_AMOUNT,
274
+ identifier: 'shirt_red_m',
275
+ amount: 2,
276
+ });
277
+ state = configuratorReducer(state, {
278
+ type: SAVE_BULKNAMES,
279
+ identifier: 'shirt_red_m',
280
+ bulkNamesData: ['Alex', 'Leon'],
281
+ });
282
+ state = configuratorReducer(state, {
283
+ type: SWITCH_SELECTED_AMOUNT,
284
+ srcIdentifier: 'shirt_red_m',
285
+ dstIdentifier: 'shirt_blue_m',
286
+ });
287
+ expect(get(state, 'configuration.customdata.selectedAmounts')).toEqual({ shirt_blue_m: 2 });
288
+ expect(get(state, 'configuration.customdata.bulkNames')).toEqual({
289
+ shirt_blue_m: ['Alex', 'Leon'],
290
+ });
291
+ });
292
+
293
+ test('SWITCH_SELECTED_AMOUNT is a no-op when src is not in selectedAmounts', () => {
294
+ const state = configuratorReducer(initialState, {
295
+ type: SAVE_BULKNAMES,
296
+ identifier: 'shirt_red_m',
297
+ bulkNamesData: ['Alex'],
298
+ });
299
+ const next = configuratorReducer(state, {
300
+ type: SWITCH_SELECTED_AMOUNT,
301
+ srcIdentifier: 'shirt_red_m',
302
+ dstIdentifier: 'shirt_blue_m',
303
+ });
304
+ expect(next).toBe(state);
305
+ expect(get(next, 'configuration.customdata.bulkNames')).toEqual({ shirt_red_m: ['Alex'] });
306
+ });
307
+
308
+ test('SWITCH_SELECTED_AMOUNT is a no-op when src and dst are the same identifier', () => {
309
+ let state = configuratorReducer(initialState, {
310
+ type: SET_SELECTED_AMOUNT,
311
+ identifier: 'shirt_red_m',
312
+ amount: 2,
313
+ });
314
+ state = configuratorReducer(state, {
315
+ type: SAVE_BULKNAMES,
316
+ identifier: 'shirt_red_m',
317
+ bulkNamesData: ['Alex', 'Leon'],
318
+ });
319
+ const next = configuratorReducer(state, {
320
+ type: SWITCH_SELECTED_AMOUNT,
321
+ srcIdentifier: 'shirt_red_m',
322
+ dstIdentifier: 'shirt_red_m',
323
+ });
324
+ expect(next).toBe(state);
325
+ expect(get(next, 'configuration.customdata.selectedAmounts')).toEqual({ shirt_red_m: 2 });
326
+ expect(get(next, 'configuration.customdata.bulkNames')).toEqual({
327
+ shirt_red_m: ['Alex', 'Leon'],
328
+ });
329
+ });
330
+
331
+ test('SWITCH_SELECTED_AMOUNT moves amount and clears dst bulkNames when src has no names', () => {
332
+ let state = configuratorReducer(initialState, {
333
+ type: SET_SELECTED_AMOUNT,
334
+ identifier: 'shirt_red_m',
335
+ amount: 2,
336
+ });
337
+ state = configuratorReducer(state, {
338
+ type: SAVE_BULKNAMES,
339
+ identifier: 'shirt_blue_m',
340
+ bulkNamesData: ['Lea'],
341
+ });
342
+ state = configuratorReducer(state, {
343
+ type: SWITCH_SELECTED_AMOUNT,
344
+ srcIdentifier: 'shirt_red_m',
345
+ dstIdentifier: 'shirt_blue_m',
346
+ });
347
+ expect(get(state, 'configuration.customdata.selectedAmounts')).toEqual({ shirt_blue_m: 2 });
348
+ expect(get(state, 'configuration.customdata.bulkNames')).toEqual({});
349
+ });
350
+
351
+ test('SET_SELECTED_AMOUNTS removes all bulkNames when every size is set to 0', () => {
352
+ const variant = {
353
+ identifier: 'hoodie_blue_s',
354
+ sizes: [{ identifier: 'hoodie_blue_s' }, { identifier: 'hoodie_green_m' }],
355
+ };
356
+ let state = configuratorReducer(initialState, {
357
+ type: SET_SELECTED_AMOUNTS,
358
+ variant,
359
+ sizes: { hoodie_green_m: 5, hoodie_blue_s: 2 },
360
+ });
361
+ state = configuratorReducer(state, {
362
+ type: SAVE_BULKNAMES,
363
+ identifier: 'hoodie_blue_s',
364
+ bulkNamesData: ['Alex'],
365
+ });
366
+ state = configuratorReducer(state, {
367
+ type: SAVE_BULKNAMES,
368
+ identifier: 'hoodie_green_m',
369
+ bulkNamesData: ['Lea'],
370
+ });
371
+ state = configuratorReducer(state, {
372
+ type: SET_SELECTED_AMOUNTS,
373
+ variant,
374
+ sizes: { hoodie_blue_s: 0, hoodie_green_m: 0 },
375
+ });
376
+ expect(get(state, 'configuration.customdata.selectedAmounts')).toEqual({});
377
+ expect(get(state, 'configuration.customdata.bulkNames')).toEqual({});
177
378
  });
178
- expect(get(stateWithSelectedAmounts, 'configuration.customdata.selectedAmounts')).toEqual({});
179
379
  });
180
380
 
181
381
  test('returns the correct state for SWITCH_SELECTED_AMOUNT action', () => {
@@ -646,6 +646,32 @@ export const parseColorData = jsonStr => {
646
646
  };
647
647
  };
648
648
 
649
+ /**
650
+ * Normalizes canvas text color entries (style.colors or text editor) for HTML generation.
651
+ * @param {{ identifier: string|object, value: string }|null|undefined} color
652
+ * @returns {{ identifier: string, value: string }|null}
653
+ */
654
+ export const normalizeTextColorData = color => {
655
+ if (!color?.identifier || !color?.value) {
656
+ return null;
657
+ }
658
+ const identifier =
659
+ typeof color.identifier === 'string' ? color.identifier : JSON.stringify(color.identifier);
660
+ return { identifier, value: color.value.toLowerCase() };
661
+ };
662
+
663
+ /**
664
+ * Color from canvas object style.colors (parsed from content HTML).
665
+ * @param {{ styleColors?: Array }} params
666
+ * @returns {{ identifier: string, value: string }|null}
667
+ */
668
+ export const getColorDataForTextContent = ({ styleColors }) => {
669
+ if (styleColors?.length > 0) {
670
+ return normalizeTextColorData(styleColors[0]);
671
+ }
672
+ return null;
673
+ };
674
+
649
675
  export const getCameraViewListForDesignAreaList = (filteredDesignAreas, designedAreas) =>
650
676
  ['default'].concat(
651
677
  filteredDesignAreas
@@ -20,6 +20,8 @@ import {
20
20
  getColorsFromCanvasDataWithPalette,
21
21
  createColorData,
22
22
  parseColorData,
23
+ normalizeTextColorData,
24
+ getColorDataForTextContent,
23
25
  isActiveBulkName,
24
26
  getDefaultDesignAreaIdentifierFromProductionMethod,
25
27
  getDefaultColorsFromDesignProductionMethod,
@@ -1128,6 +1130,23 @@ describe('Reducers/DesignArea/Selectors', () => {
1128
1130
  });
1129
1131
  });
1130
1132
 
1133
+ test('getColorDataForTextContent returns color from styleColors', () => {
1134
+ const styleColors = [
1135
+ {
1136
+ identifier: '{"color":"blue","palette":"default"}',
1137
+ value: '#0078b8',
1138
+ },
1139
+ ];
1140
+ expect(getColorDataForTextContent({ styleColors })).toEqual(styleColors[0]);
1141
+ expect(getColorDataForTextContent({ styleColors: [] })).toBeNull();
1142
+ expect(
1143
+ normalizeTextColorData({ identifier: { color: 'red', palette: 'default' }, value: '#F73429' })
1144
+ ).toEqual({
1145
+ identifier: '{"color":"red","palette":"default"}',
1146
+ value: '#f73429',
1147
+ });
1148
+ });
1149
+
1131
1150
  test('get default design area from the flag isDefault', () => {
1132
1151
  const state = {
1133
1152
  designArea: {
@@ -11,6 +11,30 @@ export function getDefaultFont(state) {
11
11
  return find(fonts, { isDefault: true }) || first(fonts);
12
12
  }
13
13
 
14
+ const stringifyFontParams = fontParams =>
15
+ Object.keys(fontParams)
16
+ .map(prop => `${prop}: ${fontParams[prop]};`)
17
+ .join(' ');
18
+
19
+ /**
20
+ * Inline font CSS from the text toolbar selection (font family + bold/italic).
21
+ * @param {{ fontItem?: { family?: string }, isBold?: boolean, isItalic?: boolean }} params
22
+ * @returns {string}
23
+ */
24
+ export const getTextContentFontParamsFromSelection = ({ fontItem, isBold, isItalic }) => {
25
+ if (!fontItem?.family) {
26
+ return '';
27
+ }
28
+ const fontParams = {
29
+ 'font-family': `"${escapeQuotes(fontItem.family)}"`,
30
+ 'font-weight': isBold ? 'bold' : 'normal',
31
+ 'font-style': isItalic ? 'italic' : 'normal',
32
+ };
33
+ return Object.keys(fontParams)
34
+ .map(prop => `${prop}: ${fontParams[prop]};`)
35
+ .join(' ');
36
+ };
37
+
14
38
  export const getDefaultFontParams = (state, stringify = true) => {
15
39
  const defaultFont = getDefaultFont(state);
16
40
  const fontParams = {
@@ -18,11 +42,7 @@ export const getDefaultFontParams = (state, stringify = true) => {
18
42
  'font-weight': 'normal',
19
43
  'font-style': 'normal',
20
44
  };
21
- return stringify
22
- ? Object.keys(fontParams)
23
- .map(prop => `${prop}: ${fontParams[prop]};`)
24
- .join(' ')
25
- : fontParams;
45
+ return stringify ? stringifyFontParams(fontParams) : fontParams;
26
46
  };
27
47
 
28
48
  export const getFontMapping = createSelector(
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- import { getDefaultFont } from '../Selectors';
3
+ import { getDefaultFont, getTextContentFontParamsFromSelection } from '../Selectors';
4
4
 
5
5
  describe('Reducers/Fonts/Selectors', () => {
6
6
  test('getDefaultFont returns the correct font object (isDefault is true)', () => {
@@ -56,4 +56,17 @@ describe('Reducers/Fonts/Selectors', () => {
56
56
  isDefault: false,
57
57
  });
58
58
  });
59
+
60
+ it('getTextContentFontParamsFromSelection builds inline font CSS', () => {
61
+ expect(
62
+ getTextContentFontParamsFromSelection({
63
+ fontItem: { family: 'Open Sans' },
64
+ isBold: true,
65
+ isItalic: false,
66
+ })
67
+ ).toBe('font-family: "Open Sans"; font-weight: bold; font-style: normal;');
68
+ expect(
69
+ getTextContentFontParamsFromSelection({ fontItem: null, isBold: false, isItalic: false })
70
+ ).toBe('');
71
+ });
59
72
  });
@@ -29,11 +29,12 @@ import {
29
29
  getFontSizeMin,
30
30
  hasPlaceHolderImagesForActiveDesignArea,
31
31
  getVectorizedImageColor,
32
+ getColorDataForTextContent,
32
33
  getElementLockState,
33
34
  getSelectedDesignProductionMethod,
34
35
  getDesignDataForSelectedDesignArea,
35
36
  } from '../Reducers/DesignArea/Selectors';
36
- import { getDefaultFontParams } from '../Reducers/Fonts/Selectors';
37
+ import { getDefaultFontParams, getTextContentFontParamsFromSelection } from '../Reducers/Fonts/Selectors';
37
38
  import { createTextPlaceholder } from '../Reducers/ImageGallery/Modifiers';
38
39
  import { updateLinkedImagesWithImageData } from '../Reducers/DesignData/Actions';
39
40
  import { checkDesignElementRestrictions } from '../Reducers/DesignArea/DesignElementSelectors';
@@ -334,21 +335,39 @@ export default class DesignerService {
334
335
  * @returns {string}
335
336
  */
336
337
  createColorizedTextContent(str, colorCount = 1, options = {}) {
337
- const { withPlaceholder = true } = options;
338
+ const {
339
+ withPlaceholder = true,
340
+ styleColors = null,
341
+ fontItem = null,
342
+ isBold = false,
343
+ isItalic = false,
344
+ } = options;
338
345
 
339
346
  const state = Services.store.state;
340
- const fontParams = getDefaultFontParams(state);
347
+ const selectionFontParams = getTextContentFontParamsFromSelection({
348
+ fontItem,
349
+ isBold,
350
+ isItalic,
351
+ });
352
+ const fontParams = selectionFontParams || getDefaultFontParams(state);
341
353
  const chunkLength = Math.ceil(str.length / colorCount);
342
354
 
343
355
  const content = str
344
356
  .match(new RegExp(`.{1,${chunkLength}}`, 'g'))
345
357
  .map((chunk, idx) => {
346
- const color = getDefaultTextColor(state, colorCount > 1 ? idx : null);
347
-
348
- const lastImageColor = getVectorizedImageColor(state);
349
- const colorData = isEmpty(lastImageColor)
350
- ? createColorData(color.identifier, color.paletteIdentifier, color.custom, color.value)
351
- : lastImageColor;
358
+ let colorData = getColorDataForTextContent({ styleColors });
359
+ if (!colorData) {
360
+ const color = getDefaultTextColor(state, colorCount > 1 ? idx : null);
361
+ const lastImageColor = getVectorizedImageColor(state);
362
+ colorData = isEmpty(lastImageColor)
363
+ ? createColorData(
364
+ color.identifier,
365
+ color.paletteIdentifier,
366
+ color.custom,
367
+ color.value
368
+ )
369
+ : lastImageColor;
370
+ }
352
371
 
353
372
  return `<span style='color: ${colorData.value}; ${fontParams}' data-color='${colorData.identifier}'>${chunk}</span>`;
354
373
  })