@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
|
@@ -55,6 +55,11 @@
|
|
|
55
55
|
import { SearchInput } from '@widgetic/design-system/components';
|
|
56
56
|
import CategoryTabs from './shared/CategoryTabs.svelte';
|
|
57
57
|
import { RecentItemsSection, ItemGrid } from './shared';
|
|
58
|
+
import { useEditorLanguage } from '../../i18n/language';
|
|
59
|
+
import { categoryLabel } from '../../i18n/labels';
|
|
60
|
+
|
|
61
|
+
// The words of the editor in the language of the page.
|
|
62
|
+
const { t } = useEditorLanguage();
|
|
58
63
|
|
|
59
64
|
// Utils imports
|
|
60
65
|
import {
|
|
@@ -66,6 +71,9 @@
|
|
|
66
71
|
} from '../../utils/vectorUtils';
|
|
67
72
|
import type { VectorData } from '../../utils/VectorManager';
|
|
68
73
|
import VectorManager from '../../utils/VectorManager';
|
|
74
|
+
import { escapeHtml } from '../../utils/htmlSafety';
|
|
75
|
+
import { listenForScroll, queryInTreeOf } from '../../utils/shadowDom';
|
|
76
|
+
import { readEditorStorageValue, writeEditorStorageValue } from '../../utils/editorStorage';
|
|
69
77
|
|
|
70
78
|
// Constants
|
|
71
79
|
const VECTOR_FILTER_KEY = (id: string) => `vector-specific-filter-state-${id}`;
|
|
@@ -109,6 +117,8 @@
|
|
|
109
117
|
|
|
110
118
|
// Vector loading optimization
|
|
111
119
|
let vectorListContainer: HTMLElement;
|
|
120
|
+
// The control's own element: it says which tree (the page or a module's shadow root) to listen for scrolling in
|
|
121
|
+
let controllerElement: HTMLDivElement | undefined;
|
|
112
122
|
let vectorObserver = new IntersectionObserver(() => {});
|
|
113
123
|
|
|
114
124
|
// Initialize VectorManager instance
|
|
@@ -175,6 +185,15 @@
|
|
|
175
185
|
`;
|
|
176
186
|
}
|
|
177
187
|
|
|
188
|
+
/**
|
|
189
|
+
* The sticky header of the vector list. It is looked up in the tree the list lives in (inside a module that is a
|
|
190
|
+
* shadow root, which `document.querySelector` cannot see into), and the id is escaped so that one with dots or
|
|
191
|
+
* brackets in it is found too.
|
|
192
|
+
*/
|
|
193
|
+
function findStickyHeader(): Element | null {
|
|
194
|
+
return queryInTreeOf(vectorListContainer, `#${CSS.escape(`vector-header-${controllerId}`)}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
178
197
|
// Function to update scroll state
|
|
179
198
|
function updateScrollState() {
|
|
180
199
|
if (vectorListContainer) {
|
|
@@ -183,7 +202,7 @@
|
|
|
183
202
|
|
|
184
203
|
// If we're transitioning from scrolled to not scrolled, remove transition
|
|
185
204
|
if (wasScrolled && !isScrolled) {
|
|
186
|
-
const stickyHeader =
|
|
205
|
+
const stickyHeader = findStickyHeader();
|
|
187
206
|
if (stickyHeader) {
|
|
188
207
|
stickyHeader.classList.remove('transition-border');
|
|
189
208
|
// Force a reflow to ensure the transition class removal takes effect
|
|
@@ -191,7 +210,7 @@
|
|
|
191
210
|
}
|
|
192
211
|
} else if (!wasScrolled && isScrolled) {
|
|
193
212
|
// Add transition class when starting to scroll
|
|
194
|
-
const stickyHeader =
|
|
213
|
+
const stickyHeader = findStickyHeader();
|
|
195
214
|
if (stickyHeader) {
|
|
196
215
|
stickyHeader.classList.add('transition-border');
|
|
197
216
|
}
|
|
@@ -314,31 +333,14 @@
|
|
|
314
333
|
await handleSearch(event);
|
|
315
334
|
}, 300); // 300ms debounce time
|
|
316
335
|
|
|
317
|
-
//
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const vectorName = event.detail;
|
|
321
|
-
// Check if we can get the vector URL from allVectors
|
|
322
|
-
const vectorData = allVectors.find(v => v.name === vectorName);
|
|
323
|
-
const vectorUrl = vectorData?.url || 'unknown';
|
|
324
|
-
|
|
325
|
-
console.error(`❌ [VectorInput] Failed to load SVG: "${vectorName}" from URL: ${vectorUrl}`);
|
|
326
|
-
svgLoadErrors.set(vectorName, true);
|
|
327
|
-
// Force component update
|
|
328
|
-
svgLoadErrors = new Map(svgLoadErrors);
|
|
329
|
-
}
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
// Add the event listener
|
|
333
|
-
document.addEventListener('vector-load-error', vectorErrorHandler);
|
|
334
|
-
|
|
335
|
-
// Store the handler reference for cleanup in onDestroy
|
|
336
|
-
(window as any)['vectorErrorHandler'] = vectorErrorHandler;
|
|
336
|
+
// SVG loading errors are noticed by the watchVectorImageErrors action on the list of pictures. They used to be
|
|
337
|
+
// announced on `document` by an inline onerror handler and caught here; with the editor inside a partner's page
|
|
338
|
+
// that was an event on the partner's document and a handler stored on the partner's `window`.
|
|
337
339
|
|
|
338
340
|
try {
|
|
339
341
|
if (browser) {
|
|
340
342
|
// Load saved filter
|
|
341
|
-
const savedFilter =
|
|
343
|
+
const savedFilter = readEditorStorageValue(VECTOR_FILTER_KEY(controllerId));
|
|
342
344
|
if (savedFilter) {
|
|
343
345
|
currentFilter = savedFilter as FilterType;
|
|
344
346
|
}
|
|
@@ -493,7 +495,7 @@
|
|
|
493
495
|
lastActiveFilter = currentFilter;
|
|
494
496
|
currentFilter = 'All'; // Reset to All when starting a search
|
|
495
497
|
if (browser) {
|
|
496
|
-
|
|
498
|
+
writeEditorStorageValue(VECTOR_FILTER_KEY(controllerId), 'All');
|
|
497
499
|
}
|
|
498
500
|
}
|
|
499
501
|
|
|
@@ -970,16 +972,7 @@
|
|
|
970
972
|
}
|
|
971
973
|
|
|
972
974
|
onDestroy(() => {
|
|
973
|
-
|
|
974
|
-
window.removeEventListener('touchstart', () => {});
|
|
975
|
-
window.removeEventListener('touchmove', () => {});
|
|
976
|
-
|
|
977
|
-
// Remove vector load error listener using the stored reference
|
|
978
|
-
const vectorErrorHandler = (window as any)['vectorErrorHandler'];
|
|
979
|
-
if (vectorErrorHandler) {
|
|
980
|
-
document.removeEventListener('vector-load-error', vectorErrorHandler);
|
|
981
|
-
delete (window as any)['vectorErrorHandler'];
|
|
982
|
-
}
|
|
975
|
+
stopListeningForScroll();
|
|
983
976
|
|
|
984
977
|
if (vectorObserver) {
|
|
985
978
|
vectorObserver.disconnect();
|
|
@@ -1030,7 +1023,7 @@
|
|
|
1030
1023
|
lastActiveFilter = filter; // Remember the last active filter
|
|
1031
1024
|
|
|
1032
1025
|
if (browser) {
|
|
1033
|
-
|
|
1026
|
+
writeEditorStorageValue(VECTOR_FILTER_KEY(controllerId), filter);
|
|
1034
1027
|
}
|
|
1035
1028
|
|
|
1036
1029
|
if (vectorManagerInstance) {
|
|
@@ -1104,25 +1097,26 @@
|
|
|
1104
1097
|
}
|
|
1105
1098
|
|
|
1106
1099
|
/**
|
|
1107
|
-
* Highlights search term matches in vector names
|
|
1100
|
+
* Highlights search term matches in vector names.
|
|
1101
|
+
* The result is meant for {@html}, so every piece of the name is escaped as it is put in.
|
|
1108
1102
|
*/
|
|
1109
1103
|
function highlightMatch(name: string, term: string) {
|
|
1110
|
-
if (!term) return name;
|
|
1104
|
+
if (!term) return escapeHtml(name);
|
|
1111
1105
|
const nameLower = name.toLowerCase();
|
|
1112
1106
|
const termLower = term.toLowerCase().trim();
|
|
1113
1107
|
|
|
1114
1108
|
// If the name contains the search term, highlight that part in blue
|
|
1115
1109
|
if (termLower && nameLower.includes(termLower)) {
|
|
1116
1110
|
const index = nameLower.indexOf(termLower);
|
|
1117
|
-
const beforeMatch = name.slice(0, index);
|
|
1118
|
-
const match = name.slice(index, index + termLower.length);
|
|
1119
|
-
const afterMatch = name.slice(index + termLower.length);
|
|
1111
|
+
const beforeMatch = escapeHtml(name.slice(0, index));
|
|
1112
|
+
const match = escapeHtml(name.slice(index, index + termLower.length));
|
|
1113
|
+
const afterMatch = escapeHtml(name.slice(index + termLower.length));
|
|
1120
1114
|
|
|
1121
1115
|
return `<span style="color: hsl(var(--grey7))">${beforeMatch}<span style="color: #45BCFE">${match}</span>${afterMatch}</span>`;
|
|
1122
1116
|
}
|
|
1123
1117
|
|
|
1124
1118
|
// Default case - just return the name in grey7 color
|
|
1125
|
-
return `<span style="color: hsl(var(--grey7))">${name}</span>`;
|
|
1119
|
+
return `<span style="color: hsl(var(--grey7))">${escapeHtml(name)}</span>`;
|
|
1126
1120
|
}
|
|
1127
1121
|
|
|
1128
1122
|
|
|
@@ -1173,15 +1167,16 @@
|
|
|
1173
1167
|
}
|
|
1174
1168
|
}
|
|
1175
1169
|
|
|
1176
|
-
// Add scroll listener with passive option
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1170
|
+
// Add scroll listener with passive option. It listens to the page and, inside a module, to the module's own panels
|
|
1171
|
+
// (a scroll never leaves a shadow root, so a listener on the window alone does not hear about those).
|
|
1172
|
+
let stopListeningForScroll: () => void = () => {};
|
|
1173
|
+
onMount(() => {
|
|
1174
|
+
stopListeningForScroll = listenForScroll(controllerElement, handleGlobalScroll, { passive: true });
|
|
1180
1175
|
});
|
|
1181
1176
|
|
|
1182
|
-
//
|
|
1183
|
-
|
|
1184
|
-
|
|
1177
|
+
// (Two touchstart/touchmove listeners that did nothing used to be added to the window here, and could never be
|
|
1178
|
+
// removed: each input left two more on the customer's page every time it was shown. They did nothing, so they are
|
|
1179
|
+
// gone.)
|
|
1185
1180
|
|
|
1186
1181
|
// Add to VectorInputController.svelte
|
|
1187
1182
|
function handleToggle() {
|
|
@@ -1299,6 +1294,37 @@
|
|
|
1299
1294
|
removeVector(vector.name);
|
|
1300
1295
|
}
|
|
1301
1296
|
|
|
1297
|
+
/**
|
|
1298
|
+
* Svelte action for the list that holds the vector pictures: notices a picture that does not load and shows the
|
|
1299
|
+
* "Failed to load" state for that vector. `error` events do not bubble, so it listens in the capture phase; a
|
|
1300
|
+
* picture is found by its data-vector-name (see renderVectorItem). It replaces an inline onerror attribute that
|
|
1301
|
+
* reported through `document`, which is the partner's page when the editor runs inside one.
|
|
1302
|
+
*/
|
|
1303
|
+
function watchVectorImageErrors(node: HTMLElement) {
|
|
1304
|
+
const onImageError = (event: Event) => {
|
|
1305
|
+
const image = event.target;
|
|
1306
|
+
if (!(image instanceof HTMLImageElement)) return;
|
|
1307
|
+
const vectorName = image.dataset.vectorName;
|
|
1308
|
+
if (!vectorName) return;
|
|
1309
|
+
image.style.display = 'none';
|
|
1310
|
+
// Reported once per vector, however many pictures of it are on screen.
|
|
1311
|
+
if (hasSvgLoadError(vectorName)) return;
|
|
1312
|
+
const vectorData = allVectors.find(vector => vector.name === vectorName);
|
|
1313
|
+
console.error(`❌ [VectorInput] Failed to load SVG: "${vectorName}" from URL: ${vectorData?.url || image.src || 'unknown'}`);
|
|
1314
|
+
svgLoadErrors.set(vectorName, true);
|
|
1315
|
+
// Force component update
|
|
1316
|
+
svgLoadErrors = new Map(svgLoadErrors);
|
|
1317
|
+
};
|
|
1318
|
+
|
|
1319
|
+
node.addEventListener('error', onImageError, true);
|
|
1320
|
+
|
|
1321
|
+
return {
|
|
1322
|
+
destroy() {
|
|
1323
|
+
node.removeEventListener('error', onImageError, true);
|
|
1324
|
+
}
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1302
1328
|
// Create a Svelte action to handle image error events
|
|
1303
1329
|
function handleImageError(node: HTMLElement, vectorName: string) {
|
|
1304
1330
|
const errorHandler = (event: Event) => {
|
|
@@ -1345,15 +1371,19 @@
|
|
|
1345
1371
|
};
|
|
1346
1372
|
}
|
|
1347
1373
|
|
|
1374
|
+
// The result is used as HTML by RecentItemsSection and ItemGrid: the name and the address of a vector (a custom
|
|
1375
|
+
// one is named after the person's own file) are escaped as they are put in. A picture that does not load is
|
|
1376
|
+
// noticed by watchVectorImageErrors, which finds it by data-vector-name; an inline onerror="..." would not run
|
|
1377
|
+
// under a strict Content-Security-Policy.
|
|
1348
1378
|
return {
|
|
1349
1379
|
html: `
|
|
1350
1380
|
<div class="flex items-center justify-center w-full h-full vector-img-container">
|
|
1351
1381
|
<img
|
|
1352
|
-
src="${vector.url}"
|
|
1353
|
-
alt="${vector.name}"
|
|
1382
|
+
src="${escapeHtml(vector.url)}"
|
|
1383
|
+
alt="${escapeHtml(vector.name)}"
|
|
1384
|
+
data-vector-name="${escapeHtml(vector.name)}"
|
|
1354
1385
|
class="w-[72px] h-[72px] object-contain"
|
|
1355
1386
|
draggable="false"
|
|
1356
|
-
onerror="this.style.display='none'; document.dispatchEvent(new CustomEvent('vector-load-error', {detail: this.alt, bubbles: true})); console.error('❌ [VectorInput] Vector failed to load:', this.alt, 'URL:', this.src);"
|
|
1357
1387
|
/>
|
|
1358
1388
|
</div>
|
|
1359
1389
|
`
|
|
@@ -1399,7 +1429,7 @@
|
|
|
1399
1429
|
lastActiveFilter = currentFilter;
|
|
1400
1430
|
currentFilter = 'All'; // Reset to All when starting a search
|
|
1401
1431
|
if (browser) {
|
|
1402
|
-
|
|
1432
|
+
writeEditorStorageValue(VECTOR_FILTER_KEY(controllerId), 'All');
|
|
1403
1433
|
}
|
|
1404
1434
|
}
|
|
1405
1435
|
|
|
@@ -1472,16 +1502,16 @@
|
|
|
1472
1502
|
}
|
|
1473
1503
|
</script>
|
|
1474
1504
|
|
|
1475
|
-
<div class="flex flex-col gap-2 widgetic-colors">
|
|
1505
|
+
<div bind:this={controllerElement} class="flex flex-col gap-2 widgetic-colors">
|
|
1476
1506
|
<div class="flex items-center w-full relative">
|
|
1477
|
-
<div class="w-[185px] flex-shrink-0
|
|
1507
|
+
<div class="w-[185px] flex-shrink-0 ms-auto">
|
|
1478
1508
|
<InputControllerHeader
|
|
1479
1509
|
label={label}
|
|
1480
1510
|
helpText={description}
|
|
1481
1511
|
/>
|
|
1482
1512
|
</div>
|
|
1483
1513
|
|
|
1484
|
-
<div class="absolute
|
|
1514
|
+
<div class="absolute end-0">
|
|
1485
1515
|
<div
|
|
1486
1516
|
class="h-[48px] w-[185px] rounded-large border border-grey2 flex items-center bg-grey1 transition-colors duration-200 hover:border-forest-green"
|
|
1487
1517
|
onfocusin={onFocus}
|
|
@@ -1527,7 +1557,7 @@
|
|
|
1527
1557
|
}}
|
|
1528
1558
|
>
|
|
1529
1559
|
<Popover.Trigger
|
|
1530
|
-
class="w-full h-full flex items-center border-0 bg-transparent hover:bg-transparent focus:bg-transparent
|
|
1560
|
+
class="w-full h-full flex items-center border-0 bg-transparent hover:bg-transparent focus:bg-transparent ps-3 pe-[16px] group"
|
|
1531
1561
|
onclick={handleButtonClick}
|
|
1532
1562
|
onkeydown={handleKeyDown}
|
|
1533
1563
|
onmouseenter={() => {
|
|
@@ -1552,8 +1582,8 @@
|
|
|
1552
1582
|
<path d="M12 17h.01"></path>
|
|
1553
1583
|
</svg>
|
|
1554
1584
|
</div>
|
|
1555
|
-
<span class="truncate text-
|
|
1556
|
-
{value?.name ||
|
|
1585
|
+
<span class="truncate text-start capitalize subtext text-grey7">
|
|
1586
|
+
{value?.name || $t('editor.vector.select')}
|
|
1557
1587
|
</span>
|
|
1558
1588
|
{:else}
|
|
1559
1589
|
<!-- Use a simple img tag with Svelte action for error handling -->
|
|
@@ -1563,13 +1593,13 @@
|
|
|
1563
1593
|
class="w-5 h-5 object-contain"
|
|
1564
1594
|
use:handleImageError={value.name}
|
|
1565
1595
|
/>
|
|
1566
|
-
<span class="truncate text-
|
|
1567
|
-
{value?.name ||
|
|
1596
|
+
<span class="truncate text-start capitalize subtext text-grey7">
|
|
1597
|
+
{value?.name || $t('editor.vector.select')}
|
|
1568
1598
|
</span>
|
|
1569
1599
|
{/if}
|
|
1570
1600
|
{:else}
|
|
1571
|
-
<span class="truncate text-
|
|
1572
|
-
|
|
1601
|
+
<span class="truncate text-start subtext text-grey7">
|
|
1602
|
+
{$t('editor.vector.select')}
|
|
1573
1603
|
</span>
|
|
1574
1604
|
{/if}
|
|
1575
1605
|
</div>
|
|
@@ -1580,10 +1610,10 @@
|
|
|
1580
1610
|
<div class="flex flex-col h-full vector-popover-content">
|
|
1581
1611
|
<!-- Title and upload icon -->
|
|
1582
1612
|
<div class="relative pt-6 pb-[8px] px-4">
|
|
1583
|
-
<div class="h5 text-grey7 widgetic-colors">
|
|
1613
|
+
<div class="h5 text-grey7 widgetic-colors">{$t('editor.vector.title')}</div>
|
|
1584
1614
|
<button
|
|
1585
1615
|
type="button"
|
|
1586
|
-
class="absolute
|
|
1616
|
+
class="absolute end-6 top-[26px] bg-transparent border-0 cursor-pointer p-0 rounded-full hover:bg-grey3 transition-all duration-300"
|
|
1587
1617
|
onclick={() => {
|
|
1588
1618
|
try {
|
|
1589
1619
|
openFileBrowser();
|
|
@@ -1603,7 +1633,7 @@
|
|
|
1603
1633
|
<!-- Search input -->
|
|
1604
1634
|
<div class="relative px-4">
|
|
1605
1635
|
<SearchInput
|
|
1606
|
-
placeholder=
|
|
1636
|
+
placeholder={$t('editor.vector.search')}
|
|
1607
1637
|
bind:value={searchTerm}
|
|
1608
1638
|
onInput={(e: Event) => {
|
|
1609
1639
|
// Use debounced search for better performance
|
|
@@ -1617,7 +1647,7 @@
|
|
|
1617
1647
|
// Restore the previous filter when clearing search
|
|
1618
1648
|
currentFilter = lastActiveFilter;
|
|
1619
1649
|
if (browser) {
|
|
1620
|
-
|
|
1650
|
+
writeEditorStorageValue(VECTOR_FILTER_KEY(controllerId), lastActiveFilter);
|
|
1621
1651
|
}
|
|
1622
1652
|
|
|
1623
1653
|
// Reset batch index to start fresh
|
|
@@ -1682,6 +1712,7 @@
|
|
|
1682
1712
|
categories={filterOptions}
|
|
1683
1713
|
activeCategory={currentFilter}
|
|
1684
1714
|
onCategorySelect={handleFilterChange}
|
|
1715
|
+
getCategoryLabel={(category) => categoryLabel(category, $t)}
|
|
1685
1716
|
filterVisible={!searchTerm}
|
|
1686
1717
|
/>
|
|
1687
1718
|
</div>
|
|
@@ -1691,6 +1722,7 @@
|
|
|
1691
1722
|
<div
|
|
1692
1723
|
class="flex-1 {filteredVectors.length > 0 ? 'overflow-y-auto' : 'overflow-hidden'} min-h-0 vector-list-container"
|
|
1693
1724
|
bind:this={vectorListContainer}
|
|
1725
|
+
use:watchVectorImageErrors
|
|
1694
1726
|
onscroll={(e: Event) => {
|
|
1695
1727
|
e.stopPropagation();
|
|
1696
1728
|
handleVectorListScroll(e);
|
|
@@ -1698,14 +1730,13 @@
|
|
|
1698
1730
|
}}
|
|
1699
1731
|
id={`vector-list-${controllerId}`}
|
|
1700
1732
|
role="listbox"
|
|
1701
|
-
aria-label=
|
|
1733
|
+
aria-label={$t('editor.vector.list')}
|
|
1702
1734
|
>
|
|
1703
1735
|
<div class="space-y-0">
|
|
1704
1736
|
<!-- Recently used section - only show when not searching -->
|
|
1705
1737
|
{#if recentVectors.length > 0 && !searchTerm}
|
|
1706
1738
|
<RecentItemsSection
|
|
1707
1739
|
recentItems={recentVectors}
|
|
1708
|
-
title="Recently used"
|
|
1709
1740
|
onItemSelect={handleVectorSelect}
|
|
1710
1741
|
onItemDelete={handleVectorDelete}
|
|
1711
1742
|
isItemSelected={isVectorSelected}
|
|
@@ -1724,7 +1755,7 @@
|
|
|
1724
1755
|
{#if filteredVectors.length > 0}
|
|
1725
1756
|
<div id="vector-header-{controllerId}" class="sticky top-0 -mt-2 bg-white z-10 pt-2 {isScrolled ? 'border-b border-grey2' : 'border-b border-transparent'} transition-border">
|
|
1726
1757
|
<h3 class="subtext text-grey7 px-4 pb-2">
|
|
1727
|
-
{searchTerm ? '
|
|
1758
|
+
{searchTerm ? $t('editor.list.searchResults') : $t('editor.list.more')}
|
|
1728
1759
|
</h3>
|
|
1729
1760
|
</div>
|
|
1730
1761
|
{/if}
|
|
@@ -1732,7 +1763,7 @@
|
|
|
1732
1763
|
{#if filteredVectors.length === 0}
|
|
1733
1764
|
<div class="empty-state-wrapper">
|
|
1734
1765
|
<EmptyState
|
|
1735
|
-
message={searchTerm ? '
|
|
1766
|
+
message={searchTerm ? $t('editor.vector.noneFound') : $t('editor.vector.noneAvailable')}
|
|
1736
1767
|
height="auto"
|
|
1737
1768
|
/>
|
|
1738
1769
|
</div>
|
|
@@ -8,9 +8,17 @@
|
|
|
8
8
|
* - Handles item selection
|
|
9
9
|
* - Supports custom item rendering
|
|
10
10
|
* - Optimized with intersection observer
|
|
11
|
+
*
|
|
12
|
+
* `renderItem` may return a plain string (shown as text) or `{ html }`. The `html` is put on the page as it is, so the
|
|
13
|
+
* function that makes it must escape everything that comes from outside (names, addresses): see escapeHtml.
|
|
11
14
|
-->
|
|
12
15
|
<script lang="ts">
|
|
13
16
|
import { Check } from '@widgetic/design-system/icons';
|
|
17
|
+
import { escapeHtml } from '../../../utils/htmlSafety';
|
|
18
|
+
import { useEditorLanguage } from '../../../i18n/language';
|
|
19
|
+
|
|
20
|
+
// The words of the editor in the language of the page.
|
|
21
|
+
const { t } = useEditorLanguage();
|
|
14
22
|
|
|
15
23
|
export let items: any[] = [];
|
|
16
24
|
export let containerRef: (node: HTMLElement) => void;
|
|
@@ -41,7 +49,7 @@
|
|
|
41
49
|
function renderContent(item: any) {
|
|
42
50
|
const rendered = renderItem(item);
|
|
43
51
|
if (typeof rendered === 'string') {
|
|
44
|
-
return `<span>${rendered}</span>`;
|
|
52
|
+
return `<span>${escapeHtml(rendered)}</span>`;
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
if (rendered && typeof rendered === 'object' && 'html' in rendered) {
|
|
@@ -113,7 +121,7 @@
|
|
|
113
121
|
data-item-type="font"
|
|
114
122
|
>
|
|
115
123
|
<div class="flex items-center flex-1 min-w-0 group">
|
|
116
|
-
<Check class="
|
|
124
|
+
<Check class="me-2 h-6 w-6 flex-shrink-0 {isItemSelected(item) ? 'text-white' : 'text-sage-green opacity-0'}"/>
|
|
117
125
|
<div class="flex-1 overflow-hidden">
|
|
118
126
|
{@html renderContent(item)}
|
|
119
127
|
</div>
|
|
@@ -132,7 +140,7 @@
|
|
|
132
140
|
data-item-type="font"
|
|
133
141
|
>
|
|
134
142
|
<div class="flex items-center flex-1 min-w-0 group">
|
|
135
|
-
<Check class="
|
|
143
|
+
<Check class="me-2 h-6 w-6 flex-shrink-0 {isItemSelected(item) ? 'text-white' : 'text-sage-green opacity-0'}"/>
|
|
136
144
|
<div class="flex-1 overflow-hidden">
|
|
137
145
|
{@html renderContent(item)}
|
|
138
146
|
</div>
|
|
@@ -145,7 +153,7 @@
|
|
|
145
153
|
|
|
146
154
|
{#if loadingMore}
|
|
147
155
|
<div class="py-4 subtext text-center text-grey5">
|
|
148
|
-
|
|
156
|
+
{$t('editor.list.loadingMore')}
|
|
149
157
|
</div>
|
|
150
158
|
{/if}
|
|
151
159
|
</div>
|
|
@@ -13,9 +13,14 @@
|
|
|
13
13
|
// Import Check icon from design system
|
|
14
14
|
import { Check } from '@widgetic/design-system/icons';
|
|
15
15
|
import icTrash from '../../../assets/icons/ic-browser/trash.svg?url';
|
|
16
|
-
|
|
16
|
+
import { useEditorLanguage } from '../../../i18n/language';
|
|
17
|
+
|
|
18
|
+
// The words of the editor in the language of the page.
|
|
19
|
+
const { t } = useEditorLanguage();
|
|
20
|
+
|
|
17
21
|
export let recentItems: any[] = [];
|
|
18
|
-
|
|
22
|
+
/** The title of the section. Without one, the editor's own word, in the language of the page. */
|
|
23
|
+
export let title: string | undefined = undefined;
|
|
19
24
|
export let onItemSelect: (item: any) => void;
|
|
20
25
|
export let onItemDelete: (item: any, event: Event) => void;
|
|
21
26
|
export let isItemSelected: (item: any) => boolean;
|
|
@@ -36,7 +41,7 @@
|
|
|
36
41
|
|
|
37
42
|
{#if recentItems.length > 0}
|
|
38
43
|
<div class="px-4 pt-4 pb-4 widgetic-colors">
|
|
39
|
-
<h5 class="subtext text-grey7">{title}</h5>
|
|
44
|
+
<h5 class="subtext text-grey7">{title ?? $t('editor.list.recentlyUsed')}</h5>
|
|
40
45
|
|
|
41
46
|
{#if displayMode === 'list'}
|
|
42
47
|
<div class="grid grid-cols-1">
|
|
@@ -59,7 +64,7 @@
|
|
|
59
64
|
data-item={getItemKey(item)}
|
|
60
65
|
>
|
|
61
66
|
<div class="flex items-center flex-1 min-w-0 group">
|
|
62
|
-
<Check class="
|
|
67
|
+
<Check class="me-2 h-6 w-6 flex-shrink-0 {selected ? 'text-white' : 'text-sage-green opacity-0'}"/>
|
|
63
68
|
<div class="flex-1 overflow-hidden">
|
|
64
69
|
{#if typeof renderedItem === 'string'}
|
|
65
70
|
<span class="text-[24px]">{renderedItem}</span>
|
|
@@ -78,7 +83,7 @@
|
|
|
78
83
|
onItemDelete(item, e);
|
|
79
84
|
}}
|
|
80
85
|
>
|
|
81
|
-
<img src={icTrash} alt=
|
|
86
|
+
<img src={icTrash} alt={$t('editor.list.deleteItem')} class="w-4 h-4" />
|
|
82
87
|
</button>
|
|
83
88
|
{/if}
|
|
84
89
|
</div>
|
|
@@ -97,7 +102,7 @@
|
|
|
97
102
|
data-item={getItemKey(item)}
|
|
98
103
|
>
|
|
99
104
|
<div class="flex items-center flex-1 min-w-0 group">
|
|
100
|
-
<Check class="
|
|
105
|
+
<Check class="me-2 h-6 w-6 flex-shrink-0 {selected ? 'text-white' : 'text-sage-green opacity-0'}"/>
|
|
101
106
|
<div class="flex-1 overflow-hidden">
|
|
102
107
|
{#if typeof renderedItem === 'string'}
|
|
103
108
|
<span class="text-[24px]">{renderedItem}</span>
|
|
@@ -116,7 +121,7 @@
|
|
|
116
121
|
onItemDelete(item, e);
|
|
117
122
|
}}
|
|
118
123
|
>
|
|
119
|
-
<img src={icTrash} alt=
|
|
124
|
+
<img src={icTrash} alt={$t('editor.list.deleteItem')} class="w-4 h-4" />
|
|
120
125
|
</button>
|
|
121
126
|
{/if}
|
|
122
127
|
</div>
|
|
@@ -157,14 +162,14 @@
|
|
|
157
162
|
{#if isCustomItem(item) && !hideTrashIcon}
|
|
158
163
|
<button
|
|
159
164
|
type="button"
|
|
160
|
-
class="absolute top-1
|
|
165
|
+
class="absolute top-1 end-1 p-1 rounded-full hover:bg-red-100 hover:text-red-500 transition-colors bg-white"
|
|
161
166
|
tabindex="-1"
|
|
162
167
|
onclick={(e) => {
|
|
163
168
|
e.stopPropagation();
|
|
164
169
|
onItemDelete(item, e);
|
|
165
170
|
}}
|
|
166
171
|
>
|
|
167
|
-
<img src={icTrash} alt=
|
|
172
|
+
<img src={icTrash} alt={$t('editor.list.deleteItem')} class="w-3 h-3" />
|
|
168
173
|
</button>
|
|
169
174
|
{/if}
|
|
170
175
|
</div>
|
|
@@ -195,14 +200,14 @@
|
|
|
195
200
|
{#if isCustomItem(item) && !hideTrashIcon}
|
|
196
201
|
<button
|
|
197
202
|
type="button"
|
|
198
|
-
class="absolute top-1
|
|
203
|
+
class="absolute top-1 end-1 p-1 rounded-full hover:bg-red-100 hover:text-red-500 transition-colors bg-white"
|
|
199
204
|
tabindex="-1"
|
|
200
205
|
onclick={(e) => {
|
|
201
206
|
e.stopPropagation();
|
|
202
207
|
onItemDelete(item, e);
|
|
203
208
|
}}
|
|
204
209
|
>
|
|
205
|
-
<img src={icTrash} alt=
|
|
210
|
+
<img src={icTrash} alt={$t('editor.list.deleteItem')} class="w-3 h-3" />
|
|
206
211
|
</button>
|
|
207
212
|
{/if}
|
|
208
213
|
</div>
|
|
@@ -13,7 +13,7 @@ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> =
|
|
|
13
13
|
}
|
|
14
14
|
declare const RecentItemsSection: $$__sveltets_2_IsomorphicComponent<{
|
|
15
15
|
recentItems?: any[];
|
|
16
|
-
title?: string;
|
|
16
|
+
/** The title of the section. Without one, the editor's own word, in the language of the page. */ title?: string | undefined;
|
|
17
17
|
onItemSelect: (item: any) => void;
|
|
18
18
|
onItemDelete: (item: any, event: Event) => void;
|
|
19
19
|
isItemSelected: (item: any) => boolean;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The calls an embedded composition editor makes to the Widgetic API, over `fetch`.
|
|
3
|
+
*
|
|
4
|
+
* Why not the generated SDK: the editor runs in a customer's browser and may only call a short list of routes
|
|
5
|
+
* (those an editor session allows), the list is fixed, and it must work with whatever token the host page gives it.
|
|
6
|
+
* A client of its own keeps the editor small and independent of the SDK's release cycle, and it keeps the few
|
|
7
|
+
* rules that matter in one place:
|
|
8
|
+
*
|
|
9
|
+
* - the token is asked from the host page before each request (`getAccessToken('initial')`); when the API
|
|
10
|
+
* answers 401 it is asked again (`'expired'`) and the request is sent once more with a token that is new;
|
|
11
|
+
* - a save carries an `Idempotency-Key` (the API requires one on every PUT from a session), kept the same when
|
|
12
|
+
* the request is sent again after a 401, so one save is never applied twice;
|
|
13
|
+
* - only the fields editing changes can be sent (`SaveCompositionBody`); a session is refused any other;
|
|
14
|
+
* - every failure is a `CompositionApiError` with the API's own code, so the host can tell an ended session
|
|
15
|
+
* (`EDITOR_SESSION_EXPIRED`...) from a missing composition (`NOT_FOUND`) from a widget that was never
|
|
16
|
+
* published (`WIDGET_NOT_PUBLISHED`);
|
|
17
|
+
* - a request that gets no answer does not hang the editor: it fails with `TIMEOUT` after a set time.
|
|
18
|
+
*/
|
|
19
|
+
import { type JsonRecord } from './compositionHelpers.js';
|
|
20
|
+
/** Why the token is being asked for. `expired` means the API just refused the previous one. */
|
|
21
|
+
export type TokenReason = 'initial' | 'expired';
|
|
22
|
+
/** The fields of a composition an editor may change. Anything else is refused by the API for an editor session. */
|
|
23
|
+
export interface SaveCompositionBody {
|
|
24
|
+
name?: string;
|
|
25
|
+
content?: JsonRecord;
|
|
26
|
+
design?: JsonRecord;
|
|
27
|
+
embedSettings?: JsonRecord;
|
|
28
|
+
}
|
|
29
|
+
/** Where to show a composition: the widget build the preview frame loads (`GET /compositions/{id}/preview`). */
|
|
30
|
+
export interface CompositionPreviewInfo {
|
|
31
|
+
compositionId: string;
|
|
32
|
+
widgetId: string;
|
|
33
|
+
version: number;
|
|
34
|
+
previewUrl: string;
|
|
35
|
+
}
|
|
36
|
+
export interface CompositionApiOptions {
|
|
37
|
+
/** The API's address including the version, like `https://api.widgetic.com/v1`. */
|
|
38
|
+
apiBasePath: string;
|
|
39
|
+
/**
|
|
40
|
+
* Gives the token to call the API with. Called before every request, so a host that keeps its token fresh is
|
|
41
|
+
* always used up to date; called again with `'expired'` when the API answers 401, after which the request is
|
|
42
|
+
* sent once more if (and only if) the token that comes back is a different one. That second call also gets the
|
|
43
|
+
* token that was refused, so a host can tell the refusal of its current token (get a new one) from the late
|
|
44
|
+
* refusal of one it has already replaced (hand out the new one) when several requests are refused together.
|
|
45
|
+
*/
|
|
46
|
+
getAccessToken: (reason: TokenReason, refusedToken?: string) => Promise<string> | string;
|
|
47
|
+
/** The `fetch` to use. Default: the page's own. */
|
|
48
|
+
fetch?: typeof fetch;
|
|
49
|
+
/** Makes the key a save is identified by (8 to 50 letters, digits, dashes and underscores). Default: a random UUID. */
|
|
50
|
+
createIdempotencyKey?: () => string;
|
|
51
|
+
/** How long to wait for an answer before giving up with `TIMEOUT`, in milliseconds. Default: 30 seconds. */
|
|
52
|
+
requestTimeoutMs?: number;
|
|
53
|
+
}
|
|
54
|
+
/** Options of one call. */
|
|
55
|
+
export interface CompositionRequestOptions {
|
|
56
|
+
/** Cancels the request: the call then fails with `ABORTED`. */
|
|
57
|
+
signal?: AbortSignal;
|
|
58
|
+
}
|
|
59
|
+
export interface SaveCompositionOptions extends CompositionRequestOptions {
|
|
60
|
+
/** Identifies this save. Give the same key to send the same save again; default: a new one. */
|
|
61
|
+
idempotencyKey?: string;
|
|
62
|
+
}
|
|
63
|
+
export declare const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
64
|
+
/** Codes the client itself reports (the API's own codes, such as `NOT_FOUND`, are passed through as they are). */
|
|
65
|
+
export type ClientErrorCode = 'NETWORK' | 'TIMEOUT' | 'ABORTED' | 'TOKEN_UNAVAILABLE' | 'INVALID_RESPONSE';
|
|
66
|
+
/** A request to the API that did not give the answer wanted. */
|
|
67
|
+
export declare class CompositionApiError extends Error {
|
|
68
|
+
/** The HTTP status; `0` when no answer arrived (network, timeout, abort, or no token to send). */
|
|
69
|
+
readonly status: number;
|
|
70
|
+
/** The API's error code (`NOT_FOUND`, `EDITOR_SESSION_EXPIRED`...), or one of {@link ClientErrorCode}. */
|
|
71
|
+
readonly code: string;
|
|
72
|
+
/** The API's correlation id for the failed request, to quote in a support request. */
|
|
73
|
+
readonly correlationId?: string;
|
|
74
|
+
constructor(message: string, details: {
|
|
75
|
+
status: number;
|
|
76
|
+
code: string;
|
|
77
|
+
correlationId?: string;
|
|
78
|
+
});
|
|
79
|
+
/** True when the editor session cannot be used any more: the host page has to give a new one. */
|
|
80
|
+
get sessionEnded(): boolean;
|
|
81
|
+
/** True when sending the same request again later may work: nothing was refused, the network or the server failed. */
|
|
82
|
+
get retryable(): boolean;
|
|
83
|
+
}
|
|
84
|
+
/** What a client of the composition routes can do. */
|
|
85
|
+
export interface CompositionApi {
|
|
86
|
+
getComposition(compositionId: string, options?: CompositionRequestOptions): Promise<JsonRecord>;
|
|
87
|
+
getPreview(compositionId: string, options?: CompositionRequestOptions): Promise<CompositionPreviewInfo>;
|
|
88
|
+
getContentSchema(compositionId: string, options?: CompositionRequestOptions): Promise<unknown>;
|
|
89
|
+
getDesignSchema(compositionId: string, options?: CompositionRequestOptions): Promise<unknown>;
|
|
90
|
+
saveComposition(compositionId: string, body: SaveCompositionBody, options?: SaveCompositionOptions): Promise<JsonRecord>;
|
|
91
|
+
}
|
|
92
|
+
/** A key that is unique, not secret: the page's `crypto.randomUUID`, else random bytes, else the clock and `Math.random`. */
|
|
93
|
+
export declare function createRandomIdempotencyKey(): string;
|
|
94
|
+
/** Builds the client. `apiBasePath` and `getAccessToken` are required. */
|
|
95
|
+
export declare function createCompositionApi(options: CompositionApiOptions): CompositionApi;
|