@alpaca-editor/core 1.0.4084 → 1.0.4086

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.
Files changed (46) hide show
  1. package/dist/components/ui/paste-button.d.ts +14 -0
  2. package/dist/components/ui/paste-button.js +114 -0
  3. package/dist/components/ui/paste-button.js.map +1 -0
  4. package/dist/config/config.js +50 -0
  5. package/dist/config/config.js.map +1 -1
  6. package/dist/config/types.d.ts +21 -0
  7. package/dist/editor/FieldListField.js +50 -1
  8. package/dist/editor/FieldListField.js.map +1 -1
  9. package/dist/editor/LinkEditorDialog.d.ts +1 -0
  10. package/dist/editor/LinkEditorDialog.js +4 -2
  11. package/dist/editor/LinkEditorDialog.js.map +1 -1
  12. package/dist/editor/control-center/Setup.d.ts +1 -0
  13. package/dist/editor/control-center/Setup.js +229 -0
  14. package/dist/editor/control-center/Setup.js.map +1 -0
  15. package/dist/editor/field-types/InternalLinkFieldEditor.js +3 -1
  16. package/dist/editor/field-types/InternalLinkFieldEditor.js.map +1 -1
  17. package/dist/editor/field-types/richtext/components/ReactSlate.js +8 -4
  18. package/dist/editor/field-types/richtext/components/ReactSlate.js.map +1 -1
  19. package/dist/editor/field-types/richtext/utils/conversion.js +147 -13
  20. package/dist/editor/field-types/richtext/utils/conversion.js.map +1 -1
  21. package/dist/editor/field-types/richtext/utils/plugins.d.ts +1 -0
  22. package/dist/editor/field-types/richtext/utils/plugins.js +19 -4
  23. package/dist/editor/field-types/richtext/utils/plugins.js.map +1 -1
  24. package/dist/editor/utils/urlUtils.d.ts +9 -0
  25. package/dist/editor/utils/urlUtils.js +25 -0
  26. package/dist/editor/utils/urlUtils.js.map +1 -0
  27. package/dist/editor/utils.d.ts +5 -0
  28. package/dist/editor/utils.js +29 -0
  29. package/dist/editor/utils.js.map +1 -1
  30. package/dist/revision.d.ts +2 -2
  31. package/dist/revision.js +2 -2
  32. package/dist/styles.css +8 -0
  33. package/package.json +1 -1
  34. package/src/components/ui/paste-button.tsx +163 -0
  35. package/src/config/config.tsx +59 -0
  36. package/src/config/types.ts +21 -0
  37. package/src/editor/FieldListField.tsx +62 -0
  38. package/src/editor/LinkEditorDialog.tsx +6 -2
  39. package/src/editor/control-center/Setup.tsx +432 -0
  40. package/src/editor/field-types/InternalLinkFieldEditor.tsx +3 -1
  41. package/src/editor/field-types/richtext/components/ReactSlate.tsx +11 -6
  42. package/src/editor/field-types/richtext/utils/conversion.ts +167 -13
  43. package/src/editor/field-types/richtext/utils/plugins.ts +24 -4
  44. package/src/editor/utils/urlUtils.ts +24 -0
  45. package/src/editor/utils.ts +33 -0
  46. package/src/revision.ts +2 -2
@@ -12,6 +12,8 @@ import {
12
12
  HtmlToSlateResult,
13
13
  SlateToHtmlResult
14
14
  } from '../types';
15
+ import { normalizeUrl } from '../../../utils/urlUtils';
16
+
15
17
 
16
18
  // Elements that should be completely preserved as raw HTML
