@widgetic/editor 4.0.10 → 4.0.11
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/README.md +118 -0
- package/dist/components/WidgetPreview.svelte +65 -27
- package/dist/components/WidgetPreview.svelte.d.ts +8 -0
- package/dist/components/editors/CompositionEditorApp.svelte +508 -0
- package/dist/components/editors/CompositionEditorApp.svelte.d.ts +75 -0
- package/dist/components/editors/PropsEditor.svelte +193 -792
- package/dist/components/editors/PropsEditor.svelte.d.ts +7 -0
- package/dist/components/ic/BrowserInputController.svelte +21 -2
- package/dist/components/ic/ColorPickerInputController.svelte +24 -6
- package/dist/components/ic/DateFieldInputController.svelte +7 -3
- package/dist/components/ic/DatePickerInputController.svelte +10 -4
- package/dist/components/ic/DropdownInputController.svelte +11 -4
- package/dist/components/ic/FontInputController.svelte +68 -49
- package/dist/components/ic/InputControllerHeader.svelte +11 -8
- package/dist/components/ic/NumberInputController.svelte +4 -2
- package/dist/components/ic/PositionInputController.svelte +14 -11
- package/dist/components/ic/RangeCalendarInputController.svelte +10 -4
- package/dist/components/ic/RoundedCornersInputController.svelte +17 -9
- package/dist/components/ic/TextAreaInputController.svelte +20 -7
- package/dist/components/ic/TextAreaPreviewInputController.svelte +19 -69
- package/dist/components/ic/TimeInputController.svelte +9 -3
- package/dist/components/ic/VectorInputController.svelte +80 -54
- package/dist/components/ic/shared/ItemGrid.svelte +5 -1
- package/dist/composition/compositionApi.d.ts +95 -0
- package/dist/composition/compositionApi.js +289 -0
- package/dist/composition/compositionEditorController.d.ts +254 -0
- package/dist/composition/compositionEditorController.js +734 -0
- package/dist/composition/compositionHelpers.d.ts +72 -0
- package/dist/composition/compositionHelpers.js +403 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/mediaLibraryContext.d.ts +10 -0
- package/dist/mediaLibraryContext.js +30 -0
- package/dist/utils/FontManager.d.ts +21 -5
- package/dist/utils/FontManager.js +62 -67
- package/dist/utils/VectorManager.d.ts +0 -1
- package/dist/utils/VectorManager.js +10 -12
- package/dist/utils/editorStorage.d.ts +23 -0
- package/dist/utils/editorStorage.js +95 -0
- package/dist/utils/fontUtils.js +5 -3
- package/dist/utils/htmlSafety.d.ts +21 -0
- package/dist/utils/htmlSafety.js +38 -0
- package/dist/utils/itemReorder.d.ts +90 -0
- package/dist/utils/itemReorder.js +354 -0
- package/dist/utils/recentColors.js +4 -2
- package/dist/utils/richTextPreview.d.ts +38 -0
- package/dist/utils/richTextPreview.js +207 -0
- package/dist/utils/scrollActivity.d.ts +14 -0
- package/dist/utils/scrollActivity.js +63 -0
- package/dist/utils/shadowDom.d.ts +67 -0
- package/dist/utils/shadowDom.js +211 -0
- package/dist/utils/vectorUtils.js +5 -3
- package/dist/utils/visibilityCondition.d.ts +40 -0
- package/dist/utils/visibilityCondition.js +535 -0
- package/package.json +6 -5
|
@@ -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
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
378
|
+
* Loads previously cached fonts from the durable L1 store
|
|
374
379
|
*/
|
|
375
380
|
async loadCachedFonts() {
|
|
376
381
|
try {
|
|
377
|
-
|
|
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
|
-
//
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
-
|
|
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
|
-
|
|
508
|
-
//
|
|
509
|
-
|
|
510
|
-
|
|
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
|
|
680
|
+
* Gets font list from the durable L1 cache
|
|
666
681
|
*/
|
|
667
682
|
async getFontListFromCache() {
|
|
668
683
|
try {
|
|
669
|
-
|
|
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
|
|
1044
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
253
|
+
writeEditorStorageValue(VectorManager.LOADED_VECTORS_KEY, JSON.stringify(vectorsToCache));
|
|
251
254
|
// Verify the save operation
|
|
252
|
-
const savedData =
|
|
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
|
-
|
|
310
|
-
|
|
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 =
|
|
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,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
|
+
}
|
package/dist/utils/fontUtils.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 `'` 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
|
+
'&': '&',
|
|
12
|
+
'<': '<',
|
|
13
|
+
'>': '>',
|
|
14
|
+
'"': '"',
|
|
15
|
+
"'": '''
|
|
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 `'` 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
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reordering a list of items by dragging a handle.
|
|
3
|
+
*
|
|
4
|
+
* This replaces @shopify/draggable, which cannot work inside a shadow root (the editor runs in one when it is a
|
|
5
|
+
* module on a customer's page):
|
|
6
|
+
* - it listens on the `document`, where every event that comes from inside a shadow root has the shadow host as its
|
|
7
|
+
* target, so it never finds the list it was given and no drag ever starts;
|
|
8
|
+
* - the events it makes itself do not leave the shadow root, so its own listeners on the `document` never hear them;
|
|
9
|
+
* - it adds classes to the page's `<body>`, switches text selection off on it, appends its floating copy of the item
|
|
10
|
+
* to it, and leaves a screen reader region in it that stays after the editor is gone.
|
|
11
|
+
*
|
|
12
|
+
* This one listens on the list itself, keeps hold of the pointer with pointer capture (so the drag carries on over the
|
|
13
|
+
* preview frame and outside the editor), works out where an item would be dropped from where the items are on the
|
|
14
|
+
* screen, and leaves the page it runs in alone. It never moves the items itself: the list changes only when the
|
|
15
|
+
* caller changes the data the list is drawn from, in `onReorder`.
|
|
16
|
+
*/
|
|
17
|
+
/** How far (px) the pointer must move from where it was pressed before the press counts as a drag and not a click. */
|
|
18
|
+
export declare const DRAG_START_DISTANCE_PX = 4;
|
|
19
|
+
/** The height (px) of the copy of the item that follows the pointer. */
|
|
20
|
+
export declare const MIRROR_HEIGHT_PX = 56;
|
|
21
|
+
/** How close (px) to the top or bottom edge of the scrolling area the pointer must be for the area to start scrolling. */
|
|
22
|
+
export declare const EDGE_SCROLL_ZONE_PX = 56;
|
|
23
|
+
/** The most (px) the scrolling area moves in one frame, reached when the pointer is at or beyond the edge. */
|
|
24
|
+
export declare const EDGE_SCROLL_MAX_STEP_PX = 18;
|
|
25
|
+
/** The classes put on the list and its items while one is dragged; the editor's stylesheet draws them. */
|
|
26
|
+
export declare const REORDER_CLASSES: {
|
|
27
|
+
/** On the list for as long as an item is being dragged. */
|
|
28
|
+
readonly list: "is-reordering";
|
|
29
|
+
/** On the item being dragged. */
|
|
30
|
+
readonly source: "is-reorder-source";
|
|
31
|
+
/** On the item the dragged one would be dropped in front of. */
|
|
32
|
+
readonly dropBefore: "is-reorder-drop-before";
|
|
33
|
+
/** On the last item, when the dragged one would be dropped behind it. */
|
|
34
|
+
readonly dropAfter: "is-reorder-drop-after";
|
|
35
|
+
/** On the floating copy of the dragged item. */
|
|
36
|
+
readonly mirror: "reorder-mirror";
|
|
37
|
+
};
|
|
38
|
+
/** The top and height of an item on the screen. */
|
|
39
|
+
export interface VerticalBox {
|
|
40
|
+
top: number;
|
|
41
|
+
height: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Where a dragged item ends up when it is dropped with the pointer at `pointerY`: its index in the list once it has
|
|
45
|
+
* been taken out and put back. `otherItems` are the boxes of the items on the screen without the dragged one. The
|
|
46
|
+
* dragged item goes after every item whose middle is above the pointer.
|
|
47
|
+
*/
|
|
48
|
+
export declare function dropIndexForPointer(otherItems: readonly VerticalBox[], pointerY: number): number;
|
|
49
|
+
/**
|
|
50
|
+
* How far (px) to scroll the area that holds the list in this frame: upwards (negative) when the pointer is in the
|
|
51
|
+
* top `zone` of the area or above it, downwards (positive) in the bottom `zone` or below it, faster the nearer the
|
|
52
|
+
* edge, and 0 anywhere else.
|
|
53
|
+
*/
|
|
54
|
+
export declare function edgeScrollStep(areaTop: number, areaBottom: number, pointerY: number, zone?: number, maxStep?: number): number;
|
|
55
|
+
/**
|
|
56
|
+
* A copy of `items` with the item at `oldIndex` moved to `newIndex` (its index in the copy). When either index is not
|
|
57
|
+
* a place in the list, the copy keeps the original order.
|
|
58
|
+
*/
|
|
59
|
+
export declare function moveListItem<ItemType>(items: readonly ItemType[], oldIndex: number, newIndex: number): ItemType[];
|
|
60
|
+
/** What `reorderableList` needs to know about the list. */
|
|
61
|
+
export interface ItemReorderOptions {
|
|
62
|
+
/** CSS selector of one item. Items are direct children of the list. */
|
|
63
|
+
itemSelector: string;
|
|
64
|
+
/** CSS selector of the part of an item that starts a drag when it is pressed. */
|
|
65
|
+
handleSelector: string;
|
|
66
|
+
/** The drag began (the pointer moved far enough from where it was pressed). The list is not changed yet. */
|
|
67
|
+
onDragStart?: (drag: {
|
|
68
|
+
item: HTMLElement;
|
|
69
|
+
index: number;
|
|
70
|
+
}) => void;
|
|
71
|
+
/** The item was dropped at another place in the list. Change the data the list is drawn from here. */
|
|
72
|
+
onReorder: (move: {
|
|
73
|
+
item: HTMLElement;
|
|
74
|
+
oldIndex: number;
|
|
75
|
+
newIndex: number;
|
|
76
|
+
}) => void;
|
|
77
|
+
/** The drag is over: dropped (`reordered` tells if the order changed), cancelled with Escape, or the list went away. */
|
|
78
|
+
onDragEnd?: (outcome: {
|
|
79
|
+
reordered: boolean;
|
|
80
|
+
}) => void;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Svelte action that makes the direct children of a list reorderable by dragging their handle:
|
|
84
|
+
* `<div use:reorderableList={{ itemSelector, handleSelector, onReorder }}>`.
|
|
85
|
+
*/
|
|
86
|
+
export declare function reorderableList(list: HTMLElement, initialOptions: ItemReorderOptions): {
|
|
87
|
+
/** Svelte calls this when the options change; the callbacks of the new options are the ones used from then on. */
|
|
88
|
+
update(nextOptions: ItemReorderOptions): void;
|
|
89
|
+
destroy(): void;
|
|
90
|
+
};
|