@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.
- package/README.md +242 -0
- package/dist/components/WidgetPreview.svelte +123 -86
- package/dist/components/WidgetPreview.svelte.d.ts +8 -0
- package/dist/components/color-select/ColorSelect.svelte +4 -4
- package/dist/components/editors/CompositionEditorApp.svelte +562 -0
- package/dist/components/editors/CompositionEditorApp.svelte.d.ts +95 -0
- package/dist/components/editors/EmbedEditor.svelte +33 -22
- package/dist/components/editors/EmbedEditor.svelte.d.ts +1 -1
- package/dist/components/editors/PropsEditor.svelte +267 -850
- package/dist/components/editors/PropsEditor.svelte.d.ts +10 -3
- package/dist/components/ic/AngleInputController.svelte +1 -1
- package/dist/components/ic/BrowserInputController.svelte +58 -31
- package/dist/components/ic/ColorPickerInputController.svelte +55 -21
- package/dist/components/ic/DateFieldInputController.svelte +34 -14
- package/dist/components/ic/DatePickerInputController.svelte +37 -21
- package/dist/components/ic/DropdownInputController.svelte +21 -9
- package/dist/components/ic/DropdownInputController.svelte.d.ts +1 -1
- package/dist/components/ic/FixedPositionInputController.svelte +12 -6
- package/dist/components/ic/FontInputController.svelte +100 -76
- package/dist/components/ic/InputControllerHeader.svelte +16 -9
- package/dist/components/ic/NumberControls.svelte +4 -4
- package/dist/components/ic/NumberInputController.svelte +6 -4
- package/dist/components/ic/PositionInputController.svelte +23 -15
- package/dist/components/ic/RangeCalendarInputController.svelte +36 -20
- package/dist/components/ic/RoundedCornersInputController.svelte +39 -26
- package/dist/components/ic/SliderInputController.svelte +7 -2
- package/dist/components/ic/SwitchInputController.svelte +12 -3
- package/dist/components/ic/TagsInputController.svelte +19 -4
- package/dist/components/ic/TextAreaInputController.svelte +79 -51
- package/dist/components/ic/TextAreaPreviewInputController.svelte +27 -72
- package/dist/components/ic/TextInputController.svelte +17 -1
- package/dist/components/ic/TimeInputController.svelte +11 -4
- package/dist/components/ic/VectorInputController.svelte +101 -70
- package/dist/components/ic/shared/ItemGrid.svelte +12 -4
- package/dist/components/ic/shared/RecentItemsSection.svelte +16 -11
- package/dist/components/ic/shared/RecentItemsSection.svelte.d.ts +1 -1
- package/dist/composition/compositionApi.d.ts +95 -0
- package/dist/composition/compositionApi.js +289 -0
- package/dist/composition/compositionEditorController.d.ts +263 -0
- package/dist/composition/compositionEditorController.js +746 -0
- package/dist/composition/compositionHelpers.d.ts +74 -0
- package/dist/composition/compositionHelpers.js +405 -0
- package/dist/i18n/index.d.ts +4 -0
- package/dist/i18n/index.js +4 -0
- package/dist/i18n/labels.d.ts +15 -0
- package/dist/i18n/labels.js +57 -0
- package/dist/i18n/language.d.ts +228 -0
- package/dist/i18n/language.js +34 -0
- package/dist/i18n/languageKit.d.ts +54 -0
- package/dist/i18n/languageKit.js +102 -0
- package/dist/i18n/messages.d.ts +241 -0
- package/dist/i18n/messages.js +253 -0
- package/dist/i18n/translate.d.ts +6 -0
- package/dist/i18n/translate.js +8 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +8 -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/dockPlacement.d.ts +12 -0
- package/dist/utils/dockPlacement.js +13 -0
- package/dist/utils/editorStorage.d.ts +23 -0
- package/dist/utils/editorStorage.js +95 -0
- package/dist/utils/fieldDirection.d.ts +13 -0
- package/dist/utils/fieldDirection.js +25 -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 +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
|
-
//
|
|
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,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
|
+
}
|
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
|
+
}
|