17
19
  const PRESERVE_AS_RAW_ELEMENTS = new Set([
@@ -241,14 +243,56 @@ const htmlToSlateInternal = (html: string, profile: SimplifiedProfile): Descenda
241
243
  } else if (tagName === 'a') {
242
244
  const href = childElement.getAttribute('href') || '';
243
245
  const target = childElement.getAttribute('target') || '_self';
244
- const isInternal = childElement.hasAttribute('data-internal');
245
- const itemId = childElement.getAttribute('data-item-id') || '';
246
- const targetItemLongId = childElement.getAttribute('data-item-longid') || '';
247
- const queryString = childElement.getAttribute('data-querystring') || '';
246
+ const isInternal = childElement.hasAttribute('data-internal') ||
247
+ href.includes('~/link.aspx?_id=') ||
248
+ href.includes('/sitecore/service/notfound.aspx?item=');
249
+
250
+ let itemId = childElement.getAttribute('data-item-id') || '';
251
+ let targetItemLongId = childElement.getAttribute('data-item-longid') || '';
252
+ let queryString = childElement.getAttribute('data-querystring') || '';
253
+
254
+ // Parse standard Sitecore RTE format: ~/link.aspx?_id=GUID&_z=z
255
+ if (href.includes('~/link.aspx?_id=') && !itemId) {
256
+ // Decode HTML entities (e.g., & to &) before parsing
257
+ const decodedHref = href.replace(/&/g, '&');
258
+ const queryString_raw = decodedHref.split('?')[1];
259
+
260
+ const urlParams = new URLSearchParams(queryString_raw);
261
+ const id = urlParams.get('_id');
262
+ if (id) {
263
+ // Convert GUID format back to standard format with dashes
264
+ itemId = id.replace(/(\w{8})(\w{4})(\w{4})(\w{4})(\w{12})/, '$1-$2-$3-$4-$5').toLowerCase();
265
+ targetItemLongId = itemId;
266
+
267
+ // Extract other query parameters (excluding _id and _z)
268
+ const otherParams = Array.from(urlParams.entries())
269
+ .filter(([key]) => key !== '_id' && key !== '_z')
270
+ .map(([key, value]) => `${key}=${value}`)
271
+ .join('&');
272
+ queryString = otherParams;
273
+ }
274
+ }
275
+
276
+ // Parse Sitecore error page format: /sitecore/service/notfound.aspx?item=core:{GUID}@en
277
+ if (href.includes('/sitecore/service/notfound.aspx?item=') && !itemId) {
278
+ const urlParams = new URLSearchParams(href.split('?')[1]);
279
+ const itemParam = urlParams.get('item');
280
+ if (itemParam) {
281
+ // Decode URL encoding
282
+ const decodedItem = decodeURIComponent(itemParam);
283
+
284
+ // Extract GUID from format like: core:{B84A7A3D-8ED4-4336-BE4F-94D3C38C9E9F}@en
285
+ const guidMatch = decodedItem.match(/\{([A-F0-9-]{36})\}/i);
286
+ if (guidMatch && guidMatch[1]) {
287
+ itemId = guidMatch[1].toLowerCase();
288
+ targetItemLongId = itemId;
289
+ }
290
+ }
291
+ }
248
292
 
249
293
  const linkChildren = processNodeWithInlines(childElement, configuredMarks);
250
294
 
251
- results.push({
295
+ const linkElement = {
252
296
  type: 'link',
253
297
  url: href,
254
298
  link: {
@@ -257,6 +301,33 @@ const htmlToSlateInternal = (html: string, profile: SimplifiedProfile): Descenda
257
301
  target,
258
302
  itemId,
259
303
  targetItemLongId,
304
+ queryString,
305
+ originalHref: (href.includes('~/link.aspx?_id=') && !href.includes('/sitecore/service/notfound.aspx')) ? href : undefined // Preserve original Sitecore href, but not error pages
306
+ },
307
+ children: linkChildren.length ? linkChildren : [{ text: childElement.textContent || 'Link' }]
308
+ };
309
+
310
+
311
+ results.push(linkElement);
312
+ } else if (tagName === 'link') {
313
+ // Handle Sitecore XML link tags
314
+ const linktype = childElement.getAttribute('linktype') || '';
315
+ const itemId = childElement.getAttribute('id') || '';
316
+ const target = childElement.getAttribute('target') || '_self';
317
+ const queryString = childElement.getAttribute('querystring') || '';
318
+ const url = childElement.getAttribute('url') || '';
319
+
320
+ const linkChildren = processNodeWithInlines(childElement, configuredMarks);
321
+
322
+ results.push({
323
+ type: 'link',
324
+ url: linktype === 'internal' ? '' : url,
325
+ link: {
326
+ type: linktype === 'internal' ? 'internal' : 'external',
327
+ url: linktype === 'internal' ? '' : url,
328
+ target,
329
+ itemId: linktype === 'internal' ? itemId : '',
330
+ targetItemLongId: linktype === 'internal' ? itemId : '',
260
331
  queryString
261
332
  },
262
333
  children: linkChildren.length ? linkChildren : [{ text: childElement.textContent || 'Link' }]
@@ -312,14 +383,56 @@ const htmlToSlateInternal = (html: string, profile: SimplifiedProfile): Descenda
312
383
  } else if (tagName === 'a') {
313
384
  const href = childElement.getAttribute('href') || '';
314
385
  const target = childElement.getAttribute('target') || '_self';
315
- const isInternal = childElement.hasAttribute('data-internal');
316
- const itemId = childElement.getAttribute('data-item-id') || '';
317
- const targetItemLongId = childElement.getAttribute('data-item-longid') || '';
318
- const queryString = childElement.getAttribute('data-querystring') || '';
386
+ const isInternal = childElement.hasAttribute('data-internal') ||
387
+ href.includes('~/link.aspx?_id=') ||
388
+ href.includes('/sitecore/service/notfound.aspx?item=');
389
+
390
+ let itemId = childElement.getAttribute('data-item-id') || '';
391
+ let targetItemLongId = childElement.getAttribute('data-item-longid') || '';
392
+ let queryString = childElement.getAttribute('data-querystring') || '';
393
+
394
+ // Parse standard Sitecore RTE format: ~/link.aspx?_id=GUID&_z=z
395
+ if (href.includes('~/link.aspx?_id=') && !itemId) {
396
+ // Decode HTML entities (e.g., & to &) before parsing
397
+ const decodedHref = href.replace(/&/g, '&');
398
+ const queryString_raw = decodedHref.split('?')[1];
399
+
400
+ const urlParams = new URLSearchParams(queryString_raw);
401
+ const id = urlParams.get('_id');
402
+ if (id) {
403
+ // Convert GUID format back to standard format with dashes
404
+ itemId = id.replace(/(\w{8})(\w{4})(\w{4})(\w{4})(\w{12})/, '$1-$2-$3-$4-$5').toLowerCase();
405
+ targetItemLongId = itemId;
406
+
407
+ // Extract other query parameters (excluding _id and _z)
408
+ const otherParams = Array.from(urlParams.entries())
409
+ .filter(([key]) => key !== '_id' && key !== '_z')
410
+ .map(([key, value]) => `${key}=${value}`)
411
+ .join('&');
412
+ queryString = otherParams;
413
+ }
414
+ }
415
+
416
+ // Parse Sitecore error page format: /sitecore/service/notfound.aspx?item=core:{GUID}@en
417
+ if (href.includes('/sitecore/service/notfound.aspx?item=') && !itemId) {
418
+ const urlParams = new URLSearchParams(href.split('?')[1]);
419
+ const itemParam = urlParams.get('item');
420
+ if (itemParam) {
421
+ // Decode URL encoding
422
+ const decodedItem = decodeURIComponent(itemParam);
423
+
424
+ // Extract GUID from format like: core:{B84A7A3D-8ED4-4336-BE4F-94D3C38C9E9F}@en
425
+ const guidMatch = decodedItem.match(/\{([A-F0-9-]{36})\}/i);
426
+ if (guidMatch && guidMatch[1]) {
427
+ itemId = guidMatch[1].toLowerCase();
428
+ targetItemLongId = itemId;
429
+ }
430
+ }
431
+ }
319
432
 
320
433
  const linkChildren = processNodeWithInlines(childElement, configuredMarks);
321
434
 
322
- results.push({
435
+ const linkElement = {
323
436
  type: 'link',
324
437
  url: href,
325
438
  link: {
@@ -328,6 +441,33 @@ const htmlToSlateInternal = (html: string, profile: SimplifiedProfile): Descenda
328
441
  target,
329
442
  itemId,
330
443
  targetItemLongId,
444
+ queryString,
445
+ originalHref: (href.includes('~/link.aspx?_id=') && !href.includes('/sitecore/service/notfound.aspx')) ? href : undefined // Preserve original Sitecore href, but not error pages
446
+ },
447
+ children: linkChildren.length ? linkChildren : [{ text: childElement.textContent || 'Link' }]
448
+ };
449
+
450
+
451
+ results.push(linkElement);
452
+ } else if (tagName === 'link') {
453
+ // Handle Sitecore XML link tags
454
+ const linktype = childElement.getAttribute('linktype') || '';
455
+ const itemId = childElement.getAttribute('id') || '';
456
+ const target = childElement.getAttribute('target') || '_self';
457
+ const queryString = childElement.getAttribute('querystring') || '';
458
+ const url = childElement.getAttribute('url') || '';
459
+
460
+ const linkChildren = processNodeWithInlines(childElement, configuredMarks);
461
+
462
+ results.push({
463
+ type: 'link',
464
+ url: linktype === 'internal' ? '' : url,
465
+ link: {
466
+ type: linktype === 'internal' ? 'internal' : 'external',
467
+ url: linktype === 'internal' ? '' : url,
468
+ target,
469
+ itemId: linktype === 'internal' ? itemId : '',
470
+ targetItemLongId: linktype === 'internal' ? itemId : '',
331
471
  queryString
332
472
  },
333
473
  children: linkChildren.length ? linkChildren : [{ text: childElement.textContent || 'Link' }]
@@ -619,12 +759,26 @@ const slateToHtmlInternal = (value: Descendant[], profile: SimplifiedProfile): s
619
759
 
620
760
  if (isInternal) {
621
761
  const itemId = linkElement.link?.itemId || '';
622
- const targetItemLongId = linkElement.link?.targetItemLongId || '';
623
762
  const target = linkElement.link?.target || '_self';
624
763
  const queryString = linkElement.link?.queryString || '';
625
- result += `<a href="#" data-internal="true" data-item-id="${itemId}" data-item-longid="${targetItemLongId}" target="${target}" data-querystring="${queryString}" class="internal-link">${linkText}</a>`;
764
+ const originalHref = (linkElement.link as any)?.originalHref;
765
+
766
+ let href;
767
+ if (originalHref) {
768
+ // Use original Sitecore RTE href to preserve exact format
769
+ href = originalHref;
770
+ } else {
771
+ // Generate standard Sitecore RTE format for new links
772
+ href = `~/link.aspx?_id=${itemId.replace(/-/g, '').toUpperCase()}&_z=z`;
773
+ if (queryString) {
774
+ href += `&${queryString}`;
775
+ }
776
+ }
777
+
778
+ result += `<a href="${href}" target="${target}">${linkText}</a>`;
626
779
  } else {
627
- const url = linkElement.url || linkElement.link?.url || '#';
780
+ const rawUrl = linkElement.url || linkElement.link?.url || '#';
781
+ const url = normalizeUrl(rawUrl);
628
782
  const target = linkElement.link?.target || '_self';
629
783
  const queryString = linkElement.link?.queryString || '';
630
784
  result += `<a href="${url}" target="${target}" data-querystring="${queryString}">${linkText}</a>`;
@@ -9,6 +9,7 @@ import {
9
9
  SLATE_MARKS,
10
10
  SLATE_BLOCKS,
11
11
  } from "../types";
12
+ import { normalizeUrl } from "../../../utils/urlUtils";
12
13
 
13
14
  // Hot key utility function
14
15
  export const isHotkey = (hotkey: string, event: React.KeyboardEvent): boolean => {
@@ -146,7 +147,7 @@ export const withLink = (editor: Editor) => {
146
147
 
147
148
  editor.insertText = (text: string) => {
148
149
  if (text && isUrl(text)) {
149
- wrapLink(editor, text);
150
+ wrapLink(editor, normalizeUrl(text));
150
151
  } else {
151
152
  insertText(text);
152
153
  }
@@ -156,7 +157,7 @@ export const withLink = (editor: Editor) => {
156
157
  const text = data.getData('text/plain');
157
158
 
158
159
  if (text && isUrl(text)) {
159
- wrapLink(editor, text);
160
+ wrapLink(editor, normalizeUrl(text));
160
161
  } else {
161
162
  insertData(data);
162
163
  }
@@ -586,10 +587,29 @@ const isUrl = (text: string): boolean => {
586
587
  new URL(text);
587
588
  return true;
588
589
  } catch {
589
- return false;
590
+ // Check if it looks like a domain without protocol (like www.nivea.de, nivea.com)
591
+ const domainPattern = /^([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(\/.*)?$/;
592
+ return domainPattern.test(text);
590
593
  }
591
594
  };
592
595
 
596
+
597
+ export const generateInternalLinkUrl = (itemId?: string, targetItemLongId?: string, queryString?: string): string => {
598
+ if (!itemId && !targetItemLongId) {
599
+ return '#';
600
+ }
601
+
602
+ // Use itemId if available, otherwise fall back to targetItemLongId
603
+ const id = itemId || targetItemLongId;
604
+ let url = `/items/${id}`;
605
+
606
+ if (queryString) {
607
+ url += `?${queryString}`;
608
+ }
609
+
610
+ return url;
611
+ };
612
+
593
613
  const wrapLink = (editor: Editor, url: string, linkData?: any): void => {
594
614
  if (editor.isLinkActive()) {
595
615
  unwrapLink(editor);
@@ -605,7 +625,7 @@ const wrapLink = (editor: Editor, url: string, linkData?: any): void => {
605
625
  url,
606
626
  target: '_blank',
607
627
  },
608
- children: isCollapsed ? [{ text: url }] : [],
628
+ children: isCollapsed ? [{ text: url }] : [{ text: '' }], // Use placeholder for selected text
609
629
  };
610
630
 
611
631
  if (isCollapsed) {
@@ -0,0 +1,24 @@
1
+ /**
2
+ * URL utility functions for the editor
3
+ */
4
+
5
+ /**
6
+ * Normalizes a URL by adding https:// protocol if missing for domain-like strings
7
+ * @param text - The text to normalize
8
+ * @returns The normalized URL
9
+ */
10
+ export const normalizeUrl = (text: string): string => {
11
+ if (!text) return text;
12
+
13
+ try {
14
+ new URL(text);
15
+ return text; // Already a valid URL
16
+ } catch {
17
+ // Check if it looks like a domain without protocol
18
+ const domainPattern = /^([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(\/.*)?$/;
19
+ if (domainPattern.test(text)) {
20
+ return `https://${text}`;
21
+ }
22
+ return text; // Return as is if not a recognizable domain
23
+ }
24
+ };
@@ -539,3 +539,36 @@ export function findClosestFieldElement(node: Node | null): HTMLElement | null {
539
539
 
540
540
  return null;
541
541
  }
542
+
543
+ // GUID helpers for clipboard parsing and validation
544
+ export const GUID_REGEX_GLOBAL =
545
+ /[\{\(]?[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}[\}\)]?/g;
546
+ export const GUID_REGEX_EXACT =
547
+ /^[\{\(]?[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}[\}\)]?$/;
548
+ // Non-global regex for quick containment checks (avoids lastIndex issues with .test on /g)
549
+ export const GUID_REGEX_CONTAINS =
550
+ /[\{\(]?[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}[\}\)]?/;
551
+
552
+ export function containsGuidInText(text: string | undefined | null): boolean {
553
+ if (!text) return false;
554
+ return GUID_REGEX_CONTAINS.test(text);
555
+ }
556
+
557
+ export function findGuidsInText(text: string | undefined | null): string[] {
558
+ if (!text) return [];
559
+ const matches = text.match(GUID_REGEX_GLOBAL) || [];
560
+ const normalized = matches
561
+ .map((m) => m.replace(/[{}()]/g, ""))
562
+ .filter((m) => !!m)
563
+ .map((m) => normalizeGuid(m));
564
+ // Deduplicate while preserving order
565
+ const seen = new Set<string>();
566
+ const unique: string[] = [];
567
+ for (const id of normalized) {
568
+ if (!seen.has(id)) {
569
+ seen.add(id);
570
+ unique.push(id);
571
+ }
572
+ }
573
+ return unique;
574
+ }
package/src/revision.ts CHANGED
@@ -1,2 +1,2 @@
1
- export const version = "1.0.4084";
2
- export const buildDate = "2025-09-11 12:15:31";
1
+ export const version = "1.0.4086";
2
+ export const buildDate = "2025-09-12 01:46:21";