@websline/cms-view-utils 1.6.0 → 1.7.1

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.
@@ -1 +1 @@
1
- (()=>{var r={BLOCK_ADD:"BLOCK_ADD",BLOCK_DELETE:"BLOCK_DELETE",BLOCK_EDIT:"BLOCK_EDIT",BLOCK_POSITION:"BLOCK_POSITION",BLOCK_SHOW:"BLOCK_SHOW",BLOCK_HIDE:"BLOCK_HIDE",PATH_CHANGED:"PATH_CHANGED",PATH_ON_LOAD:"PATH_ON_LOAD",IFRAME_DOM_LOADED:"IFRAME_DOM_LOADED",CURRENT_BLOCK_EDIT:"CURRENT_BLOCK_EDIT",SCROLL_TO_BLOCK:"SCROLL_TO_BLOCK"},_=(i,a={})=>{i&&window.parent!==window&&window.parent.postMessage({type:i,payload:a},"*")};(function(){if(window.parent===window||window.__PREVIEW_BRIDGE_INITIALIZED__)return;window.__PREVIEW_BRIDGE_INITIALIZED__=!0;let i=t=>t.metaKey||t.ctrlKey||t.shiftKey,a=t=>{let n=t.target.closest("a[href]");if(!n||n.target==="_blank"||i(t))return;let o=new URL(n.href,window.location.origin);o.origin===window.location.origin&&(t.preventDefault(),t.stopPropagation(),_(r.PATH_CHANGED,{path:o.pathname}))};document.addEventListener("click",a,!0),_(r.PATH_ON_LOAD,{path:window.location.pathname}),window.addEventListener("load",()=>{_(r.IFRAME_DOM_LOADED)});let L=t=>{let n=`[data-cms-block-id="${t}"]`,o=0,e=10,O=setInterval(()=>{let E=document.querySelector(n);E&&(E.scrollIntoView({behavior:"smooth",block:"start"}),clearInterval(O)),++o>=e&&clearInterval(O)},100)};window.addEventListener("message",t=>{let{type:n,payload:o}=t.data||{};if(n&&n===r.SCROLL_TO_BLOCK){let{draftBlockUuid:e}=o||{};if(!e)return;L(e)}})})();})();
1
+ (()=>{var r={BLOCK_ADD:"BLOCK_ADD",BLOCK_COMMENT:"BLOCK_COMMENT",BLOCK_DELETE:"BLOCK_DELETE",BLOCK_EDIT:"BLOCK_EDIT",BLOCK_POSITION:"BLOCK_POSITION",BLOCK_SHOW:"BLOCK_SHOW",BLOCK_HIDE:"BLOCK_HIDE",PATH_CHANGED:"PATH_CHANGED",PATH_ON_LOAD:"PATH_ON_LOAD",IFRAME_DOM_LOADED:"IFRAME_DOM_LOADED",COMMENT_COUNTS:"COMMENT_COUNTS",CURRENT_BLOCK_EDIT:"CURRENT_BLOCK_EDIT",SCROLL_TO_BLOCK:"SCROLL_TO_BLOCK"},_=(i,O={})=>{i&&window.parent!==window&&window.parent.postMessage({type:i,payload:O},"*")};(function(){if(window.parent===window||window.__PREVIEW_BRIDGE_INITIALIZED__)return;window.__PREVIEW_BRIDGE_INITIALIZED__=!0;let i=t=>t.metaKey||t.ctrlKey||t.shiftKey,O=t=>{let n=t.target.closest("a[href]");if(!n||n.target==="_blank"||i(t))return;let o=new URL(n.href,window.location.origin);o.origin===window.location.origin&&(t.preventDefault(),t.stopPropagation(),_(r.PATH_CHANGED,{path:o.pathname}))};document.addEventListener("click",O,!0),_(r.PATH_ON_LOAD,{path:window.location.pathname}),window.addEventListener("load",()=>{_(r.IFRAME_DOM_LOADED)});let L=t=>{let n=`[data-cms-block-id="${t}"]`,o=0,e=10,a=setInterval(()=>{let E=document.querySelector(n);E&&(E.scrollIntoView({behavior:"smooth",block:"start"}),clearInterval(a)),++o>=e&&clearInterval(a)},100)};window.addEventListener("message",t=>{let{type:n,payload:o}=t.data||{};if(n&&n===r.SCROLL_TO_BLOCK){let{draftBlockUuid:e}=o||{};if(!e)return;L(e)}})})();})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@websline/cms-view-utils",
3
- "version": "1.6.0",
3
+ "version": "1.7.1",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -21,10 +21,10 @@
21
21
  "devDependencies": {
22
22
  "@eslint/compat": "^2.1.0",
23
23
  "@eslint/js": "^10.0.1",
24
- "eslint": "^10.8.0",
24
+ "eslint": "^10.8.1",
25
25
  "eslint-config-prettier": "^10.1.8",
26
- "eslint-plugin-svelte": "^3.22.0",
27
- "globals": "^17.9.0",
26
+ "eslint-plugin-svelte": "^3.23.0",
27
+ "globals": "^17.11.0",
28
28
  "prettier": "^3.9.6",
29
29
  "prettier-plugin-svelte": "^4.1.1",
30
30
  "prettier-plugin-tailwindcss": "^0.8.1",
@@ -1,3 +1,11 @@
1
+ /**
2
+ * Fetch a single ABM board.
3
+ *
4
+ * @param {object} options
5
+ * @param {string} options.locale - Locale code, e.g. `"de"`.
6
+ * @param {string|number} options.boardId - ABM id of the board.
7
+ * @returns {Promise<object>} The board object.
8
+ */
1
9
  const fetchBoard = async ({ locale, boardId }) => {
2
10
  const url = `/api/cms/api/public/abm-boards/${locale}/${boardId}`;
3
11
 
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Fetch all ABM boards.
3
+ *
4
+ * @param {object} options
5
+ * @param {string} options.locale - Locale code, e.g. `"de"`.
6
+ * @returns {Promise<object[]>} The boards.
7
+ */
1
8
  const fetchBoards = async ({ locale }) => {
2
9
  const url = `/api/cms/api/public/abm-boards/${locale}`;
3
10
 
@@ -1,3 +1,13 @@
1
+ /**
2
+ * Fetch the items of a collection, or a single item of it.
3
+ *
4
+ * @param {object} options
5
+ * @param {string} options.locale - Locale code, e.g. `"de"`.
6
+ * @param {string} options.identifier - Collection identifier as configured in the CMS, e.g. `"blog"`.
7
+ * @param {string} [options.itemUuid] - Return only this item, as a plain object instead of an array.
8
+ * @param {number} [options.limit] - Maximum number of items, counted from the start of the CMS order. Ignored together with `itemUuid`.
9
+ * @returns {Promise<object[]|object>} An array of items, or a single item when `itemUuid` is set.
10
+ */
1
11
  const fetchCollections = async ({ identifier, itemUuid, limit, locale }) => {
2
12
  const params = new URLSearchParams();
3
13
 
@@ -1,3 +1,14 @@
1
+ /**
2
+ * Fetch ABM FAQs.
3
+ *
4
+ * @param {object} options
5
+ * @param {string} options.locale - Locale code, e.g. `"de"`.
6
+ * @param {string|number} [options.excludeId] - ABM FAQ id to leave out, e.g. the FAQ already shown on the page.
7
+ * @param {string[]} [options.tags] - Keep only FAQs carrying at least one of these tags (OR, not AND).
8
+ * @param {string[]} [options.groups] - Keep only FAQs belonging to one of these group ids.
9
+ * @param {number} [options.limit] - Maximum number of FAQs, applied after the filters.
10
+ * @returns {Promise<object[]>} The matching FAQs.
11
+ */
1
12
  const fetchFaqs = async ({ excludeId, limit, locale, tags, groups }) => {
2
13
  const params = new URLSearchParams();
3
14
 
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Fetch the teasers of the tenant's published pages, e.g. for a blog index or related-posts list.
3
+ *
4
+ * @param {object} options
5
+ * @param {string} options.locale - Locale code, e.g. `"de"`.
6
+ * @param {string} [options.excludePageUuid] - Page UUID to leave out, e.g. the page currently shown.
7
+ * @param {boolean} [options.isBlog] - `true` for blog pages only, `false` for non-blog pages only, omit for all.
8
+ * @param {string[]} [options.tags] - Keep only pages carrying at least one of these tag UUIDs (OR, not AND).
9
+ * @param {"newest"|"oldest"} [options.order] - Sort by publication date; defaults to `"newest"`.
10
+ * @param {number} [options.offset] - Number of teasers to skip, applied after all filters.
11
+ * @param {number} [options.limit] - Maximum number of teasers; omit for all — there is no default.
12
+ * @returns {Promise<object[]>} The teasers.
13
+ */
14
+ const fetchPageTeasers = async ({
15
+ locale,
16
+ excludePageUuid,
17
+ isBlog,
18
+ tags,
19
+ order,
20
+ offset,
21
+ limit,
22
+ }) => {
23
+ const params = new URLSearchParams();
24
+
25
+ if (excludePageUuid != null && excludePageUuid !== "") {
26
+ params.set("excludePageUuid", excludePageUuid);
27
+ }
28
+
29
+ // The API accepts exactly `true` or `false` — anything else answers 422,
30
+ // so only real booleans are forwarded.
31
+ if (isBlog === true || isBlog === false) {
32
+ params.set("isBlog", isBlog);
33
+ }
34
+
35
+ if (Array.isArray(tags)) {
36
+ for (const tag of tags) {
37
+ if (tag != null && tag !== "") {
38
+ params.append("tags", tag);
39
+ }
40
+ }
41
+ }
42
+
43
+ if (order != null && order !== "") {
44
+ params.set("order", order);
45
+ }
46
+
47
+ if (offset != null) {
48
+ params.set("offset", offset);
49
+ }
50
+
51
+ if (limit != null) {
52
+ params.set("limit", limit);
53
+ }
54
+
55
+ const query = params.toString();
56
+ const url = `/api/cms/api/public/page-teasers/${locale}${query ? `?${query}` : ""}`;
57
+
58
+ const response = await fetch(url, { method: "GET" });
59
+
60
+ return response.json();
61
+ };
62
+
63
+ export { fetchPageTeasers };
@@ -1,3 +1,12 @@
1
+ /**
2
+ * Fetch ABM packages, typically for a "similar packages" section.
3
+ *
4
+ * @param {object} options
5
+ * @param {string} options.locale - Locale code, e.g. `"de"`.
6
+ * @param {string|number} [options.excludeId] - ABM package id to leave out, e.g. the package currently shown.
7
+ * @param {number} [options.limit] - Maximum number of packages to return.
8
+ * @returns {Promise<object[]>} The packages.
9
+ */
1
10
  const fetchSimilarPackages = async ({ excludeId, limit, locale }) => {
2
11
  const params = new URLSearchParams();
3
12
 
@@ -1,3 +1,12 @@
1
+ /**
2
+ * Fetch ABM rooms, typically for a "similar rooms" section.
3
+ *
4
+ * @param {object} options
5
+ * @param {string} options.locale - Locale code, e.g. `"de"`.
6
+ * @param {string|number} [options.excludeId] - ABM room id to leave out, e.g. the room currently shown.
7
+ * @param {number} [options.limit] - Maximum number of rooms to return.
8
+ * @returns {Promise<object[]>} The rooms.
9
+ */
1
10
  const fetchSimilarRooms = async ({ excludeId, limit, locale }) => {
2
11
  const params = new URLSearchParams();
3
12
 
@@ -49,6 +49,12 @@
49
49
  if (autoTranslate) return;
50
50
  notifyCMS(EVENTS.BLOCK_ADD, { position });
51
51
  break;
52
+ case CMS_ACTIONS.comment:
53
+ notifyCMS(EVENTS.BLOCK_COMMENT, {
54
+ blockIdentifier: block.identifier,
55
+ draftBlockUuid: block.uuid,
56
+ });
57
+ break;
52
58
  case CMS_ACTIONS.delete:
53
59
  if (isSynced) return;
54
60
  notifyCMS(EVENTS.BLOCK_DELETE, {
@@ -126,20 +132,36 @@
126
132
  isHovered = true;
127
133
  }, 50);
128
134
 
135
+ let commentCount = $state(0);
136
+ // Off for users without the comment read permission; the CMS says so in the same
137
+ // message as the counts.
138
+ let commentsEnabled = $state(true);
139
+
129
140
  const handler = (event) => {
130
141
  const { type, payload } = event.data || {};
131
- if (type !== EVENTS.CURRENT_BLOCK_EDIT) return;
132
142
 
133
- const { draftBlockUuid } = payload || {};
143
+ if (type === EVENTS.CURRENT_BLOCK_EDIT) {
144
+ isActive = payload?.draftBlockUuid === block.uuid;
145
+ return;
146
+ }
134
147
 
135
- isActive = draftBlockUuid === block.uuid;
148
+ // The counts arrive from the CMS: this iframe has no session for its API.
149
+ if (type === EVENTS.COMMENT_COUNTS) {
150
+ commentCount = payload?.counts?.[block.uuid] ?? 0;
151
+ commentsEnabled = payload?.enabled !== false;
152
+ }
136
153
  };
137
154
 
138
155
  let isOutside = $derived(calcBarPosition === "outside");
139
156
 
140
157
  let containerShape = $derived.by(() => {
141
- if (isOutside) return "absolute rounded-t-lg -top-8";
142
- return `rounded-b-lg top-1.5 ${isSticky === "full" ? "fixed" : "absolute"}`;
158
+ // Both orientations have to read as the same bar, and the block's 6px frame
159
+ // counts: pointing up the bar overlaps it by 4px, pointing down it starts
160
+ // below it. 40px up (offset by the same 4px) and 36px down therefore end up
161
+ // equally tall — 42px including the line — and the 4px bottom padding mirrors
162
+ // the icon inset: 4px towards the line, 8px towards the rounded edge.
163
+ if (isOutside) return "absolute h-10 rounded-t-lg -top-9";
164
+ return `h-9 rounded-b-lg pb-1 top-1.5 ${isSticky === "full" ? "fixed" : "absolute"}`;
143
165
  });
144
166
 
145
167
  let cornerEdges = $derived(
@@ -150,13 +172,16 @@
150
172
  "after:top-0 after:-right-4 after:shadow-[-6px_-6px_0_0_var(--block-color)]",
151
173
  );
152
174
 
175
+ // 34px from the icon in both directions, which lands 2px off the bar either way.
153
176
  let tooltipClasses = $derived(
154
- `tooltip absolute ${isOutside ? "-top-8" : "top-8"} bg-[var(--block-color)] leading-none text-xs p-1.5 rounded left-1/2 -translate-x-1/2 whitespace-nowrap transition-opacity duration-150`,
177
+ `tooltip absolute ${isOutside ? "-top-8.5" : "top-8.5"} bg-[var(--block-color)] leading-none text-xs p-1.5 rounded left-1/2 -translate-x-1/2 whitespace-nowrap transition-opacity duration-150`,
155
178
  );
156
179
 
157
180
  const labels = {
158
181
  de: {
159
182
  add: "Hinzufügen",
183
+ comment: "Kommentare",
184
+ commentAria: "Kommentare des Blocks",
160
185
  position: "Anordnen",
161
186
  delete: "Löschen",
162
187
  edit: "Bearbeiten",
@@ -167,6 +192,8 @@
167
192
  },
168
193
  en: {
169
194
  add: "Add",
195
+ comment: "Comments",
196
+ commentAria: "Comments of the block",
170
197
  position: "Arrange",
171
198
  delete: "Delete",
172
199
  edit: "Edit",
@@ -210,14 +237,14 @@
210
237
  {/if}
211
238
 
212
239
  <div
213
- class="left-1/2 z-9999 flex h-9 -translate-x-1/2 items-center justify-center gap-1.5 px-3 transition-opacity duration-100 before:absolute before:h-4 before:w-4 before:rounded-full before:bg-transparent before:content-[''] after:absolute after:h-4 after:w-4 after:rounded-full after:bg-transparent after:content-[''] {containerShape} {cornerEdges}"
240
+ class="left-1/2 z-9999 flex -translate-x-1/2 items-center justify-center gap-1.5 px-3 transition-opacity duration-100 before:absolute before:h-4 before:w-4 before:rounded-full before:bg-transparent before:content-[''] after:absolute after:h-4 after:w-4 after:rounded-full after:bg-transparent after:content-[''] {containerShape} {cornerEdges}"
214
241
  style="background: var(--block-color); color: var(--block-text);"
215
242
  class:opacity-100={toolbarVisible}
216
243
  class:opacity-0={!toolbarVisible}>
217
244
  <button
218
245
  aria-label={t.addAria}
219
246
  class={[
220
- "cursor-pointer",
247
+ "relative cursor-pointer",
221
248
  (autoTranslate || isSynced) && "pointer-events-none opacity-40",
222
249
  ]}
223
250
  onmouseenter={() => showTooltip("add")}
@@ -322,7 +349,7 @@
322
349
  <button
323
350
  aria-label={t.positionAria}
324
351
  class={[
325
- "cursor-pointer",
352
+ "relative cursor-pointer",
326
353
  (autoTranslate || isSynced) && "pointer-events-none opacity-40",
327
354
  ]}
328
355
  onmouseenter={() => showTooltip("position")}
@@ -352,7 +379,10 @@
352
379
 
353
380
  <button
354
381
  aria-label={t.deleteAria}
355
- class={["cursor-pointer", isSynced && "pointer-events-none opacity-40"]}
382
+ class={[
383
+ "relative cursor-pointer",
384
+ isSynced && "pointer-events-none opacity-40",
385
+ ]}
356
386
  onmouseenter={() => showTooltip("delete")}
357
387
  onmouseleave={hideTooltip}
358
388
  onclick={(event) => handleClick(event, CMS_ACTIONS.delete)}>
@@ -378,6 +408,39 @@
378
408
  </div>
379
409
  </button>
380
410
 
411
+ {#if commentsEnabled}
412
+ <button
413
+ aria-label={t.commentAria}
414
+ class="relative flex cursor-pointer items-center gap-1"
415
+ onmouseenter={() => showTooltip("comment")}
416
+ onmouseleave={hideTooltip}
417
+ onclick={(event) => handleClick(event, CMS_ACTIONS.comment)}>
418
+ <svg
419
+ width="24"
420
+ height="24"
421
+ viewBox="0 0 24 24"
422
+ fill="none"
423
+ xmlns="http://www.w3.org/2000/svg">
424
+ <path
425
+ d="M6.99997 9H16M6.99997 13H16M6.99997 17H12M22 12C22 17.5229 17.5229 22 12 22C9.01327 22 2 22 2 22C2 22 2 14.5361 2 12C2 6.47715 6.47717 2 12 2C17.5229 2 22 6.47715 22 12Z"
426
+ stroke="var(--block-text)"
427
+ stroke-width="1.5"
428
+ stroke-linecap="round"
429
+ stroke-linejoin="round" />
430
+ </svg>
431
+ {#if commentCount > 0}
432
+ <span class="text-xs leading-none tabular-nums">{commentCount}</span>
433
+ {/if}
434
+ <div
435
+ class={tooltipClasses}
436
+ class:opacity-0={activeTooltip !== "comment"}
437
+ class:opacity-100={activeTooltip === "comment"}
438
+ class:pointer-events-none={activeTooltip !== "comment"}>
439
+ {t.comment}
440
+ </div>
441
+ </button>
442
+ {/if}
443
+
381
444
  <button
382
445
  aria-label={t.editAria}
383
446
  class="relative cursor-pointer"
@@ -1,5 +1,6 @@
1
1
  const CMS_ACTIONS = {
2
2
  add: "add-block",
3
+ comment: "comment-block",
3
4
  copy: "copy-block",
4
5
  delete: "delete-block",
5
6
  edit: "edit-block",
@@ -1,5 +1,6 @@
1
1
  const EVENTS = {
2
2
  BLOCK_ADD: "BLOCK_ADD",
3
+ BLOCK_COMMENT: "BLOCK_COMMENT",
3
4
  BLOCK_DELETE: "BLOCK_DELETE",
4
5
  BLOCK_EDIT: "BLOCK_EDIT",
5
6
  BLOCK_POSITION: "BLOCK_POSITION",
@@ -8,6 +9,7 @@ const EVENTS = {
8
9
  PATH_CHANGED: "PATH_CHANGED",
9
10
  PATH_ON_LOAD: "PATH_ON_LOAD",
10
11
  IFRAME_DOM_LOADED: "IFRAME_DOM_LOADED",
12
+ COMMENT_COUNTS: "COMMENT_COUNTS",
11
13
  CURRENT_BLOCK_EDIT: "CURRENT_BLOCK_EDIT",
12
14
  SCROLL_TO_BLOCK: "SCROLL_TO_BLOCK",
13
15
  };
package/src/index.js CHANGED
@@ -5,6 +5,7 @@ export { fetchFaqs } from "./client/fetchFaqs.js";
5
5
  export { fetchCollections } from "./client/fetchCollections.js";
6
6
  export { fetchBoards } from "./client/fetchBoards.js";
7
7
  export { fetchBoard } from "./client/fetchBoard.js";
8
+ export { fetchPageTeasers } from "./client/fetchPageTeasers.js";
8
9
 
9
10
  export { default as AddBlockPlaceholder } from "./components/AddBlockPlaceholder.svelte";
10
11
  export { default as BlockWrapper } from "./components/BlockWrapper.svelte";