@broberg/cms-inline-edit 0.7.1 → 0.8.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/dist/index.d.cts CHANGED
@@ -183,6 +183,19 @@ declare function disconnect(options: InlineEditOptions): void;
183
183
  * MPA consumers (broberg: full reload per navigation re-runs init) never need it.
184
184
  */
185
185
  declare function rescanFields(): void;
186
+ /**
187
+ * A plain field's value, with its line breaks intact.
188
+ *
189
+ * `textContent` drops <br> entirely — a two-paragraph value read through it
190
+ * comes back as one run of text, and saving then DESTROYS the break that was
191
+ * already stored. So this is not only how a new break gets in; it is what stops
192
+ * an existing one being silently flattened the next time anyone edits the field.
193
+ *
194
+ * Deliberately narrow: <br> becomes "\n" and text nodes come through verbatim.
195
+ * A plain field is a string, not a document — anything richer belongs in a
196
+ * richtext field, which has its own serializer.
197
+ */
198
+ declare function plainTextWithBreaks(el: HTMLElement): string;
186
199
  interface LinkablePage {
187
200
  collection: string;
188
201
  slug: string;
@@ -253,4 +266,4 @@ declare function setLinkTarget(el: HTMLElement, newTab: boolean): void;
253
266
  */
254
267
  declare function htmlToMarkdown(html: string): string;
255
268
 
256
- export { type InlineEditLabels, type InlineEditOptions, applyFieldSlice, buildConnectUrl, disconnect, extractLinkTargets, getConnectedToken, htmlToMarkdown, initInlineEdit, isBareEmail, isDangerousUrl, isExternalHost, isSchemeless, positionPopover, renderCurrentRefLine, rescanFields, resolveExistingRef, setLinkTarget, withHttps };
269
+ export { type InlineEditLabels, type InlineEditOptions, applyFieldSlice, buildConnectUrl, disconnect, extractLinkTargets, getConnectedToken, htmlToMarkdown, initInlineEdit, isBareEmail, isDangerousUrl, isExternalHost, isSchemeless, plainTextWithBreaks, positionPopover, renderCurrentRefLine, rescanFields, resolveExistingRef, setLinkTarget, withHttps };
package/dist/index.d.ts CHANGED
@@ -183,6 +183,19 @@ declare function disconnect(options: InlineEditOptions): void;
183
183
  * MPA consumers (broberg: full reload per navigation re-runs init) never need it.
184
184
  */
185
185
  declare function rescanFields(): void;
186
+ /**
187
+ * A plain field's value, with its line breaks intact.
188
+ *
189
+ * `textContent` drops <br> entirely — a two-paragraph value read through it
190
+ * comes back as one run of text, and saving then DESTROYS the break that was
191
+ * already stored. So this is not only how a new break gets in; it is what stops
192
+ * an existing one being silently flattened the next time anyone edits the field.
193
+ *
194
+ * Deliberately narrow: <br> becomes "\n" and text nodes come through verbatim.
195
+ * A plain field is a string, not a document — anything richer belongs in a
196
+ * richtext field, which has its own serializer.
197
+ */
198
+ declare function plainTextWithBreaks(el: HTMLElement): string;
186
199
  interface LinkablePage {
187
200
  collection: string;
188
201
  slug: string;
@@ -253,4 +266,4 @@ declare function setLinkTarget(el: HTMLElement, newTab: boolean): void;
253
266
  */
254
267
  declare function htmlToMarkdown(html: string): string;
255
268
 
256
- export { type InlineEditLabels, type InlineEditOptions, applyFieldSlice, buildConnectUrl, disconnect, extractLinkTargets, getConnectedToken, htmlToMarkdown, initInlineEdit, isBareEmail, isDangerousUrl, isExternalHost, isSchemeless, positionPopover, renderCurrentRefLine, rescanFields, resolveExistingRef, setLinkTarget, withHttps };
269
+ export { type InlineEditLabels, type InlineEditOptions, applyFieldSlice, buildConnectUrl, disconnect, extractLinkTargets, getConnectedToken, htmlToMarkdown, initInlineEdit, isBareEmail, isDangerousUrl, isExternalHost, isSchemeless, plainTextWithBreaks, positionPopover, renderCurrentRefLine, rescanFields, resolveExistingRef, setLinkTarget, withHttps };
package/dist/index.js CHANGED
@@ -397,7 +397,7 @@ function wireField(el, token, options) {
397
397
  e.preventDefault();
398
398
  e.stopPropagation();
399
399
  const tokenSafe = hasTokenChips(el);
400
- el.dataset.cmsOriginalValue = tokenSafe ? serializeTokenSafe(el) : el.textContent ?? "";
400
+ el.dataset.cmsOriginalValue = tokenSafe ? serializeTokenSafe(el) : plainTextWithBreaks(el);
401
401
  el.setAttribute("contenteditable", "true");
402
402
  if (tokenSafe) lockTokenChips(el);
403
403
  el.focus();
@@ -405,7 +405,7 @@ function wireField(el, token, options) {
405
405
  el.addEventListener("blur", () => {
406
406
  el.removeAttribute("contenteditable");
407
407
  const original = el.dataset.cmsOriginalValue ?? "";
408
- const current = hasTokenChips(el) ? serializeTokenSafe(el) : el.textContent ?? "";
408
+ const current = hasTokenChips(el) ? serializeTokenSafe(el) : plainTextWithBreaks(el);
409
409
  if (current.trim() === original.trim()) return;
410
410
  void saveField(el, current.trim(), token, options);
411
411
  });
@@ -415,12 +415,35 @@ function wireField(el, token, options) {
415
415
  document.execCommand("insertText", false, text);
416
416
  });
417
417
  el.addEventListener("keydown", (e) => {
418
- if (e.key === "Enter") {
419
- e.preventDefault();
420
- el.blur();
418
+ if (e.key !== "Enter") return;
419
+ e.preventDefault();
420
+ if (e.shiftKey) {
421
+ document.execCommand("insertLineBreak");
422
+ return;
421
423
  }
424
+ el.blur();
422
425
  });
423
426
  }
427
+ function plainTextWithBreaks(el) {
428
+ let out = "";
429
+ const walk = (node) => {
430
+ if (node.nodeType === Node.TEXT_NODE) {
431
+ out += node.textContent ?? "";
432
+ return;
433
+ }
434
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
435
+ const tag = node.tagName.toLowerCase();
436
+ if (tag === "br") {
437
+ out += "\n";
438
+ return;
439
+ }
440
+ const blocky = tag === "div" || tag === "p";
441
+ if (blocky && out !== "" && !out.endsWith("\n")) out += "\n";
442
+ node.childNodes.forEach(walk);
443
+ };
444
+ el.childNodes.forEach(walk);
445
+ return out;
446
+ }
424
447
  var richCtx = null;
425
448
  var richToolbar = null;
426
449
  function wireRichField(el, token, options, mode) {
@@ -1377,11 +1400,48 @@ function togglePageTools(options, forankring) {
1377
1400
  plus.setAttribute("data-testid", "page-tools-tags-plus");
1378
1401
  plus.style.cssText = "background:rgba(255,255,255,.04);border:1px solid #3a3f4a;border-radius:9px;padding:8px 12px;color:#fff;font:600 12px system-ui,sans-serif;cursor:pointer;";
1379
1402
  plus.textContent = "# Tags +";
1403
+ const boks = document.createElement("div");
1404
+ boks.setAttribute("data-testid", "page-tools-tags-boks");
1405
+ boks.style.cssText = "flex:1;min-width:0;display:none;flex-wrap:wrap;align-items:center;gap:5px;background:#12151a;border:1px solid #3a3f4a;border-radius:8px;padding:5px 7px;cursor:text;";
1406
+ const chips = [];
1380
1407
  const felt = document.createElement("input");
1381
1408
  felt.type = "text";
1382
1409
  felt.placeholder = "tag1, tag2, tag3";
1383
1410
  felt.setAttribute("data-testid", "page-tools-tags-input");
1384
- felt.style.cssText = "flex:1;min-width:0;display:none;background:#12151a;border:1px solid #3a3f4a;border-radius:8px;padding:7px 9px;color:#fff;font:12px system-ui,sans-serif;outline:none;";
1411
+ felt.style.cssText = "flex:1;min-width:70px;background:transparent;border:none;padding:2px;color:#fff;font:12px system-ui,sans-serif;outline:none;";
1412
+ const tegnChips = () => {
1413
+ Array.prototype.forEach.call(boks.querySelectorAll("[data-chip]"), (c) => c.remove());
1414
+ chips.forEach((t, i) => {
1415
+ const chip = document.createElement("span");
1416
+ chip.setAttribute("data-chip", "");
1417
+ chip.setAttribute("data-testid", "page-tools-tag-chip");
1418
+ chip.style.cssText = "display:inline-flex;align-items:center;gap:4px;background:rgba(0,178,255,.15);border:1px solid rgba(0,178,255,.45);border-radius:999px;padding:2px 4px 2px 8px;color:#7fd8ff;font:600 11px system-ui,sans-serif;";
1419
+ chip.textContent = t;
1420
+ const x = document.createElement("button");
1421
+ x.type = "button";
1422
+ x.setAttribute("data-testid", "page-tools-tag-fjern");
1423
+ x.setAttribute("aria-label", `Fjern ${t}`);
1424
+ x.style.cssText = "background:none;border:none;color:#7fd8ff;font:700 13px system-ui,sans-serif;cursor:pointer;line-height:1;padding:0 3px;";
1425
+ x.textContent = "\xD7";
1426
+ x.addEventListener("click", (e) => {
1427
+ e.stopPropagation();
1428
+ chips.splice(i, 1);
1429
+ tegnChips();
1430
+ felt.focus();
1431
+ });
1432
+ chip.appendChild(x);
1433
+ boks.insertBefore(chip, felt);
1434
+ });
1435
+ };
1436
+ const tagify = () => {
1437
+ const nye = parseTags(felt.value);
1438
+ for (const t of nye) {
1439
+ if (!chips.some((c) => c.toLowerCase() === t.toLowerCase())) chips.push(t);
1440
+ }
1441
+ felt.value = "";
1442
+ tegnChips();
1443
+ };
1444
+ boks.addEventListener("click", () => felt.focus());
1385
1445
  const gem = document.createElement("button");
1386
1446
  gem.type = "button";
1387
1447
  gem.setAttribute("data-testid", "page-tools-tags-gem");
@@ -1389,12 +1449,13 @@ function togglePageTools(options, forankring) {
1389
1449
  gem.textContent = "Gem";
1390
1450
  plus.addEventListener("click", () => {
1391
1451
  plus.style.display = "none";
1392
- felt.style.display = "block";
1452
+ boks.style.display = "flex";
1393
1453
  gem.style.display = "inline-block";
1394
1454
  felt.focus();
1395
1455
  });
1396
1456
  const gemTags = async () => {
1397
- const nye = parseTags(felt.value);
1457
+ tagify();
1458
+ const nye = [...chips];
1398
1459
  if (!nye.length) return;
1399
1460
  gem.disabled = true;
1400
1461
  gem.textContent = "\u2026";
@@ -1407,6 +1468,8 @@ function togglePageTools(options, forankring) {
1407
1468
  gem.textContent = holdt ? "Gemt \u2713" : "Fejl";
1408
1469
  if (holdt) {
1409
1470
  felt.value = "";
1471
+ chips.length = 0;
1472
+ tegnChips();
1410
1473
  setTimeout(() => {
1411
1474
  gem.textContent = "Gem";
1412
1475
  gem.disabled = false;
@@ -1421,12 +1484,27 @@ function togglePageTools(options, forankring) {
1421
1484
  };
1422
1485
  gem.addEventListener("click", () => void gemTags());
1423
1486
  felt.addEventListener("keydown", (e) => {
1487
+ if (e.key === ",") {
1488
+ e.preventDefault();
1489
+ tagify();
1490
+ return;
1491
+ }
1424
1492
  if (e.key === "Enter") {
1425
1493
  e.preventDefault();
1426
1494
  void gemTags();
1495
+ return;
1427
1496
  }
1497
+ if (e.key === "Backspace" && felt.value === "" && chips.length) {
1498
+ e.preventDefault();
1499
+ chips.pop();
1500
+ tegnChips();
1501
+ }
1502
+ });
1503
+ felt.addEventListener("input", () => {
1504
+ if (felt.value.includes(",")) tagify();
1428
1505
  });
1429
- tagRow.append(plus, felt, gem);
1506
+ boks.appendChild(felt);
1507
+ tagRow.append(plus, boks, gem);
1430
1508
  popup.appendChild(tagRow);
1431
1509
  const lukP\u00E5Klik = (e) => {
1432
1510
  if (!popup.contains(e.target) && !forankring.contains(e.target)) luk();
@@ -1477,6 +1555,7 @@ export {
1477
1555
  isDangerousUrl,
1478
1556
  isExternalHost,
1479
1557
  isSchemeless,
1558
+ plainTextWithBreaks,
1480
1559
  positionPopover,
1481
1560
  renderCurrentRefLine,
1482
1561
  rescanFields,