@widgetic/editor 4.0.10 → 4.1.0

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 (84) hide show
  1. package/README.md +242 -0
  2. package/dist/components/WidgetPreview.svelte +123 -86
  3. package/dist/components/WidgetPreview.svelte.d.ts +8 -0
  4. package/dist/components/color-select/ColorSelect.svelte +4 -4
  5. package/dist/components/editors/CompositionEditorApp.svelte +562 -0
  6. package/dist/components/editors/CompositionEditorApp.svelte.d.ts +95 -0
  7. package/dist/components/editors/EmbedEditor.svelte +33 -22
  8. package/dist/components/editors/EmbedEditor.svelte.d.ts +1 -1
  9. package/dist/components/editors/PropsEditor.svelte +267 -850
  10. package/dist/components/editors/PropsEditor.svelte.d.ts +10 -3
  11. package/dist/components/ic/AngleInputController.svelte +1 -1
  12. package/dist/components/ic/BrowserInputController.svelte +58 -31
  13. package/dist/components/ic/ColorPickerInputController.svelte +55 -21
  14. package/dist/components/ic/DateFieldInputController.svelte +34 -14
  15. package/dist/components/ic/DatePickerInputController.svelte +37 -21
  16. package/dist/components/ic/DropdownInputController.svelte +21 -9
  17. package/dist/components/ic/DropdownInputController.svelte.d.ts +1 -1
  18. package/dist/components/ic/FixedPositionInputController.svelte +12 -6
  19. package/dist/components/ic/FontInputController.svelte +100 -76
  20. package/dist/components/ic/InputControllerHeader.svelte +16 -9
  21. package/dist/components/ic/NumberControls.svelte +4 -4
  22. package/dist/components/ic/NumberInputController.svelte +6 -4
  23. package/dist/components/ic/PositionInputController.svelte +23 -15
  24. package/dist/components/ic/RangeCalendarInputController.svelte +36 -20
  25. package/dist/components/ic/RoundedCornersInputController.svelte +39 -26
  26. package/dist/components/ic/SliderInputController.svelte +7 -2
  27. package/dist/components/ic/SwitchInputController.svelte +12 -3
  28. package/dist/components/ic/TagsInputController.svelte +19 -4
  29. package/dist/components/ic/TextAreaInputController.svelte +79 -51
  30. package/dist/components/ic/TextAreaPreviewInputController.svelte +27 -72
  31. package/dist/components/ic/TextInputController.svelte +17 -1
  32. package/dist/components/ic/TimeInputController.svelte +11 -4
  33. package/dist/components/ic/VectorInputController.svelte +101 -70
  34. package/dist/components/ic/shared/ItemGrid.svelte +12 -4
  35. package/dist/components/ic/shared/RecentItemsSection.svelte +16 -11
  36. package/dist/components/ic/shared/RecentItemsSection.svelte.d.ts +1 -1
  37. package/dist/composition/compositionApi.d.ts +95 -0
  38. package/dist/composition/compositionApi.js +289 -0
  39. package/dist/composition/compositionEditorController.d.ts +263 -0
  40. package/dist/composition/compositionEditorController.js +746 -0
  41. package/dist/composition/compositionHelpers.d.ts +74 -0
  42. package/dist/composition/compositionHelpers.js +405 -0
  43. package/dist/i18n/index.d.ts +4 -0
  44. package/dist/i18n/index.js +4 -0
  45. package/dist/i18n/labels.d.ts +15 -0
  46. package/dist/i18n/labels.js +57 -0
  47. package/dist/i18n/language.d.ts +228 -0
  48. package/dist/i18n/language.js +34 -0
  49. package/dist/i18n/languageKit.d.ts +54 -0
  50. package/dist/i18n/languageKit.js +102 -0
  51. package/dist/i18n/messages.d.ts +241 -0
  52. package/dist/i18n/messages.js +253 -0
  53. package/dist/i18n/translate.d.ts +6 -0
  54. package/dist/i18n/translate.js +8 -0
  55. package/dist/index.d.ts +4 -0
  56. package/dist/index.js +8 -0
  57. package/dist/mediaLibraryContext.d.ts +10 -0
  58. package/dist/mediaLibraryContext.js +30 -0
  59. package/dist/utils/FontManager.d.ts +21 -5
  60. package/dist/utils/FontManager.js +62 -67
  61. package/dist/utils/VectorManager.d.ts +0 -1
  62. package/dist/utils/VectorManager.js +10 -12
  63. package/dist/utils/dockPlacement.d.ts +12 -0
  64. package/dist/utils/dockPlacement.js +13 -0
  65. package/dist/utils/editorStorage.d.ts +23 -0
  66. package/dist/utils/editorStorage.js +95 -0
  67. package/dist/utils/fieldDirection.d.ts +13 -0
  68. package/dist/utils/fieldDirection.js +25 -0
  69. package/dist/utils/fontUtils.js +5 -3
  70. package/dist/utils/htmlSafety.d.ts +21 -0
  71. package/dist/utils/htmlSafety.js +38 -0
  72. package/dist/utils/itemReorder.d.ts +90 -0
  73. package/dist/utils/itemReorder.js +354 -0
  74. package/dist/utils/recentColors.js +4 -2
  75. package/dist/utils/richTextPreview.d.ts +38 -0
  76. package/dist/utils/richTextPreview.js +207 -0
  77. package/dist/utils/scrollActivity.d.ts +14 -0
  78. package/dist/utils/scrollActivity.js +63 -0
  79. package/dist/utils/shadowDom.d.ts +67 -0
  80. package/dist/utils/shadowDom.js +211 -0
  81. package/dist/utils/vectorUtils.js +5 -3
  82. package/dist/utils/visibilityCondition.d.ts +40 -0
  83. package/dist/utils/visibilityCondition.js +535 -0
  84. package/package.json +7 -6
