suneditor 3.3.1 → 3.3.2

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.
@@ -71,6 +71,7 @@ export const DEFAULTS = {
71
71
  '@text': 'font-family|font-size|color|background-color|width|height',
72
72
  '@line': 'text-align|margin|margin-left|margin-right|line-height',
73
73
  '@component': 'width|height|min-width',
74
+ li: 'font-family|font-size|color|background-color|font-weight|font-style',
74
75
  'table|th|td':
75
76
  'border|border-[a-z]+|color|background-color|text-align|float|font-weight|text-decoration|font-style|vertical-align',
76
77
  'table|td': 'width',
@@ -457,7 +458,7 @@ export const DEFAULTS = {
457
458
  * - Value is a pipe-delimited list of allowed style names.
458
459
  * - Resolution order when filtering an element: `@component` (for `.se-component` containers) → explicit tag entry → `@line` (for formatLine elements) → `@text` (for textStyleTags).
459
460
  * - `@component` guards the inline sizing (`width`/`height`/`min-width`) the editor writes on a media component's container for percentage-based sizes; keep these so a clean() round-trip does not reset the component to full width.
460
- * - An explicit tag entry **replaces** the category default — include category styles in the value if you want both.
461
+ * - An explicit tag entry is **merged** with its category default when the tag belongs to one (`@line` for formatLine elements, else `@text` for textStyleTags) — the entry adds styles on top of the category's.
461
462
  * - Merged with {@link DEFAULTS.TAG_STYLES}; user-supplied keys win.
462
463
  * ```js
463
464
  * {
@@ -465,8 +466,8 @@ export const DEFAULTS = {
465
466
  * '@text': 'color|font-size|background-color', // default for span, b, i, em, ...
466
467
  * '@line': 'text-align|margin|line-height', // default for p, h1-h6, div, li, ...
467
468
  * 'table|td': 'border|color|background-color', // per-tag whitelist
468
- * div: 'color', // explicit override; ignores `@line` default
469
- * hr: 'border-top',
469
+ * div: 'color', // merged with the `@line` default (div is a line element)
470
+ * hr: 'border-top',
470
471
  * }
471
472
  * }
472
473
  * ```
package/src/events.js CHANGED
@@ -17,7 +17,7 @@
17
17
  * @property {Event} event - event object
18
18
  * @property {string} data - drop data
19
19
  * @property {boolean} maxCharCount - is max char count
20
- * @property {string} from - `"SE"`|`"MS"`|`""` - source
20
+ * @property {string} from - `"SE"`|`"MS"`|`"GOOGLE"`|`""` - source
21
21
  */
22
22
 
23
23
  // --- media
@@ -513,7 +513,8 @@ export function getEdgeChildNodes(first, last) {
513
513
 
514
514
  /**
515
515
  * @template {Node} T
516
- * @description Gets the previous sibling last child. If there is no sibling, then it'll take it from the closest ancestor with child
516
+ * @description Gets the previous sibling last child. If there is no sibling, then it'll take it from the closest ancestor with child.
517
+ * - Components (image, table, etc.) are treated as a single tag and not traversed into.
517
518
  * @param {Node} node Reference element
518
519
  * @param {?Node} [ceiling] Highest boundary allowed
519
520
  * @returns {T|null} Not found: `null`
@@ -533,14 +534,17 @@ export function getPreviousDeepestNode(node, ceiling) {
533
534
 
534
535
  if (domCheck.isNonEditable(previousNode)) return /** @type {T} */ (/** @type {unknown} */ (previousNode));
535
536
 
536
- while (previousNode.lastChild) previousNode = previousNode.lastChild;
537
+ while (!domCheck.isComponentContainer(previousNode) && previousNode.lastChild) {
538
+ previousNode = previousNode.lastChild;
539
+ }
537
540
 
538
541
  return /** @type {T} */ (/** @type {unknown} */ (previousNode));
539
542
  }
540
543
 
541
544
  /**
542
545
  * @template {Node} T
543
- * @description Gets the next sibling first child. If there is no sibling, then it'll take it from the closest ancestor with child
546
+ * @description Gets the next sibling first child. If there is no sibling, then it'll take it from the closest ancestor with child.
547
+ * - Components (image, table, etc.) are treated as a single tag and not traversed into.
544
548
  * @param {Node} node Reference element
545
549
  * @param {?Node} [ceiling] Highest boundary allowed
546
550
  * @returns {T|null} Not found: `null`
@@ -560,7 +564,9 @@ export function getNextDeepestNode(node, ceiling) {
560
564
 
561
565
  if (domCheck.isNonEditable(nextNode)) return /** @type {T} */ (/** @type {unknown} */ (nextNode));
562
566
 
563
- while (nextNode.firstChild) nextNode = nextNode.firstChild;
567
+ while (!domCheck.isComponentContainer(nextNode) && nextNode.firstChild) {
568
+ nextNode = nextNode.firstChild;
569
+ }
564
570
 
565
571
  return /** @type {T} */ (/** @type {unknown} */ (nextNode));
566
572
  }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @description Cleans Google Docs clipboard HTML.
3
+ */
4
+
5
+ /** Matches the Google Docs clipboard wrapper id */
6
+ const _RE_GUID = /id=["']?docs-internal-guid-/i;
7
+
8
+ /**
9
+ * @description Whether the HTML string is a Google Docs clipboard payload.
10
+ * @param {string} html HTML string
11
+ * @returns {boolean}
12
+ */
13
+ export function isGoogleDocs(html) {
14
+ return _RE_GUID.test(html);
15
+ }
16
+
17
+ /**
18
+ * @description Removes the Google Docs clipboard wrapper tags, keeping their children.
19
+ * - Only the guid-carrying inline wrappers (`b`/`span`) are unwrapped; real formatting tags inside are untouched.
20
+ * @param {string} html HTML string
21
+ * @returns {string} HTML string
22
+ */
23
+ export function cleanHTML(html) {
24
+ const doc = new DOMParser().parseFromString(html, 'text/html');
25
+ const wrappers = doc.body.querySelectorAll('b[id^="docs-internal-guid-"], span[id^="docs-internal-guid-"]');
26
+ if (wrappers.length === 0) return html;
27
+
28
+ for (let i = 0, len = wrappers.length, w; i < len; i++) {
29
+ w = wrappers[i];
30
+ while (w.firstChild) w.parentNode.insertBefore(w.firstChild, w);
31
+ w.parentNode.removeChild(w);
32
+ }
33
+
34
+ return doc.body.innerHTML;
35
+ }
36
+
37
+ export default {
38
+ isGoogleDocs,
39
+ cleanHTML,
40
+ };
@@ -7,6 +7,7 @@ import KeyCodeMap from './keyCodeMap';
7
7
  import Clipboard from './clipboard';
8
8
  import Markdown from './markdown';
9
9
  import MSOffice from './msOffice';
10
+ import GoogleDocs from './googleDocs';
10
11
 
11
12
  export const env = Env;
12
13
  export const unicode = Unicode;
@@ -17,6 +18,7 @@ export const keyCodeMap = KeyCodeMap;
17
18
  export const clipboard = Clipboard;
18
19
  export const markdown = Markdown;
19
20
  export const msOffice = MSOffice;
21
+ export const googleDocs = GoogleDocs;
20
22
 
21
23
  export default {
22
24
  env,
@@ -28,4 +30,5 @@ export default {
28
30
  clipboard,
29
31
  markdown,
30
32
  msOffice,
33
+ googleDocs,
31
34
  };
@@ -51,3 +51,59 @@ export function isRtlBidiMismatch(
51
51
  detectedEdge: 'front' | 'end',
52
52
  doc: Document,
53
53
  ): boolean;
54
+ /**
55
+ * @description Whether the caret sits on a bare `<br>` that stands at the front/end edge of its `line`.
56
+ * @param {Range} range - The current range
57
+ * @param {Node} selectionNode - Current selection node
58
+ * @param {'front'|'end'} edge - Edge to test: `front` for Backspace, `end` for Delete
59
+ * @returns {boolean} `true` if the caret is on an edge `<br>`
60
+ */
61
+ export function isEdgeBreakCaret(range: Range, selectionNode: Node, edge: 'front' | 'end'): boolean;
62
+ /**
63
+ * @description The previous/next element in document order — crossing in and out of blocks (list, quote).
64
+ * - Out of list-cell ancestors (a nested list), stopping at a closure block (table cell).
65
+ * @param {EventPorts['format']} format - Format module
66
+ * @param {?HTMLElement} line - The caret's `line` element
67
+ * @param {'front'|'end'} edge - `front` for the previous element, `end` for the next one
68
+ * @returns {?HTMLElement} The adjacent element, or `null` at the document edge
69
+ */
70
+ export function getAdjacentElement(
71
+ format: EventPorts['format'],
72
+ line: HTMLElement | null,
73
+ edge: 'front' | 'end',
74
+ ): HTMLElement | null;
75
+ /**
76
+ * @description The previous/next `line` in document order — {@link getAdjacentElement} filtered to lines.
77
+ * - `null` means either the document edge or a non-`line` neighbour (a component); use
78
+ * {@link getAdjacentElement} when the two must be told apart.
79
+ * @param {EventPorts['format']} format - Format module
80
+ * @param {?HTMLElement} line - The caret's `line` element
81
+ * @param {'front'|'end'} edge - `front` for the previous line, `end` for the next one
82
+ * @returns {?HTMLElement} The adjacent line, or `null`
83
+ */
84
+ export function getAdjacentLine(
85
+ format: EventPorts['format'],
86
+ line: HTMLElement | null,
87
+ edge: 'front' | 'end',
88
+ ): HTMLElement | null;
89
+ /**
90
+ * @description The neighbouring `line` an empty line collapses into, or `null` when there is nothing to merge.
91
+ * - A cell owning a nested list belongs to {@link getNestedListTarget} instead.
92
+ * @param {EventPorts['format']} format - Format module
93
+ * @param {?HTMLElement} formatEl - The caret's `line` element
94
+ * @param {'front'|'end'} edge - `front` for Backspace (previous line), `end` for Delete (next line)
95
+ * @returns {?HTMLElement} The neighbouring line to merge into, or `null`
96
+ */
97
+ export function getEmptyLineMergeTarget(
98
+ format: EventPorts['format'],
99
+ formatEl: HTMLElement | null,
100
+ edge: 'front' | 'end',
101
+ ): HTMLElement | null;
102
+ /**
103
+ * @description The nested list a list-cell Backspace/Delete would lift, or `null` when there is none.
104
+ * - The rules gate their list branch on it so the branch can't claim the key with nothing to do.
105
+ * @param {HTMLElement} formatEl - The caret's list cell
106
+ * @param {HTMLElement} rangeEl - The list (`UL`/`OL`) the cell belongs to
107
+ * @returns {?HTMLElement} The element carrying the nested list, or `null`
108
+ */
109
+ export function getNestedListTarget(formatEl: HTMLElement, rangeEl: HTMLElement): HTMLElement | null;
@@ -28,6 +28,7 @@ export namespace DEFAULTS {
28
28
  '@text': string;
29
29
  '@line': string;
30
30
  '@component': string;
31
+ li: string;
31
32
  'table|th|td': string;
32
33
  'table|td': string;
33
34
  tr: string;
@@ -400,7 +401,7 @@ export namespace DEFAULTS {
400
401
  * - Value is a pipe-delimited list of allowed style names.
401
402
  * - Resolution order when filtering an element: `@component` (for `.se-component` containers) → explicit tag entry → `@line` (for formatLine elements) → `@text` (for textStyleTags).
402
403
  * - `@component` guards the inline sizing (`width`/`height`/`min-width`) the editor writes on a media component's container for percentage-based sizes; keep these so a clean() round-trip does not reset the component to full width.
403
- * - An explicit tag entry **replaces** the category default — include category styles in the value if you want both.
404
+ * - An explicit tag entry is **merged** with its category default when the tag belongs to one (`@line` for formatLine elements, else `@text` for textStyleTags) — the entry adds styles on top of the category's.
404
405
  * - Merged with {@link DEFAULTS.TAG_STYLES}; user-supplied keys win.
405
406
  * ```js
406
407
  * {
@@ -408,8 +409,8 @@ export namespace DEFAULTS {
408
409
  * '@text': 'color|font-size|background-color', // default for span, b, i, em, ...
409
410
  * '@line': 'text-align|margin|line-height', // default for p, h1-h6, div, li, ...
410
411
  * 'table|td': 'border|color|background-color', // per-tag whitelist
411
- * div: 'color', // explicit override; ignores `@line` default
412
- * hr: 'border-top',
412
+ * div: 'color', // merged with the `@line` default (div is a line element)
413
+ * hr: 'border-top',
413
414
  * }
414
415
  * }
415
416
  * ```
@@ -1254,7 +1255,7 @@ export type EditorBaseOptions = {
1254
1255
  * - Value is a pipe-delimited list of allowed style names.
1255
1256
  * - Resolution order when filtering an element: `@component` (for `.se-component` containers) → explicit tag entry → `@line` (for formatLine elements) → `@text` (for textStyleTags).
1256
1257
  * - `@component` guards the inline sizing (`width`/`height`/`min-width`) the editor writes on a media component's container for percentage-based sizes; keep these so a clean() round-trip does not reset the component to full width.
1257
- * - An explicit tag entry **replaces** the category default — include category styles in the value if you want both.
1258
+ * - An explicit tag entry is **merged** with its category default when the tag belongs to one (`@line` for formatLine elements, else `@text` for textStyleTags) — the entry adds styles on top of the category's.
1258
1259
  * - Merged with {@link DEFAULTS.TAG_STYLES}; user-supplied keys win.
1259
1260
  * ```js
1260
1261
  * {
@@ -1262,7 +1263,7 @@ export type EditorBaseOptions = {
1262
1263
  * '@text': 'color|font-size|background-color', // default for span, b, i, em, ...
1263
1264
  * '@line': 'text-align|margin|line-height', // default for p, h1-h6, div, li, ...
1264
1265
  * 'table|td': 'border|color|background-color', // per-tag whitelist
1265
- * div: 'color', // explicit override; ignores `@line` default
1266
+ * div: 'color', // merged with the `@line` default (div is a line element)
1266
1267
  * hr: 'border-top',
1267
1268
  * }
1268
1269
  * }
package/types/events.d.ts CHANGED
@@ -35,7 +35,7 @@ export type ClipboardEvent = {
35
35
  */
36
36
  maxCharCount: boolean;
37
37
  /**
38
- * - `"SE"`|`"MS"`|`""` - source
38
+ * - `"SE"`|`"MS"`|`"GOOGLE"`|`""` - source
39
39
  */
40
40
  from: string;
41
41
  };
@@ -289,7 +289,7 @@ export type EventHandlers = {
289
289
  * @property {Event} event - event object
290
290
  * @property {string} data - drop data
291
291
  * @property {boolean} maxCharCount - is max char count
292
- * @property {string} from - `"SE"`|`"MS"`|`""` - source
292
+ * @property {string} from - `"SE"`|`"MS"`|`"GOOGLE"`|`""` - source
293
293
  */
294
294
  /**
295
295
  * @typedef {Object} FileManagementInfo
@@ -198,7 +198,8 @@ export function getEdgeChildNodes(
198
198
  };
199
199
  /**
200
200
  * @template {Node} T
201
- * @description Gets the previous sibling last child. If there is no sibling, then it'll take it from the closest ancestor with child
201
+ * @description Gets the previous sibling last child. If there is no sibling, then it'll take it from the closest ancestor with child.
202
+ * - Components (image, table, etc.) are treated as a single tag and not traversed into.
202
203
  * @param {Node} node Reference element
203
204
  * @param {?Node} [ceiling] Highest boundary allowed
204
205
  * @returns {T|null} Not found: `null`
@@ -206,7 +207,8 @@ export function getEdgeChildNodes(
206
207
  export function getPreviousDeepestNode<T extends Node>(node: Node, ceiling?: Node | null): T | null;
207
208
  /**
208
209
  * @template {Node} T
209
- * @description Gets the next sibling first child. If there is no sibling, then it'll take it from the closest ancestor with child
210
+ * @description Gets the next sibling first child. If there is no sibling, then it'll take it from the closest ancestor with child.
211
+ * - Components (image, table, etc.) are treated as a single tag and not traversed into.
210
212
  * @param {Node} node Reference element
211
213
  * @param {?Node} [ceiling] Highest boundary allowed
212
214
  * @returns {T|null} Not found: `null`
@@ -0,0 +1,19 @@
1
+ import type {} from '../typedef';
2
+ /**
3
+ * @description Whether the HTML string is a Google Docs clipboard payload.
4
+ * @param {string} html HTML string
5
+ * @returns {boolean}
6
+ */
7
+ export function isGoogleDocs(html: string): boolean;
8
+ /**
9
+ * @description Removes the Google Docs clipboard wrapper tags, keeping their children.
10
+ * - Only the guid-carrying inline wrappers (`b`/`span`) are unwrapped; real formatting tags inside are untouched.
11
+ * @param {string} html HTML string
12
+ * @returns {string} HTML string
13
+ */
14
+ export function cleanHTML(html: string): string;
15
+ declare namespace _default {
16
+ export { isGoogleDocs };
17
+ export { cleanHTML };
18
+ }
19
+ export default _default;
@@ -178,6 +178,10 @@ export const markdown: {
178
178
  export const msOffice: {
179
179
  cleanHTML: typeof import('./msOffice').cleanHTML;
180
180
  };
181
+ export const googleDocs: {
182
+ isGoogleDocs: typeof import('./googleDocs').isGoogleDocs;
183
+ cleanHTML: typeof import('./googleDocs').cleanHTML;
184
+ };
181
185
  declare namespace _default {
182
186
  export { env };
183
187
  export { unicode };
@@ -188,5 +192,6 @@ declare namespace _default {
188
192
  export { clipboard };
189
193
  export { markdown };
190
194
  export { msOffice };
195
+ export { googleDocs };
191
196
  }
192
197
  export default _default;