@domternal/core 0.14.0 → 0.15.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 CHANGED
@@ -78,6 +78,47 @@ const editor = new Editor({
78
78
  });
79
79
  ```
80
80
 
81
+ ## Presets
82
+
83
+ `preset: 'notion'` switches the editor to the Notion-style experience in one option. It paints
84
+ the `dm-notion-mode` class on the `.dm-editor` host for you, and preset-aware code follows it:
85
+ the bubble menu serves the Notion text context, and images offer align controls instead of
86
+ float. `'classic'` is the default.
87
+
88
+ ```ts
89
+ const editor = new Editor({
90
+ element: document.getElementById('editor')!,
91
+ extensions: [StarterKit],
92
+ preset: 'notion',
93
+ });
94
+ ```
95
+
96
+ `editor.preset` reports the resolved value, and still reports `'notion'` for a host that carries
97
+ the theme class alone, so setups that predate the option are unaffected.
98
+
99
+ ## Printing
100
+
101
+ `Print` is not part of `StarterKit`, so add it explicitly. `printDocument()` isolates the
102
+ document from the host page before the browser's dialog opens, leaving your application's
103
+ sidebar, header, and everything else beside the document off the page, and it emits
104
+ `beforePrint` and `afterPrint` so you can set `document.title` or add page rules first.
105
+
106
+ ```ts
107
+ import { Editor, StarterKit, Print } from '@domternal/core';
108
+
109
+ const editor = new Editor({
110
+ element: document.getElementById('editor')!,
111
+ extensions: [StarterKit, Print],
112
+ });
113
+
114
+ editor.commands.printDocument();
115
+ ```
116
+
117
+ The hiding itself is CSS, and it lives in
118
+ [`@domternal/theme`](https://www.npmjs.com/package/@domternal/theme), whose paper layer also
119
+ applies to the reader's own Ctrl/Cmd+P with no code involved. Set `isolateNativePrint: true` to
120
+ give that shortcut the same isolation as the command.
121
+
81
122
  ## SSR
82
123
 
83
124
  The `generateHTML`, `generateJSON`, and `generateText` helpers render content
package/dist/index.cjs CHANGED
@@ -1140,7 +1140,8 @@ function markInputRule(options) {
1140
1140
  return null;
1141
1141
  }
1142
1142
  const { tr } = state;
1143
- tr.replaceWith(start, end, state.schema.text(textContent));
1143
+ const marks = tr.storedMarks ?? tr.doc.resolve(start).marksAcross(tr.doc.resolve(end));
1144
+ tr.replaceWith(start, end, state.schema.text(textContent, marks));
1144
1145
  tr.addMark(start, start + textContent.length, type.create(attributes ?? void 0));
1145
1146
  tr.removeStoredMark(type);
1146
1147
  return tr;
@@ -1232,7 +1233,10 @@ function textInputRule(options) {
1232
1233
  const { find: find2, replace, undoable } = options;
1233
1234
  return new inputrules.InputRule(
1234
1235
  find2,
1235
- (state, _match, start, end) => state.tr.replaceWith(start, end, state.schema.text(replace)),
1236
+ // insertText inherits the replaced range's marks (stored marks first,
1237
+ // then marksAcross), so a replacement inside marked text keeps the
1238
+ // marks instead of punching an unmarked hole into them.
1239
+ (state, _match, start, end) => state.tr.insertText(replace, start, end),
1236
1240
  undoable !== void 0 ? { undoable } : {}
1237
1241
  );
1238
1242
  }
@@ -1811,6 +1815,8 @@ var Mark = class _Mark extends Extension {
1811
1815
  if (this.config.group !== void 0) spec.group = this.config.group;
1812
1816
  if (this.config.spanning !== void 0)
1813
1817
  spec.spanning = this.config.spanning;
1818
+ if (this.config.keepOnDuplicate !== void 0)
1819
+ spec["keepOnDuplicate"] = this.config.keepOnDuplicate;
1814
1820
  const attributeSpecs = callOrReturn(this.config.addAttributes, this);
1815
1821
  if (attributeSpecs) {
1816
1822
  spec.attrs = buildProseMirrorAttrs(attributeSpecs);
@@ -1848,9 +1854,9 @@ var Mark = class _Mark extends Extension {
1848
1854
  const renderFn = this.config.renderHTML;
1849
1855
  const attrSpecs = attributeSpecs;
1850
1856
  const markInstance = this;
1851
- spec.toDOM = (mark, _inline) => {
1852
- const htmlAttrs = attrSpecs ? buildHTMLAttributes(mark.attrs, attrSpecs) : {};
1853
- return renderFn.call(markInstance, { mark, HTMLAttributes: htmlAttrs });
1857
+ spec.toDOM = (mark2, _inline) => {
1858
+ const htmlAttrs = attrSpecs ? buildHTMLAttributes(mark2.attrs, attrSpecs) : {};
1859
+ return renderFn.call(markInstance, { mark: mark2, HTMLAttributes: htmlAttrs });
1854
1860
  };
1855
1861
  }
1856
1862
  return spec;
@@ -1943,8 +1949,8 @@ var setMark = (markName, attributes) => ({ state, tr, dispatch }) => {
1943
1949
  const from = firstRange.$from.pos;
1944
1950
  const existingMark = tr.storedMarks?.find((m) => m.type === markType) ?? state.storedMarks?.find((m) => m.type === markType) ?? tr.doc.resolve(from).marks().find((m) => m.type === markType) ?? null;
1945
1951
  const mergedAttrs = existingMark ? { ...existingMark.attrs, ...attributes } : attributes;
1946
- const mark = markType.create(mergedAttrs);
1947
- tr.addStoredMark(mark);
1952
+ const mark2 = markType.create(mergedAttrs);
1953
+ tr.addStoredMark(mark2);
1948
1954
  dispatch(tr);
1949
1955
  return true;
1950
1956
  }
@@ -2571,12 +2577,12 @@ var updateAttributes = (typeOrName, attributes) => ({ state, tr, dispatch }) =>
2571
2577
  const markType = state.schema.marks[typeOrName];
2572
2578
  tr.doc.nodesBetween(from, to, (node, pos) => {
2573
2579
  if (!node.isInline) return;
2574
- const mark = markType.isInSet(node.marks);
2575
- if (mark) {
2580
+ const mark2 = markType.isInSet(node.marks);
2581
+ if (mark2) {
2576
2582
  markChanges.push({
2577
2583
  pos,
2578
2584
  nodeSize: node.nodeSize,
2579
- attrs: { ...mark.attrs, ...attributes }
2585
+ attrs: { ...mark2.attrs, ...attributes }
2580
2586
  });
2581
2587
  }
2582
2588
  });
@@ -2622,12 +2628,12 @@ var resetAttributes = (typeOrName, attributeName) => ({ state, tr, dispatch }) =
2622
2628
  const defaultValue = markType.spec.attrs?.[attributeName]?.default;
2623
2629
  tr.doc.nodesBetween(from, to, (node, pos) => {
2624
2630
  if (!node.isInline) return;
2625
- const mark = markType.isInSet(node.marks);
2626
- if (mark) {
2631
+ const mark2 = markType.isInSet(node.marks);
2632
+ if (mark2) {
2627
2633
  markChanges.push({
2628
2634
  pos,
2629
2635
  nodeSize: node.nodeSize,
2630
- attrs: { ...mark.attrs, [attributeName]: defaultValue }
2636
+ attrs: { ...mark2.attrs, [attributeName]: defaultValue }
2631
2637
  });
2632
2638
  }
2633
2639
  });
@@ -3510,6 +3516,12 @@ var Editor = class _Editor extends EventEmitter {
3510
3516
  * True while EditorView's constructor runs; see buildViewDispatch.
3511
3517
  */
3512
3518
  _isViewConstructing = false;
3519
+ /**
3520
+ * The `.dm-editor` host this editor painted `dm-notion-mode` onto because
3521
+ * of `preset: 'notion'`. Tracked so destroy() removes only a class the
3522
+ * editor itself added, never one the consumer wrote.
3523
+ */
3524
+ _presetClassHost = null;
3513
3525
  /**
3514
3526
  * Creates a new Editor instance
3515
3527
  *
@@ -3554,6 +3566,43 @@ var Editor = class _Editor extends EventEmitter {
3554
3566
  get isEditable() {
3555
3567
  return this.view ? this.view.editable : this.options.editable ?? true;
3556
3568
  }
3569
+ /**
3570
+ * The resolved editing-experience preset.
3571
+ *
3572
+ * The `preset` option wins when provided (so an explicit 'classic' can
3573
+ * opt out of everything). Otherwise a `dm-notion-mode` class on or above
3574
+ * the view counts as 'notion': consumers that predate the option declare
3575
+ * Notion mode with the theme class alone, and behavior must follow what
3576
+ * the user actually sees. Resolved on every read, not cached, so a class
3577
+ * toggled at runtime is picked up.
3578
+ */
3579
+ get preset() {
3580
+ if (this.options.preset) {
3581
+ return this.options.preset;
3582
+ }
3583
+ if (this.view?.dom.closest(".dm-notion-mode")) {
3584
+ return "notion";
3585
+ }
3586
+ return "classic";
3587
+ }
3588
+ /**
3589
+ * Paints `dm-notion-mode` on the `.dm-editor` host when the editor was
3590
+ * created with `preset: 'notion'`. Runs during creation, and framework
3591
+ * wrappers call it again after adopting the view's DOM: they construct
3592
+ * the editor in a detached element, so the creation-time run cannot see
3593
+ * the host yet. Idempotent; a no-op for any other preset. Only a class
3594
+ * added here is removed again on destroy.
3595
+ */
3596
+ adoptPresetClass() {
3597
+ if (this.options.preset !== "notion" || this._presetClassHost) {
3598
+ return;
3599
+ }
3600
+ const host = this.view.dom.closest(".dm-editor");
3601
+ if (host && !host.classList.contains("dm-notion-mode")) {
3602
+ host.classList.add("dm-notion-mode");
3603
+ this._presetClassHost = host;
3604
+ }
3605
+ }
3557
3606
  /**
3558
3607
  * Checks if the editor content is empty
3559
3608
  */
@@ -3639,11 +3688,11 @@ var Editor = class _Editor extends EventEmitter {
3639
3688
  if (markType) {
3640
3689
  if (selection.empty) {
3641
3690
  const storedMarks = state.storedMarks ?? $from.marks();
3642
- const hasMark = storedMarks.some((mark) => mark.type === markType);
3691
+ const hasMark = storedMarks.some((mark2) => mark2.type === markType);
3643
3692
  if (!hasMark) return false;
3644
3693
  if (attrs) {
3645
- const mark = storedMarks.find((m) => m.type === markType);
3646
- return mark ? this.matchAttributes(mark.attrs, attrs) : false;
3694
+ const mark2 = storedMarks.find((m) => m.type === markType);
3695
+ return mark2 ? this.matchAttributes(mark2.attrs, attrs) : false;
3647
3696
  }
3648
3697
  return true;
3649
3698
  }
@@ -3718,8 +3767,8 @@ var Editor = class _Editor extends EventEmitter {
3718
3767
  const markType = schema.marks[name];
3719
3768
  if (markType) {
3720
3769
  const marks = state.storedMarks ?? $from.marks();
3721
- const mark = marks.find((m) => m.type === markType);
3722
- return mark ? { ...mark.attrs } : {};
3770
+ const mark2 = marks.find((m) => m.type === markType);
3771
+ return mark2 ? { ...mark2.attrs } : {};
3723
3772
  }
3724
3773
  const nodeType = schema.nodes[name];
3725
3774
  if (nodeType) {
@@ -3900,6 +3949,10 @@ var Editor = class _Editor extends EventEmitter {
3900
3949
  }
3901
3950
  this.emit("destroy");
3902
3951
  this.options.onDestroy?.();
3952
+ if (this._presetClassHost) {
3953
+ this._presetClassHost.classList.remove("dm-notion-mode");
3954
+ this._presetClassHost = null;
3955
+ }
3903
3956
  this.view.destroy();
3904
3957
  this._extensionManager.destroy();
3905
3958
  this.removeAllListeners();
@@ -4002,6 +4055,7 @@ var Editor = class _Editor extends EventEmitter {
4002
4055
  }
4003
4056
  });
4004
4057
  this._isViewConstructing = false;
4058
+ this.adoptPresetClass();
4005
4059
  this.emit("mount", { editor: this, view: this.view });
4006
4060
  this.options.onMount?.({ editor: this, view: this.view });
4007
4061
  this.commandManager = new CommandManager(this);
@@ -4238,7 +4292,6 @@ function refocusEditorAfterCommand(view) {
4238
4292
  }
4239
4293
 
4240
4294
  // src/utils/defaultBubbleContexts.ts
4241
- var NOTION_MODE_CLASS = "dm-notion-mode";
4242
4295
  var NOTION_TEXT_CONTEXT = Object.freeze([
4243
4296
  // `ai` leads (Notion's "Ask AI"); skipped with its leading separator when
4244
4297
  // the pro extension is absent, exactly like `mathInline`.
@@ -4266,8 +4319,7 @@ var STANDARD_TEXT_CONTEXT = Object.freeze([
4266
4319
  "link"
4267
4320
  ]);
4268
4321
  function defaultBubbleContexts(editor) {
4269
- const inNotionMode = editor.view.dom.closest("." + NOTION_MODE_CLASS) !== null;
4270
- const text = inNotionMode ? NOTION_TEXT_CONTEXT : STANDARD_TEXT_CONTEXT;
4322
+ const text = editor.preset === "notion" ? NOTION_TEXT_CONTEXT : STANDARD_TEXT_CONTEXT;
4271
4323
  return { text: [...text] };
4272
4324
  }
4273
4325
 
@@ -5026,7 +5078,8 @@ function groupFloatingMenuItems(items) {
5026
5078
  if (!list) {
5027
5079
  list = [];
5028
5080
  map.set(name, list);
5029
- order.push(name);
5081
+ if (name === "") order.unshift(name);
5082
+ else order.push(name);
5030
5083
  }
5031
5084
  list.push(item);
5032
5085
  }
@@ -5560,7 +5613,8 @@ var defaultIcons = {
5560
5613
  plus: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M224,128a8,8,0,0,1-8,8H136v80a8,8,0,0,1-16,0V136H40a8,8,0,0,1,0-16h80V40a8,8,0,0,1,16,0v80h80A8,8,0,0,1,224,128Z"/></svg>',
5561
5614
  dotsSixVertical: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M108,60A16,16,0,1,1,92,44,16,16,0,0,1,108,60Zm56,16A16,16,0,1,0,148,60,16,16,0,0,0,164,76ZM92,112a16,16,0,1,0,16,16A16,16,0,0,0,92,112Zm72,0a16,16,0,1,0,16,16A16,16,0,0,0,164,112ZM92,180a16,16,0,1,0,16,16A16,16,0,0,0,92,180Zm72,0a16,16,0,1,0,16,16A16,16,0,0,0,164,180Z"/></svg>',
5562
5615
  dotsThree: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M140,128a12,12,0,1,1-12-12A12,12,0,0,1,140,128ZM64,116a12,12,0,1,0,12,12A12,12,0,0,0,64,116Zm128,0a12,12,0,1,0,12,12A12,12,0,0,0,192,116Z"/></svg>',
5563
- copy: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M216,32H88a8,8,0,0,0-8,8V80H40a8,8,0,0,0-8,8V216a8,8,0,0,0,8,8H168a8,8,0,0,0,8-8V176h40a8,8,0,0,0,8-8V40A8,8,0,0,0,216,32ZM160,208H48V96H160Zm48-48H176V88a8,8,0,0,0-8-8H96V48H208Z"/></svg>'
5616
+ copy: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M216,32H88a8,8,0,0,0-8,8V80H40a8,8,0,0,0-8,8V216a8,8,0,0,0,8,8H168a8,8,0,0,0,8-8V176h40a8,8,0,0,0,8-8V40A8,8,0,0,0,216,32ZM160,208H48V96H160Zm48-48H176V88a8,8,0,0,0-8-8H96V48H208Z"/></svg>',
5617
+ printer: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M214.67,72H200V40a8,8,0,0,0-8-8H64a8,8,0,0,0-8,8V72H41.33C27.36,72,16,82.77,16,96v80a8,8,0,0,0,8,8H56v32a8,8,0,0,0,8,8H192a8,8,0,0,0,8-8V184h32a8,8,0,0,0,8-8V96C240,82.77,228.64,72,214.67,72ZM72,48H184V72H72ZM184,208H72V160H184Zm40-40H200V152a8,8,0,0,0-8-8H64a8,8,0,0,0-8,8v16H32V96c0-4.34,4.28-8,9.33-8H214.67c5.05,0,9.33,3.66,9.33,8Zm-24-52a12,12,0,1,1-12-12A12,12,0,0,1,200,116Z"/></svg>'
5564
5618
  };
5565
5619
 
5566
5620
  // src/nodes/Document.ts
@@ -7092,6 +7146,7 @@ var TaskList = Node2.create({
7092
7146
  // src/marks/Bold.ts
7093
7147
  var Bold = Mark.create({
7094
7148
  name: "bold",
7149
+ group: "formatting",
7095
7150
  addOptions() {
7096
7151
  return {
7097
7152
  HTMLAttributes: {}
@@ -7169,6 +7224,7 @@ var Bold = Mark.create({
7169
7224
  // src/marks/Italic.ts
7170
7225
  var Italic = Mark.create({
7171
7226
  name: "italic",
7227
+ group: "formatting",
7172
7228
  addOptions() {
7173
7229
  return {
7174
7230
  HTMLAttributes: {}
@@ -7248,6 +7304,7 @@ var Italic = Mark.create({
7248
7304
  // src/marks/Underline.ts
7249
7305
  var Underline = Mark.create({
7250
7306
  name: "underline",
7307
+ group: "formatting",
7251
7308
  addOptions() {
7252
7309
  return {
7253
7310
  HTMLAttributes: {}
@@ -7300,6 +7357,7 @@ var Underline = Mark.create({
7300
7357
  // src/marks/Strike.ts
7301
7358
  var Strike = Mark.create({
7302
7359
  name: "strike",
7360
+ group: "formatting",
7303
7361
  addOptions() {
7304
7362
  return {
7305
7363
  HTMLAttributes: {}
@@ -7371,9 +7429,14 @@ var Code = Mark.create({
7371
7429
  HTMLAttributes: {}
7372
7430
  };
7373
7431
  },
7374
- // Code mark is exclusive - it cannot be combined with other marks
7375
- // ProseMirror uses '_' to mean "exclude all marks"
7376
- excludes: "_",
7432
+ // Code cannot be combined with other FORMATTING marks (bold, italic,
7433
+ // color...), but semantic marks outside the group (link, a comment
7434
+ // thread anchor) survive: code excludes the 'formatting' group instead
7435
+ // of '_' (everything). Third-party formatting marks opt into the same
7436
+ // exclusion by declaring group: 'formatting'. Code is in the group
7437
+ // itself, which also keeps it self-exclusive.
7438
+ group: "formatting",
7439
+ excludes: "formatting",
7377
7440
  // Code should not span across multiple nodes
7378
7441
  spanning: false,
7379
7442
  parseHTML() {
@@ -7861,6 +7924,7 @@ var Link = Mark.create({
7861
7924
  // src/marks/Subscript.ts
7862
7925
  var Subscript = Mark.create({
7863
7926
  name: "subscript",
7927
+ group: "formatting",
7864
7928
  // Mutual exclusion handled in toggle commands (not schema)
7865
7929
  // so can() dry-run works correctly for toolbar disabled state
7866
7930
  excludes: "",
@@ -7922,6 +7986,7 @@ var Subscript = Mark.create({
7922
7986
  // src/marks/Superscript.ts
7923
7987
  var Superscript = Mark.create({
7924
7988
  name: "superscript",
7989
+ group: "formatting",
7925
7990
  // Mutual exclusion handled in toggle commands (not schema)
7926
7991
  // so can() dry-run works correctly for toolbar disabled state
7927
7992
  excludes: "",
@@ -7983,6 +8048,7 @@ var Superscript = Mark.create({
7983
8048
  // src/marks/TextStyle.ts
7984
8049
  var TextStyle = Mark.create({
7985
8050
  name: "textStyle",
8051
+ group: "formatting",
7986
8052
  // Lower priority so it renders after other marks
7987
8053
  priority: 101,
7988
8054
  addOptions() {
@@ -8039,7 +8105,7 @@ var TextStyle = Mark.create({
8039
8105
  let hasEmptyTextStyle = false;
8040
8106
  tr.doc.nodesBetween(from, to, (node) => {
8041
8107
  const textStyleMark = node.marks.find(
8042
- (mark) => mark.type.name === this.name
8108
+ (mark2) => mark2.type.name === this.name
8043
8109
  );
8044
8110
  if (textStyleMark) {
8045
8111
  const hasNonNullAttr = Object.values(textStyleMark.attrs).some(
@@ -8053,9 +8119,9 @@ var TextStyle = Mark.create({
8053
8119
  if (!hasEmptyTextStyle) return false;
8054
8120
  if (dispatch) {
8055
8121
  tr.doc.nodesBetween(from, to, (node, pos) => {
8056
- const mark = node.marks.find((m) => m.type === markType);
8057
- if (mark) {
8058
- const hasActiveAttr = Object.values(mark.attrs).some(
8122
+ const mark2 = node.marks.find((m) => m.type === markType);
8123
+ if (mark2) {
8124
+ const hasActiveAttr = Object.values(mark2.attrs).some(
8059
8125
  (v) => v !== null && v !== void 0
8060
8126
  );
8061
8127
  if (!hasActiveAttr) {
@@ -8560,11 +8626,7 @@ var Typography = Extension.create({
8560
8626
  rules.push(
8561
8627
  new inputrules.InputRule(/"([^"]+)"$/, (state, match, start, end) => {
8562
8628
  const text = match[1] ?? "";
8563
- return state.tr.replaceWith(
8564
- start,
8565
- end,
8566
- state.schema.text(openDoubleQuote + text + closeDoubleQuote)
8567
- );
8629
+ return state.tr.insertText(openDoubleQuote + text + closeDoubleQuote, start, end);
8568
8630
  })
8569
8631
  );
8570
8632
  rules.push(
@@ -8572,12 +8634,10 @@ var Typography = Extension.create({
8572
8634
  const text = match[1] ?? "";
8573
8635
  const prefix = match[0].charAt(0);
8574
8636
  const hasPrefix = prefix !== "'";
8575
- return state.tr.replaceWith(
8637
+ return state.tr.insertText(
8638
+ hasPrefix ? prefix + openSingleQuote + text + closeSingleQuote : openSingleQuote + text + closeSingleQuote,
8576
8639
  start,
8577
- end,
8578
- state.schema.text(
8579
- hasPrefix ? prefix + openSingleQuote + text + closeSingleQuote : openSingleQuote + text + closeSingleQuote
8580
- )
8640
+ end
8581
8641
  );
8582
8642
  })
8583
8643
  );
@@ -8842,6 +8902,9 @@ function generateUUID() {
8842
8902
  });
8843
8903
  }
8844
8904
  var uniqueIDPluginKey = new state.PluginKey("uniqueID");
8905
+ function isWithin(from, to, ranges) {
8906
+ return ranges.some((range) => from >= range.from && to <= range.to);
8907
+ }
8845
8908
  var UniqueID = Extension.create({
8846
8909
  name: "uniqueID",
8847
8910
  addOptions() {
@@ -8891,12 +8954,27 @@ var UniqueID = Extension.create({
8891
8954
  addProseMirrorPlugins() {
8892
8955
  const { types, attributeName, generateID, filterDuplicates } = this.options;
8893
8956
  const editor = this.editor;
8957
+ const replacedRanges = (view) => {
8958
+ if (view?.dragging) return [];
8959
+ const ranges = view?.state?.selection?.ranges;
8960
+ if (ranges === void 0) return [];
8961
+ const replaced = [];
8962
+ for (const range of ranges) {
8963
+ if (range.$to.pos > range.$from.pos) {
8964
+ replaced.push({ from: range.$from.pos, to: range.$to.pos });
8965
+ }
8966
+ }
8967
+ return replaced;
8968
+ };
8894
8969
  const transformPastedSlice = (slice, view) => {
8895
8970
  if (view?.dragging?.move === true) return slice;
8896
8971
  const existingIDs = /* @__PURE__ */ new Set();
8897
- editor?.state.doc.descendants((node) => {
8972
+ const replaced = replacedRanges(view);
8973
+ editor?.state.doc.descendants((node, pos) => {
8974
+ if (isWithin(pos, pos + node.nodeSize, replaced)) return false;
8898
8975
  const id = node.attrs[attributeName];
8899
8976
  if (id) existingIDs.add(id);
8977
+ return true;
8900
8978
  });
8901
8979
  const transformNode = (node) => {
8902
8980
  if (!types.includes(node.type.name)) {
@@ -9746,13 +9824,13 @@ var Highlight = Extension.create({
9746
9824
  let hasHighlight = false;
9747
9825
  if (empty) {
9748
9826
  const marks = state.storedMarks ?? state.doc.resolve(from).marks();
9749
- const mark = markType.isInSet(marks);
9750
- hasHighlight = !!mark?.attrs["backgroundColor"] || !!mark?.attrs["backgroundColorToken"];
9827
+ const mark2 = markType.isInSet(marks);
9828
+ hasHighlight = !!mark2?.attrs["backgroundColor"] || !!mark2?.attrs["backgroundColorToken"];
9751
9829
  } else {
9752
9830
  state.doc.nodesBetween(from, to, (node) => {
9753
9831
  if (hasHighlight) return false;
9754
- const mark = markType.isInSet(node.marks);
9755
- if (mark?.attrs["backgroundColor"] || mark?.attrs["backgroundColorToken"]) {
9832
+ const mark2 = markType.isInSet(node.marks);
9833
+ if (mark2?.attrs["backgroundColor"] || mark2?.attrs["backgroundColorToken"]) {
9756
9834
  hasHighlight = true;
9757
9835
  return false;
9758
9836
  }
@@ -9799,7 +9877,8 @@ var Highlight = Extension.create({
9799
9877
  const content = match[1];
9800
9878
  if (!content) return null;
9801
9879
  const { tr } = state;
9802
- tr.replaceWith(start, end, state.schema.text(content));
9880
+ const marks = tr.storedMarks ?? tr.doc.resolve(start).marksAcross(tr.doc.resolve(end));
9881
+ tr.replaceWith(start, end, state.schema.text(content, marks));
9803
9882
  tr.addMark(
9804
9883
  start,
9805
9884
  start + content.length,
@@ -10095,6 +10174,153 @@ var ClearFormatting = Extension.create({
10095
10174
  ];
10096
10175
  }
10097
10176
  });
10177
+
10178
+ // src/extensions/Print.ts
10179
+ var ROOT_CLASS = "dm-print-root";
10180
+ var ANCESTOR_CLASS = "dm-print-ancestor";
10181
+ var PRINTING_CLASS = "dm-printing";
10182
+ var marked = /* @__PURE__ */ new Set();
10183
+ var printing = false;
10184
+ var Print = Extension.create({
10185
+ name: "print",
10186
+ addOptions() {
10187
+ return {
10188
+ toolbar: true,
10189
+ root: null,
10190
+ isolateNativePrint: false
10191
+ };
10192
+ },
10193
+ addStorage() {
10194
+ return { cleanup: null };
10195
+ },
10196
+ addCommands() {
10197
+ return {
10198
+ printDocument: () => ({ dispatch }) => {
10199
+ if (!dispatch) return true;
10200
+ const editor = this.editor;
10201
+ if (!editor || typeof window === "undefined") return false;
10202
+ const root = resolveRoot(editor, this.options.root);
10203
+ if (!root) return false;
10204
+ mark(root);
10205
+ try {
10206
+ emit(editor, "beforePrint", { root });
10207
+ window.print();
10208
+ } finally {
10209
+ unmark();
10210
+ emit(editor, "afterPrint", void 0);
10211
+ }
10212
+ return true;
10213
+ }
10214
+ };
10215
+ },
10216
+ addToolbarItems() {
10217
+ if (!this.options.toolbar) return [];
10218
+ return [
10219
+ {
10220
+ type: "button",
10221
+ name: "print",
10222
+ command: "printDocument",
10223
+ icon: "printer",
10224
+ label: "Print",
10225
+ shortcut: "Mod-P",
10226
+ group: "document",
10227
+ priority: 100,
10228
+ // Reading a document out to paper is not editing it, so the button
10229
+ // stays live in a read-only editor.
10230
+ allowReadOnly: true
10231
+ }
10232
+ ];
10233
+ },
10234
+ addKeyboardShortcuts() {
10235
+ return {
10236
+ // Only bound while the caret is in the editor, which is exactly when
10237
+ // the reader means "print this document" rather than "print this
10238
+ // page". Everywhere else the browser's own Ctrl/Cmd+P is untouched.
10239
+ "Mod-p": () => this.editor?.commands.printDocument() ?? false
10240
+ };
10241
+ },
10242
+ onCreate() {
10243
+ if (!this.options.isolateNativePrint) return;
10244
+ if (typeof window === "undefined") return;
10245
+ const editor = this.editor;
10246
+ if (!editor) return;
10247
+ const resolve = this.options.root;
10248
+ const before = () => {
10249
+ if (printing) return;
10250
+ const root = resolveRoot(editor, resolve);
10251
+ if (!root) return;
10252
+ mark(root);
10253
+ emit(editor, "beforePrint", { root });
10254
+ };
10255
+ const after = () => {
10256
+ if (!printing) return;
10257
+ unmark();
10258
+ emit(editor, "afterPrint", void 0);
10259
+ };
10260
+ window.addEventListener("beforeprint", before);
10261
+ window.addEventListener("afterprint", after);
10262
+ let detachMedia = null;
10263
+ const onMediaChange = (event) => {
10264
+ if (event.matches) before();
10265
+ else after();
10266
+ };
10267
+ if (typeof window.matchMedia === "function") {
10268
+ const media = window.matchMedia("print");
10269
+ if (typeof media.addEventListener === "function") {
10270
+ media.addEventListener("change", onMediaChange);
10271
+ detachMedia = () => {
10272
+ media.removeEventListener("change", onMediaChange);
10273
+ };
10274
+ }
10275
+ }
10276
+ this.storage.cleanup = () => {
10277
+ window.removeEventListener("beforeprint", before);
10278
+ window.removeEventListener("afterprint", after);
10279
+ detachMedia?.();
10280
+ };
10281
+ },
10282
+ onDestroy() {
10283
+ this.storage.cleanup?.();
10284
+ this.storage.cleanup = null;
10285
+ unmark();
10286
+ }
10287
+ });
10288
+ function resolveRoot(editor, resolve) {
10289
+ if (resolve) return resolve(editor);
10290
+ const dom = editor.view.dom;
10291
+ return dom.closest(".dm-editor") ?? dom;
10292
+ }
10293
+ function mark(root) {
10294
+ root.classList.add(ROOT_CLASS);
10295
+ marked.add(root);
10296
+ let node = parentOf(root);
10297
+ while (node) {
10298
+ node.classList.add(ANCESTOR_CLASS);
10299
+ marked.add(node);
10300
+ node = parentOf(node);
10301
+ }
10302
+ document.body.classList.add(PRINTING_CLASS);
10303
+ printing = true;
10304
+ }
10305
+ function parentOf(node) {
10306
+ if (node.parentElement) return node.parentElement;
10307
+ if (typeof ShadowRoot === "undefined") return null;
10308
+ const root = node.getRootNode();
10309
+ return root instanceof ShadowRoot ? root.host : null;
10310
+ }
10311
+ function unmark() {
10312
+ printing = false;
10313
+ if (typeof document === "undefined") return;
10314
+ document.body.classList.remove(PRINTING_CLASS);
10315
+ for (const el of marked) {
10316
+ el.classList.remove(ROOT_CLASS, ANCESTOR_CLASS);
10317
+ }
10318
+ marked.clear();
10319
+ }
10320
+ function emit(editor, name, payload) {
10321
+ const bus = editor;
10322
+ bus.emit?.(name, payload);
10323
+ }
10098
10324
  var linkPopoverPluginKey = new state.PluginKey("linkPopover");
10099
10325
  function linkPopoverPlugin({ editor, markType, protocols }) {
10100
10326
  const el = document.createElement("div");
@@ -10432,9 +10658,10 @@ function createBubbleMenuPlugin(options) {
10432
10658
  const onDocumentMousedown = (e) => {
10433
10659
  const target = e.target;
10434
10660
  if (!target) return;
10661
+ if (!target.isConnected) return;
10435
10662
  if (element.contains(target)) return;
10436
10663
  if (editor.view.dom.contains(target)) return;
10437
- if (target instanceof HTMLElement && target.closest("[data-dm-editor-ui]")) return;
10664
+ if (target instanceof Element && target.closest("[data-dm-editor-ui]")) return;
10438
10665
  hideMenu();
10439
10666
  suppressed = true;
10440
10667
  };
@@ -10668,7 +10895,7 @@ var StarterKit = Extension.create({
10668
10895
  });
10669
10896
 
10670
10897
  // src/index.ts
10671
- var VERSION = "0.14.0";
10898
+ var VERSION = "0.15.0";
10672
10899
 
10673
10900
  Object.defineProperty(exports, "PluginKey", {
10674
10901
  enumerable: true,
@@ -10726,6 +10953,7 @@ exports.NotionColorPicker = NotionColorPicker;
10726
10953
  exports.OrderedList = OrderedList;
10727
10954
  exports.Paragraph = Paragraph;
10728
10955
  exports.Placeholder = Placeholder;
10956
+ exports.Print = Print;
10729
10957
  exports.Selection = Selection5;
10730
10958
  exports.SelectionDecoration = SelectionDecoration;
10731
10959
  exports.StarterKit = StarterKit;