@@ -79,6 +79,11 @@ class FontManager {
79
79
  static instance = null;
80
80
  // Track loaded and pending fonts to prevent duplicates
81
81
  loadedFonts = new Set();
82
+ // What this manager added to the page's <head>, to take away again when the editor leaves the page
83
+ addedHeadElements = new Set();
84
+ // True from the moment the editor leaves the page until a new one asks for the manager: a stylesheet that was still
85
+ // on its way is not added to a page the editor is no longer on
86
+ pageWasReleased = false;
82
87
  // Map of intersection observers keyed by container ID
83
88
  observers = new Map();
84
89
  // Debounce timer for scroll events
@@ -105,11 +110,9 @@ class FontManager {
105
110
  // Controls how long we keep the font list in the durable L1 store
106
111
  static CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
107
112
  static LOADED_FONTS_KEY = 'fonts.loaded';
108
- // Legacy localStorage keys read once for migration
109
- static LEGACY_CACHE_KEY = 'google_fonts_cache';
110
- static LEGACY_LOADED_FONTS_KEY = 'loaded_fonts_cache';
111
- // Controls how long browsers should cache the actual font files and stylesheets
112
- static CACHE_CONTROL = 'public, max-age=31536000'; // 1 year
113
+ // The old localStorage copies (`google_fonts_cache`, `loaded_fonts_cache`) are no longer read or removed: the editor
114
+ // runs in customers' pages, whose own localStorage keys it must not touch, and the durable store above has been the
115
+ // source for long enough that the worst case is one fresh download of the catalog.
113
116
  // Add new property to store visible fonts
114
117
  visibleFonts = new Set();
115
118
  static initialPreloadComplete = false;
@@ -257,6 +260,8 @@ class FontManager {
257
260
  FontManager.instance.apiKey = trimmedKey;
258
261
  FontManager.instance.catalogFetchPromise = null;
259
262
  }
263
+ // An editor that asks for the manager is on the page: its fonts may be added again
264
+ FontManager.instance.pageWasReleased = false;
260
265
  // Always await in-flight init — concurrent callers used to return mid-initialize.
261
266
  if (FontManager.initializationPromise) {
262
267
  await FontManager.initializationPromise;
@@ -370,24 +375,11 @@ class FontManager {
370
375
  return this.preInitializedObservers.get('default') || null;
371
376
  }
372
377
  /**
373
- * Loads previously cached fonts from the durable L1 store (legacy localStorage fallback)
378
+ * Loads previously cached fonts from the durable L1 store
374
379
  */
375
380
  async loadCachedFonts() {
376
381
  try {
377
- let cachedLoadedFonts = await readKvValue(FontManager.LOADED_FONTS_KEY);
378
- if (!cachedLoadedFonts) {
379
- const legacyRaw = localStorage.getItem(FontManager.LEGACY_LOADED_FONTS_KEY);
380
- if (legacyRaw) {
381
- try {
382
- cachedLoadedFonts = JSON.parse(legacyRaw);
383
- await writeKvValue(FontManager.LOADED_FONTS_KEY, cachedLoadedFonts);
384
- }
385
- catch {
386
- /* ignore malformed legacy data */
387
- }
388
- localStorage.removeItem(FontManager.LEGACY_LOADED_FONTS_KEY);
389
- }
390
- }
382
+ const cachedLoadedFonts = await readKvValue(FontManager.LOADED_FONTS_KEY);
391
383
  if (cachedLoadedFonts && Array.isArray(cachedLoadedFonts)) {
392
384
  // Only load the first fonts initially
393
385
  const initialFonts = cachedLoadedFonts.slice(0, FontManager.initialFontNumber);
@@ -420,18 +412,10 @@ class FontManager {
420
412
  try {
421
413
  const fontsToCache = Array.from(this.loadedFonts);
422
414
  void writeKvValue(FontManager.LOADED_FONTS_KEY, fontsToCache);
423
- // Add cache control headers for font files
424
- const styleSheets = document.querySelectorAll('link[href*="fonts.googleapis.com"]');
425
- styleSheets.forEach(sheet => {
426
- if (sheet instanceof HTMLLinkElement) {
427
- sheet.setAttribute('crossorigin', 'anonymous');
428
- // Add cache control header through meta tag since we can't directly set headers
429
- const meta = document.createElement('meta');
430
- meta.setAttribute('http-equiv', 'Cache-Control');
431
- meta.setAttribute('content', FontManager.CACHE_CONTROL);
432
- document.head.appendChild(meta);
433
- }
434
- });
415
+ // (This used to go through every Google Fonts stylesheet link of the page, the page's own included, to set
416
+ // `crossorigin` on it and to add a <meta http-equiv="Cache-Control"> to the page's head for each one. A stylesheet
417
+ // that has been fetched is not fetched again because of an attribute, no browser reads that meta, and both were
418
+ // changes to a page that is not the editor's, so they are gone.)
435
419
  }
436
420
  catch (error) {
437
421
  console.error('❌ [FontManager] Error saving loaded fonts:', error);
@@ -465,6 +449,42 @@ class FontManager {
465
449
  }
466
450
  return this.completeFontList.some(font => font.family.toLowerCase() === family.toLowerCase());
467
451
  }
452
+ /**
453
+ * Puts an element in the page's <head> and remembers it, so that it can be taken away again
454
+ * ({@link removeAddedHeadElements}): the editor can be on somebody else's page, which it should leave as it found it.
455
+ */
456
+ addToHead(element) {
457
+ // The editor has left the page (a font that was on its way arrives late): nothing is added to it any more
458
+ if (this.pageWasReleased)
459
+ return;
460
+ document.head.appendChild(element);
461
+ this.addedHeadElements.add(element);
462
+ }
463
+ /**
464
+ * Takes out of the page's <head> everything this manager put there (the stylesheets of the fonts, their preload
465
+ * hints and the connection hint), and forgets which fonts were loaded, because they are not any more. The next
466
+ * font to be asked for puts its stylesheet back. The page's own links are never touched: only what was added here
467
+ * is removed.
468
+ */
469
+ removeAddedHeadElements() {
470
+ try {
471
+ this.pageWasReleased = true;
472
+ for (const element of this.addedHeadElements)
473
+ element.remove();
474
+ this.addedHeadElements.clear();
475
+ this.loadedFonts.clear();
476
+ }
477
+ catch (error) {
478
+ console.error('❌ [FontManager] Error removing the font stylesheets from the page:', error);
479
+ }
480
+ }
481
+ /**
482
+ * Leaves the page as the editor found it as far as fonts go. Meant for when the editor is taken away; does nothing
483
+ * if no font was ever asked for.
484
+ */
485
+ static releasePageResources() {
486
+ FontManager.instance?.removeAddedHeadElements();
487
+ }
468
488
  /**
469
489
  * Inserts a font stylesheet into the document
470
490
  * @param family - Font family name
@@ -487,7 +507,7 @@ class FontManager {
487
507
  preconnect.rel = 'preconnect';
488
508
  preconnect.href = 'https://fonts.gstatic.com';
489
509
  preconnect.crossOrigin = 'anonymous';
490
- document.head.appendChild(preconnect);
510
+ this.addToHead(preconnect);
491
511
  }
492
512
  // Build URL based on whether we want subset or full font
493
513
  const baseUrl = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family)}:wght@400&display=swap`;
@@ -504,15 +524,10 @@ class FontManager {
504
524
  link.setAttribute('data-full-font', 'true');
505
525
  }
506
526
  // Add to document
507
- document.head.appendChild(link);
508
- // Add preload hint with correct attributes after stylesheet
509
- const preloadLink = document.createElement('link');
510
- preloadLink.rel = 'preload';
511
- preloadLink.as = 'style'; // Changed from 'font' to 'style'
512
- preloadLink.href = finalUrl;
513
- preloadLink.crossOrigin = 'anonymous';
514
- preloadLink.setAttribute('onload', "this.onload=null;this.rel='stylesheet'");
515
- document.head.appendChild(preloadLink);
527
+ this.addToHead(link);
528
+ // (A preload hint for the same address used to follow here, with an `onload` attribute that turned it into a
529
+ // second stylesheet. It added nothing to the stylesheet link above, and a page with a strict Content Security
530
+ // Policy blocks and reports that inline handler, so it is gone.)
516
531
  // Wait for stylesheet to load
517
532
  await new Promise((resolve, reject) => {
518
533
  link.onload = () => resolve();
@@ -662,23 +677,11 @@ class FontManager {
662
677
  }
663
678
  }
664
679
  /**
665
- * Gets font list from the durable L1 cache (legacy localStorage fallback)
680
+ * Gets font list from the durable L1 cache
666
681
  */
667
682
  async getFontListFromCache() {
668
683
  try {
669
- let cache = await readKvValue(FontManager.CACHE_KEY, FontManager.CACHE_DURATION);
670
- if (!cache) {
671
- const legacyRaw = localStorage.getItem(FontManager.LEGACY_CACHE_KEY);
672
- if (legacyRaw) {
673
- try {
674
- cache = JSON.parse(legacyRaw);
675
- }
676
- catch {
677
- /* ignore malformed legacy data */
678
- }
679
- localStorage.removeItem(FontManager.LEGACY_CACHE_KEY);
680
- }
681
- }
684
+ const cache = await readKvValue(FontManager.CACHE_KEY, FontManager.CACHE_DURATION);
682
685
  if (!cache)
683
686
  return null;
684
687
  // Check if cache is expired
@@ -1040,12 +1043,8 @@ class FontManager {
1040
1043
  fontFace: false
1041
1044
  };
1042
1045
  try {
1043
- // Check the durable loaded-fonts registry (legacy localStorage fallback)
1044
- let cachedFonts = await readKvValue(FontManager.LOADED_FONTS_KEY);
1045
- if (!cachedFonts) {
1046
- const legacyRaw = localStorage.getItem(FontManager.LEGACY_LOADED_FONTS_KEY);
1047
- cachedFonts = legacyRaw ? JSON.parse(legacyRaw) : [];
1048
- }
1046
+ // Check the durable loaded-fonts registry
1047
+ const cachedFonts = await readKvValue(FontManager.LOADED_FONTS_KEY);
1049
1048
  const loadedFonts = Array.isArray(cachedFonts) ? cachedFonts : [];
1050
1049
  const isCached = loadedFonts.includes(family);
1051
1050
  // Check if stylesheet exists in DOM
@@ -1072,12 +1071,8 @@ class FontManager {
1072
1071
  */
1073
1072
  async getBrowserCachedFonts() {
1074
1073
  try {
1075
- // Get fonts from the durable loaded-fonts registry (legacy fallback)
1074
+ // Get fonts from the durable loaded-fonts registry
1076
1075
  let cachedFonts = await readKvValue(FontManager.LOADED_FONTS_KEY);
1077
- if (!cachedFonts) {
1078
- const legacyRaw = localStorage.getItem(FontManager.LEGACY_LOADED_FONTS_KEY);
1079
- cachedFonts = legacyRaw ? JSON.parse(legacyRaw) : [];
1080
- }
1081
1076
  cachedFonts = Array.isArray(cachedFonts) ? cachedFonts : [];
1082
1077
  // console.log('📦 [FontManager] Fonts in durable registry:', cachedFonts);
1083
1078
  // Verify each font has its stylesheet in the DOM
@@ -35,7 +35,6 @@ declare class VectorManager {
35
35
  private initialVectorsLoaded;
36
36
  private allVectorsLoaded;
37
37
  private cachedVectorList;
38
- private static readonly CACHE_KEY;
39
38
  private static readonly CACHE_DURATION;
40
39
  private static readonly LOADED_VECTORS_KEY;
41
40
  private static readonly CACHE_CONTROL;
@@ -1,4 +1,5 @@
1
1
  import { vectorUtils, vectorCategories } from "./vectorUtils";
2
+ import { readEditorStorageValue, writeEditorStorageValue } from "./editorStorage";
2
3
  class VectorManager {
3
4
  // Debug configuration
4
5
  static DEBUG_MODE = false;
@@ -17,9 +18,11 @@ class VectorManager {
17
18
  allVectorsLoaded = false;
18
19
  cachedVectorList = [];
19
20
  // Cache configuration
20
- static CACHE_KEY = 'svg_vectors_cache';
21
+ // (The vector list used to be kept in localStorage under `svg_vectors_cache`; that key is no longer read or removed,
22
+ // because the editor runs in customers' pages whose own keys it must not touch.)
21
23
  // Controls how long we keep the vector list in browser's localStorage
22
24
  static CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
25
+ // Stored in the editor's own corner of localStorage (see editorStorage.ts), not under a key the customer's page may use.
23
26
  static LOADED_VECTORS_KEY = 'loaded_vectors_cache';
24
27
  // Controls how long browsers should cache the actual vector files
25
28
  static CACHE_CONTROL = 'public, max-age=31536000'; // 1 year
@@ -218,7 +221,7 @@ class VectorManager {
218
221
  */
219
222
  async getBrowserCachedVectors() {
220
223
  try {
221
- const cachedVectorsList = localStorage.getItem(VectorManager.LOADED_VECTORS_KEY);
224
+ const cachedVectorsList = readEditorStorageValue(VectorManager.LOADED_VECTORS_KEY);
222
225
  const cachedVectors = cachedVectorsList ? JSON.parse(cachedVectorsList) : [];
223
226
  // console.log('📦 [VectorManager] Vectors in localStorage:', cachedVectors);
224
227
  // Verify each vector exists in the DOM/cache
@@ -247,9 +250,9 @@ class VectorManager {
247
250
  saveLoadedVectors() {
248
251
  try {
249
252
  const vectorsToCache = Array.from(this.loadedVectors);
250
- localStorage.setItem(VectorManager.LOADED_VECTORS_KEY, JSON.stringify(vectorsToCache));
253
+ writeEditorStorageValue(VectorManager.LOADED_VECTORS_KEY, JSON.stringify(vectorsToCache));
251
254
  // Verify the save operation
252
- const savedData = localStorage.getItem(VectorManager.LOADED_VECTORS_KEY);
255
+ const savedData = readEditorStorageValue(VectorManager.LOADED_VECTORS_KEY);
253
256
  // console.log('💾 [VectorManager] Vector cache status:', {
254
257
  // savedVectors: vectorsToCache,
255
258
  // verifiedCache: savedData ? JSON.parse(savedData) : null,
@@ -306,13 +309,8 @@ class VectorManager {
306
309
  * Saves vector list to cache (avoids writing 1.2MB JSON to localStorage)
307
310
  */
308
311
  saveVectorListToCache(vectors) {
309
- try {
310
- // Remove legacy large JSON from localStorage if present to free storage quota
311
- localStorage.removeItem(VectorManager.CACHE_KEY);
312
- }
313
- catch (error) {
314
- // Ignore storage errors
315
- }
312
+ // Nothing to save: the list is not written to localStorage. (This used to remove the large legacy JSON that an
313
+ // earlier version stored under `svg_vectors_cache`; the editor no longer touches keys it did not prefix itself.)
316
314
  }
317
315
  /**
318
316
  * Verifies if a vector is actually cached in the browser
@@ -326,7 +324,7 @@ class VectorManager {
326
324
  };
327
325
  try {
328
326
  // Check if vector is in localStorage
329
- const cachedVectors = localStorage.getItem(VectorManager.LOADED_VECTORS_KEY);
327
+ const cachedVectors = readEditorStorageValue(VectorManager.LOADED_VECTORS_KEY);
330
328
  const loadedVectors = cachedVectors ? JSON.parse(cachedVectors) : [];
331
329
  const isCached = loadedVectors.includes(name);
332
330
  // Find vector URL
@@ -0,0 +1,12 @@
1
+ import type { TextDirection } from '@widgetic/design-system/components/textDirection';
2
+ /**
3
+ * The `left` (in pixels, from the left edge of the editor's area) of a box that starts `startLeft` from the left edge of a
4
+ * left to right screen and is `width` wide, on the screen as it is read.
5
+ *
6
+ * On a left to right screen it is `startLeft` itself. On a right to left screen it is the box in the mirror: the box starts
7
+ * the same distance from the right edge, so what sat left of the middle sits right of it, with the same gap.
8
+ *
9
+ * The settings editor and the preview float over the editor's area and are put in place in pixels, because they can be
10
+ * dragged and resized. The mirror is made here, once, when they are docked, so nothing else about them has to know the direction.
11
+ */
12
+ export declare function leftOnThisScreen(direction: TextDirection, startLeft: number, width: number, areaWidth: number): number;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The `left` (in pixels, from the left edge of the editor's area) of a box that starts `startLeft` from the left edge of a
3
+ * left to right screen and is `width` wide, on the screen as it is read.
4
+ *
5
+ * On a left to right screen it is `startLeft` itself. On a right to left screen it is the box in the mirror: the box starts
6
+ * the same distance from the right edge, so what sat left of the middle sits right of it, with the same gap.
7
+ *
8
+ * The settings editor and the preview float over the editor's area and are put in place in pixels, because they can be
9
+ * dragged and resized. The mirror is made here, once, when they are docked, so nothing else about them has to know the direction.
10
+ */
11
+ export function leftOnThisScreen(direction, startLeft, width, areaWidth) {
12
+ return direction === 'rtl' ? areaWidth - startLeft - width : startLeft;
13
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The editor's own corner of the browser's localStorage.
3
+ *
4
+ * The editor can run inside a customer's page, and a page shares its localStorage with everything on it. A key such as
5
+ * `recentColors` or `recent_fonts` is as likely to be the customer's own as the editor's: reading it would take the
6
+ * customer's data for the editor's, writing it would overwrite theirs. So every key the editor uses starts with
7
+ * `widgetic:editor:`, and the editor only reads, writes and removes keys that do.
8
+ *
9
+ * Storage can also be unavailable (a browser setting, a private window, a frame without storage access) or full. None
10
+ * of that may stop the editor, so a failure is reported once and the call answers as if nothing were stored.
11
+ */
12
+ export declare const EDITOR_STORAGE_PREFIX = "widgetic:editor:";
13
+ /** What was stored under `key`, or null when nothing was (or storage is not usable). */
14
+ export declare function readEditorStorageValue(key: string): string | null;
15
+ /** Stores `value` under `key`. Answers whether it was stored. */
16
+ export declare function writeEditorStorageValue(key: string, value: string): boolean;
17
+ /** Forgets what was stored under `key`. */
18
+ export declare function removeEditorStorageValue(key: string): void;
19
+ /**
20
+ * Forgets everything the editor stored under a key that begins with `keyStart`, and nothing else: the customer's keys
21
+ * stay, even those with the same text in them. Answers how many were forgotten.
22
+ */
23
+ export declare function removeEditorStorageValuesStartingWith(keyStart: string): number;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The editor's own corner of the browser's localStorage.
3
+ *
4
+ * The editor can run inside a customer's page, and a page shares its localStorage with everything on it. A key such as
5
+ * `recentColors` or `recent_fonts` is as likely to be the customer's own as the editor's: reading it would take the
6
+ * customer's data for the editor's, writing it would overwrite theirs. So every key the editor uses starts with
7
+ * `widgetic:editor:`, and the editor only reads, writes and removes keys that do.
8
+ *
9
+ * Storage can also be unavailable (a browser setting, a private window, a frame without storage access) or full. None
10
+ * of that may stop the editor, so a failure is reported once and the call answers as if nothing were stored.
11
+ */
12
+ export const EDITOR_STORAGE_PREFIX = 'widgetic:editor:';
13
+ /** The kinds of failure already reported, so that each is reported once however often it happens. */
14
+ const reportedFailures = new Set();
15
+ function reportFailureOnce(kind, error) {
16
+ if (reportedFailures.has(kind))
17
+ return;
18
+ reportedFailures.add(kind);
19
+ console.warn(`[editorStorage] The browser's storage is not usable (${kind}); the editor goes on without it:`, error);
20
+ }
21
+ /** The browser's localStorage, or nothing where there is none (a server) or where the browser refuses access to it. */
22
+ function getBrowserStorage() {
23
+ try {
24
+ return typeof localStorage === 'undefined' ? null : localStorage;
25
+ }
26
+ catch (error) {
27
+ // Even looking at `localStorage` throws when the browser blocks storage for the page.
28
+ reportFailureOnce('blocked', error);
29
+ return null;
30
+ }
31
+ }
32
+ /** What was stored under `key`, or null when nothing was (or storage is not usable). */
33
+ export function readEditorStorageValue(key) {
34
+ const storage = getBrowserStorage();
35
+ if (!storage)
36
+ return null;
37
+ try {
38
+ return storage.getItem(EDITOR_STORAGE_PREFIX + key);
39
+ }
40
+ catch (error) {
41
+ reportFailureOnce('read', error);
42
+ return null;
43
+ }
44
+ }
45
+ /** Stores `value` under `key`. Answers whether it was stored. */
46
+ export function writeEditorStorageValue(key, value) {
47
+ const storage = getBrowserStorage();
48
+ if (!storage)
49
+ return false;
50
+ try {
51
+ storage.setItem(EDITOR_STORAGE_PREFIX + key, value);
52
+ return true;
53
+ }
54
+ catch (error) {
55
+ reportFailureOnce('write', error);
56
+ return false;
57
+ }
58
+ }
59
+ /** Forgets what was stored under `key`. */
60
+ export function removeEditorStorageValue(key) {
61
+ const storage = getBrowserStorage();
62
+ if (!storage)
63
+ return;
64
+ try {
65
+ storage.removeItem(EDITOR_STORAGE_PREFIX + key);
66
+ }
67
+ catch (error) {
68
+ reportFailureOnce('remove', error);
69
+ }
70
+ }
71
+ /**
72
+ * Forgets everything the editor stored under a key that begins with `keyStart`, and nothing else: the customer's keys
73
+ * stay, even those with the same text in them. Answers how many were forgotten.
74
+ */
75
+ export function removeEditorStorageValuesStartingWith(keyStart) {
76
+ const storage = getBrowserStorage();
77
+ if (!storage)
78
+ return 0;
79
+ try {
80
+ const fullKeyStart = EDITOR_STORAGE_PREFIX + keyStart;
81
+ // Collected first: removing a key while walking the keys would skip the one that moves into its place.
82
+ const keysToRemove = [];
83
+ for (let index = 0; index < storage.length; index += 1) {
84
+ const key = storage.key(index);
85
+ if (key !== null && key.startsWith(fullKeyStart))
86
+ keysToRemove.push(key);
87
+ }
88
+ keysToRemove.forEach((key) => storage.removeItem(key));
89
+ return keysToRemove.length;
90
+ }
91
+ catch (error) {
92
+ reportFailureOnce('remove', error);
93
+ return 0;
94
+ }
95
+ }
@@ -0,0 +1,13 @@
1
+ import { type TextDirection } from '@widgetic/design-system/components/textDirection';
2
+ /**
3
+ * The `dir` of the value of a single-line field on a screen that is read in `screenDirection`:
4
+ * - an address (an email, a link): left to right on a right to left screen, nothing on a left to right one;
5
+ * - text a person types: read by its own first letter on a right to left screen (`auto`), nothing on a left to right one.
6
+ * While the field is empty there is no first letter, and `auto` then reads left to right even on a right to left screen,
7
+ * which would put the hint of the field (and the caret) at its left edge: an empty field takes the direction of the screen.
8
+ *
9
+ * A left to right screen is written no attribute at all, as it always was.
10
+ *
11
+ * @param hasValue Whether anything is typed in the field yet (default: yes).
12
+ */
13
+ export declare function directionOfFieldValue(fieldType: string | undefined, screenDirection: TextDirection, hasValue?: boolean): 'auto' | 'ltr' | undefined;
@@ -0,0 +1,25 @@
1
+ import { dirForUserText } from '@widgetic/design-system/components/textDirection';
2
+ /**
3
+ * The kinds of field whose value is an address, which is read left to right on every screen.
4
+ *
5
+ * The `type` a field is given is the name of its kind in the schema (`textInput` for a plain text field, `email`, `url`), not
6
+ * an HTML type, so only these two are addresses and everything else is text a person types. (A first version took `text` for
7
+ * the only text and read every field of a schema as an address: what a visitor typed in Arabic was set from the left.)
8
+ */
9
+ const ADDRESS_FIELD_TYPES = new Set(['email', 'url']);
10
+ /**
11
+ * The `dir` of the value of a single-line field on a screen that is read in `screenDirection`:
12
+ * - an address (an email, a link): left to right on a right to left screen, nothing on a left to right one;
13
+ * - text a person types: read by its own first letter on a right to left screen (`auto`), nothing on a left to right one.
14
+ * While the field is empty there is no first letter, and `auto` then reads left to right even on a right to left screen,
15
+ * which would put the hint of the field (and the caret) at its left edge: an empty field takes the direction of the screen.
16
+ *
17
+ * A left to right screen is written no attribute at all, as it always was.
18
+ *
19
+ * @param hasValue Whether anything is typed in the field yet (default: yes).
20
+ */
21
+ export function directionOfFieldValue(fieldType, screenDirection, hasValue = true) {
22
+ if (fieldType && ADDRESS_FIELD_TYPES.has(fieldType.toLowerCase()))
23
+ return screenDirection === 'rtl' ? 'ltr' : undefined;
24
+ return hasValue ? dirForUserText(screenDirection) : undefined;
25
+ }
@@ -1,4 +1,5 @@
1
1
  import { writable } from 'svelte/store';
2
+ import { readEditorStorageValue, writeEditorStorageValue } from './editorStorage';
2
3
  // Define our category types with proper typing
3
4
  export const fontCategories = {
4
5
  // Google Fonts categories to our categories mapping
@@ -69,6 +70,7 @@ export const fontCategories = {
69
70
  return inferredCategory;
70
71
  }
71
72
  };
73
+ // Stored in the editor's own corner of localStorage (see editorStorage.ts), not under a key the customer's page may use.
72
74
  const RECENT_FONTS_KEY = 'recent_fonts';
73
75
  const MAX_RECENT_FONTS = 3;
74
76
  // Add store for recent fonts
@@ -76,7 +78,7 @@ export const recentFontsStore = writable([]);
76
78
  export const fontUtils = {
77
79
  getRecentFonts() {
78
80
  try {
79
- const stored = localStorage.getItem(RECENT_FONTS_KEY);
81
+ const stored = readEditorStorageValue(RECENT_FONTS_KEY);
80
82
  return stored ? JSON.parse(stored) : [];
81
83
  }
82
84
  catch (error) {
@@ -95,7 +97,7 @@ export const fontUtils = {
95
97
  },
96
98
  ...filteredFonts
97
99
  ].slice(0, MAX_RECENT_FONTS);
98
- localStorage.setItem(RECENT_FONTS_KEY, JSON.stringify(newRecentFonts));
100
+ writeEditorStorageValue(RECENT_FONTS_KEY, JSON.stringify(newRecentFonts));
99
101
  recentFontsStore.set(newRecentFonts);
100
102
  return newRecentFonts;
101
103
  }
@@ -108,7 +110,7 @@ export const fontUtils = {
108
110
  try {
109
111
  const recentFonts = this.getRecentFonts();
110
112
  const filteredFonts = recentFonts.filter(f => f.family !== family);
111
- localStorage.setItem(RECENT_FONTS_KEY, JSON.stringify(filteredFonts));
113
+ writeEditorStorageValue(RECENT_FONTS_KEY, JSON.stringify(filteredFonts));
112
114
  return filteredFonts;
113
115
  }
114
116
  catch (error) {
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Text from outside the editor (a file's name, a font's name, a label in a widget's schema, what a person typed) must
3
+ * never reach the page as markup or as style rules. The editor can run inside a customer's own page, where a name like
4
+ * `x" onerror="steal()` that breaks out of an attribute runs with the customer page's rights.
5
+ *
6
+ * Svelte's `{text}` and `attribute={value}` already escape for HTML. These helpers are for the places that build HTML
7
+ * as a string for `{@html ...}`, where nothing is escaped unless it is done here, on every piece, as it goes in, and
8
+ * for the places that put a name into a CSS rule, where HTML escaping does not help at all.
9
+ */
10
+ /** `text` as it must be written between tags or inside a quoted attribute value. Anything but text becomes its string. */
11
+ export declare function escapeHtml(text: unknown): string;
12
+ /**
13
+ * `text` as a quoted CSS string (`'Open Sans'`), whatever it contains. A font family name goes into a rule like
14
+ * `font-family: 'Open Sans', sans-serif`; a name such as `x'; background: url(//evil/a)` would otherwise end the string
15
+ * and add rules of its own, and escaping it for HTML does not stop that (the HTML parser turns `&#39;` back into the
16
+ * quote). Backslashes and single quotes are escaped; line breaks and other control characters, which end a CSS string,
17
+ * become spaces. The name still reaches the browser as the same name, so it matches the font that was loaded under it.
18
+ *
19
+ * When the result goes into an HTML attribute, escape it for HTML as well (`escapeHtml(cssQuotedString(name))`).
20
+ */
21
+ export declare function cssQuotedString(text: unknown): string;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Text from outside the editor (a file's name, a font's name, a label in a widget's schema, what a person typed) must
3
+ * never reach the page as markup or as style rules. The editor can run inside a customer's own page, where a name like
4
+ * `x" onerror="steal()` that breaks out of an attribute runs with the customer page's rights.
5
+ *
6
+ * Svelte's `{text}` and `attribute={value}` already escape for HTML. These helpers are for the places that build HTML
7
+ * as a string for `{@html ...}`, where nothing is escaped unless it is done here, on every piece, as it goes in, and
8
+ * for the places that put a name into a CSS rule, where HTML escaping does not help at all.
9
+ */
10
+ const HTML_ESCAPES = {
11
+ '&': '&amp;',
12
+ '<': '&lt;',
13
+ '>': '&gt;',
14
+ '"': '&quot;',
15
+ "'": '&#39;'
16
+ };
17
+ /** `text` as it must be written between tags or inside a quoted attribute value. Anything but text becomes its string. */
18
+ export function escapeHtml(text) {
19
+ if (text === null || text === undefined)
20
+ return '';
21
+ return String(text).replace(/[&<>"']/g, (character) => HTML_ESCAPES[character]);
22
+ }
23
+ /**
24
+ * `text` as a quoted CSS string (`'Open Sans'`), whatever it contains. A font family name goes into a rule like
25
+ * `font-family: 'Open Sans', sans-serif`; a name such as `x'; background: url(//evil/a)` would otherwise end the string
26
+ * and add rules of its own, and escaping it for HTML does not stop that (the HTML parser turns `&#39;` back into the
27
+ * quote). Backslashes and single quotes are escaped; line breaks and other control characters, which end a CSS string,
28
+ * become spaces. The name still reaches the browser as the same name, so it matches the font that was loaded under it.
29
+ *
30
+ * When the result goes into an HTML attribute, escape it for HTML as well (`escapeHtml(cssQuotedString(name))`).
31
+ */
32
+ export function cssQuotedString(text) {
33
+ const cleaned = String(text ?? '')
34
+ // eslint-disable-next-line no-control-regex
35
+ .replace(/[\u0000-\u001f\u007f\u2028\u2029]/g, ' ')
36
+ .replace(/[\\']/g, '\\$&');
37
+ return `'${cleaned}'`;
38
+ }