@domternal/core 0.13.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/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);
@@ -4066,16 +4120,50 @@ var Editor = class _Editor extends EventEmitter {
4066
4120
  return super.emit(event, ...args);
4067
4121
  }
4068
4122
  };
4069
- function positionFloating(reference, floating, options) {
4070
- const placementOpt = options?.placement ?? "bottom";
4123
+ var OPPOSITE_SIDE = {
4124
+ top: "bottom",
4125
+ bottom: "top",
4126
+ left: "right",
4127
+ right: "left"
4128
+ };
4129
+ function oppositePlacement(placement) {
4130
+ const [side = "", alignment] = placement.split("-");
4131
+ const opposite = OPPOSITE_SIDE[side] ?? side;
4132
+ return alignment ? `${opposite}-${alignment}` : opposite;
4133
+ }
4134
+ function buildMiddleware(options, placementOpt) {
4071
4135
  const paddingOpt = options?.padding ?? 10;
4072
4136
  const overflowOpts = options?.boundary ? { padding: paddingOpt, boundary: options.boundary } : { padding: paddingOpt };
4073
- const middleware = [
4074
- dom.offset(options?.offsetValue ?? 4),
4075
- dom.flip(overflowOpts),
4076
- dom.shift(overflowOpts),
4077
- dom.hide()
4078
- ];
4137
+ const constrain = options?.constrainHeight;
4138
+ const middleware = [dom.offset(options?.offsetValue ?? 4)];
4139
+ if (constrain) {
4140
+ middleware.push(
4141
+ dom.size({
4142
+ ...overflowOpts,
4143
+ apply({ availableHeight, elements }) {
4144
+ elements.floating.style.setProperty(
4145
+ "--dm-available-height",
4146
+ `${String(Math.max(constrain.minHeight, Math.floor(availableHeight)))}px`
4147
+ );
4148
+ }
4149
+ })
4150
+ );
4151
+ middleware.push(
4152
+ dom.flip({
4153
+ ...overflowOpts,
4154
+ crossAxis: false,
4155
+ fallbackPlacements: [oppositePlacement(placementOpt)]
4156
+ })
4157
+ );
4158
+ } else {
4159
+ middleware.push(dom.flip(overflowOpts));
4160
+ }
4161
+ middleware.push(dom.shift(overflowOpts));
4162
+ return middleware;
4163
+ }
4164
+ function positionFloating(reference, floating, options) {
4165
+ const placementOpt = options?.placement ?? "bottom";
4166
+ const middleware = [...buildMiddleware(options, placementOpt), dom.hide()];
4079
4167
  const update = () => {
4080
4168
  void dom.computePosition(
4081
4169
  reference,
@@ -4103,13 +4191,7 @@ function positionFloating(reference, floating, options) {
4103
4191
  }
4104
4192
  function positionFloatingOnce(reference, floating, options) {
4105
4193
  const placementOpt = options?.placement ?? "bottom";
4106
- const paddingOpt = options?.padding ?? 10;
4107
- const overflowOpts = options?.boundary ? { padding: paddingOpt, boundary: options.boundary } : { padding: paddingOpt };
4108
- const middleware = [
4109
- dom.offset(options?.offsetValue ?? 4),
4110
- dom.flip(overflowOpts),
4111
- dom.shift(overflowOpts)
4112
- ];
4194
+ const middleware = buildMiddleware(options, placementOpt);
4113
4195
  const update = () => {
4114
4196
  void dom.computePosition(
4115
4197
  reference,
@@ -4210,7 +4292,6 @@ function refocusEditorAfterCommand(view) {
4210
4292
  }
4211
4293
 
4212
4294
  // src/utils/defaultBubbleContexts.ts
4213
- var NOTION_MODE_CLASS = "dm-notion-mode";
4214
4295
  var NOTION_TEXT_CONTEXT = Object.freeze([
4215
4296
  // `ai` leads (Notion's "Ask AI"); skipped with its leading separator when
4216
4297
  // the pro extension is absent, exactly like `mathInline`.
@@ -4238,8 +4319,7 @@ var STANDARD_TEXT_CONTEXT = Object.freeze([
4238
4319
  "link"
4239
4320
  ]);
4240
4321
  function defaultBubbleContexts(editor) {
4241
- const inNotionMode = editor.view.dom.closest("." + NOTION_MODE_CLASS) !== null;
4242
- const text = inNotionMode ? NOTION_TEXT_CONTEXT : STANDARD_TEXT_CONTEXT;
4322
+ const text = editor.preset === "notion" ? NOTION_TEXT_CONTEXT : STANDARD_TEXT_CONTEXT;
4243
4323
  return { text: [...text] };
4244
4324
  }
4245
4325
 
@@ -4701,6 +4781,7 @@ var ToolbarController = class _ToolbarController {
4701
4781
  * Executes a toolbar button's command.
4702
4782
  */
4703
4783
  executeCommand(item) {
4784
+ if (!this.editor.isEditable && item.allowReadOnly !== true) return;
4704
4785
  _ToolbarController.executeItem(this.editor, item);
4705
4786
  this.updateActiveStates();
4706
4787
  }
@@ -4943,16 +5024,20 @@ var ToolbarController = class _ToolbarController {
4943
5024
  checkButtonDisabled(item, canProxy) {
4944
5025
  const wasDisabled = this._disabledMap.get(item.name) ?? false;
4945
5026
  let nowDisabled = false;
4946
- try {
4947
- if (item.emitEvent) {
4948
- nowDisabled = this.editor.isActive("codeBlock");
4949
- } else if (canProxy) {
4950
- const canCmd = canProxy[item.command];
4951
- if (canCmd) {
4952
- nowDisabled = item.commandArgs?.length ? !canCmd(...item.commandArgs) : !canCmd();
5027
+ if (!this.editor.isEditable && item.allowReadOnly !== true) {
5028
+ nowDisabled = true;
5029
+ } else {
5030
+ try {
5031
+ if (item.emitEvent) {
5032
+ nowDisabled = this.editor.isActive("codeBlock");
5033
+ } else if (canProxy) {
5034
+ const canCmd = canProxy[item.command];
5035
+ if (canCmd) {
5036
+ nowDisabled = item.commandArgs?.length ? !canCmd(...item.commandArgs) : !canCmd();
5037
+ }
4953
5038
  }
5039
+ } catch {
4954
5040
  }
4955
- } catch {
4956
5041
  }
4957
5042
  if (wasDisabled !== nowDisabled) {
4958
5043
  this._disabledMap.set(item.name, nowDisabled);
@@ -4993,7 +5078,8 @@ function groupFloatingMenuItems(items) {
4993
5078
  if (!list) {
4994
5079
  list = [];
4995
5080
  map.set(name, list);
4996
- order.push(name);
5081
+ if (name === "") order.unshift(name);
5082
+ else order.push(name);
4997
5083
  }
4998
5084
  list.push(item);
4999
5085
  }
@@ -5364,6 +5450,7 @@ function createFloatingMenuPlugin(options) {
5364
5450
  };
5365
5451
  hideMenu();
5366
5452
  const isVisibleNow = (view) => {
5453
+ if (!editor.isEditable) return false;
5367
5454
  const wantsShow = shouldShow({ editor, view, state: view.state });
5368
5455
  if (!requireExplicitTrigger) return wantsShow;
5369
5456
  const triggered = pluginKey.getState(view.state)?.triggered ?? false;
@@ -5526,7 +5613,8 @@ var defaultIcons = {
5526
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>',
5527
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>',
5528
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>',
5529
- 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>'
5530
5618
  };
5531
5619
 
5532
5620
  // src/nodes/Document.ts
@@ -5743,7 +5831,8 @@ var Heading = Node2.create({
5743
5831
  key: new state.PluginKey("headingKeydownFix"),
5744
5832
  props: {
5745
5833
  handleDOMEvents: {
5746
- keydown(_view, event) {
5834
+ keydown(view, event) {
5835
+ if (!view.editable) return false;
5747
5836
  if (!event.altKey || !(event.metaKey || event.ctrlKey)) return false;
5748
5837
  const level = codeToLevel[event.code];
5749
5838
  if (level === void 0) return false;
@@ -7057,6 +7146,7 @@ var TaskList = Node2.create({
7057
7146
  // src/marks/Bold.ts
7058
7147
  var Bold = Mark.create({
7059
7148
  name: "bold",
7149
+ group: "formatting",
7060
7150
  addOptions() {
7061
7151
  return {
7062
7152
  HTMLAttributes: {}
@@ -7134,6 +7224,7 @@ var Bold = Mark.create({
7134
7224
  // src/marks/Italic.ts
7135
7225
  var Italic = Mark.create({
7136
7226
  name: "italic",
7227
+ group: "formatting",
7137
7228
  addOptions() {
7138
7229
  return {
7139
7230
  HTMLAttributes: {}
@@ -7213,6 +7304,7 @@ var Italic = Mark.create({
7213
7304
  // src/marks/Underline.ts
7214
7305
  var Underline = Mark.create({
7215
7306
  name: "underline",
7307
+ group: "formatting",
7216
7308
  addOptions() {
7217
7309
  return {
7218
7310
  HTMLAttributes: {}
@@ -7265,6 +7357,7 @@ var Underline = Mark.create({
7265
7357
  // src/marks/Strike.ts
7266
7358
  var Strike = Mark.create({
7267
7359
  name: "strike",
7360
+ group: "formatting",
7268
7361
  addOptions() {
7269
7362
  return {
7270
7363
  HTMLAttributes: {}
@@ -7336,9 +7429,14 @@ var Code = Mark.create({
7336
7429
  HTMLAttributes: {}
7337
7430
  };
7338
7431
  },
7339
- // Code mark is exclusive - it cannot be combined with other marks
7340
- // ProseMirror uses '_' to mean "exclude all marks"
7341
- 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",
7342
7440
  // Code should not span across multiple nodes
7343
7441
  spanning: false,
7344
7442
  parseHTML() {
@@ -7826,6 +7924,7 @@ var Link = Mark.create({
7826
7924
  // src/marks/Subscript.ts
7827
7925
  var Subscript = Mark.create({
7828
7926
  name: "subscript",
7927
+ group: "formatting",
7829
7928
  // Mutual exclusion handled in toggle commands (not schema)
7830
7929
  // so can() dry-run works correctly for toolbar disabled state
7831
7930
  excludes: "",
@@ -7887,6 +7986,7 @@ var Subscript = Mark.create({
7887
7986
  // src/marks/Superscript.ts
7888
7987
  var Superscript = Mark.create({
7889
7988
  name: "superscript",
7989
+ group: "formatting",
7890
7990
  // Mutual exclusion handled in toggle commands (not schema)
7891
7991
  // so can() dry-run works correctly for toolbar disabled state
7892
7992
  excludes: "",
@@ -7948,6 +8048,7 @@ var Superscript = Mark.create({
7948
8048
  // src/marks/TextStyle.ts
7949
8049
  var TextStyle = Mark.create({
7950
8050
  name: "textStyle",
8051
+ group: "formatting",
7951
8052
  // Lower priority so it renders after other marks
7952
8053
  priority: 101,
7953
8054
  addOptions() {
@@ -8004,7 +8105,7 @@ var TextStyle = Mark.create({
8004
8105
  let hasEmptyTextStyle = false;
8005
8106
  tr.doc.nodesBetween(from, to, (node) => {
8006
8107
  const textStyleMark = node.marks.find(
8007
- (mark) => mark.type.name === this.name
8108
+ (mark2) => mark2.type.name === this.name
8008
8109
  );
8009
8110
  if (textStyleMark) {
8010
8111
  const hasNonNullAttr = Object.values(textStyleMark.attrs).some(
@@ -8018,9 +8119,9 @@ var TextStyle = Mark.create({
8018
8119
  if (!hasEmptyTextStyle) return false;
8019
8120
  if (dispatch) {
8020
8121
  tr.doc.nodesBetween(from, to, (node, pos) => {
8021
- const mark = node.marks.find((m) => m.type === markType);
8022
- if (mark) {
8023
- 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(
8024
8125
  (v) => v !== null && v !== void 0
8025
8126
  );
8026
8127
  if (!hasActiveAttr) {
@@ -8525,11 +8626,7 @@ var Typography = Extension.create({
8525
8626
  rules.push(
8526
8627
  new inputrules.InputRule(/"([^"]+)"$/, (state, match, start, end) => {
8527
8628
  const text = match[1] ?? "";
8528
- return state.tr.replaceWith(
8529
- start,
8530
- end,
8531
- state.schema.text(openDoubleQuote + text + closeDoubleQuote)
8532
- );
8629
+ return state.tr.insertText(openDoubleQuote + text + closeDoubleQuote, start, end);
8533
8630
  })
8534
8631
  );
8535
8632
  rules.push(
@@ -8537,12 +8634,10 @@ var Typography = Extension.create({
8537
8634
  const text = match[1] ?? "";
8538
8635
  const prefix = match[0].charAt(0);
8539
8636
  const hasPrefix = prefix !== "'";
8540
- return state.tr.replaceWith(
8637
+ return state.tr.insertText(
8638
+ hasPrefix ? prefix + openSingleQuote + text + closeSingleQuote : openSingleQuote + text + closeSingleQuote,
8541
8639
  start,
8542
- end,
8543
- state.schema.text(
8544
- hasPrefix ? prefix + openSingleQuote + text + closeSingleQuote : openSingleQuote + text + closeSingleQuote
8545
- )
8640
+ end
8546
8641
  );
8547
8642
  })
8548
8643
  );
@@ -8807,6 +8902,9 @@ function generateUUID() {
8807
8902
  });
8808
8903
  }
8809
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
+ }
8810
8908
  var UniqueID = Extension.create({
8811
8909
  name: "uniqueID",
8812
8910
  addOptions() {
@@ -8822,7 +8920,13 @@ var UniqueID = Extension.create({
8822
8920
  "listItem",
8823
8921
  "taskItem",
8824
8922
  "image",
8825
- "horizontalRule"
8923
+ "horizontalRule",
8924
+ // Listed even though it lives in a separate package, exactly as
8925
+ // `image` already is: a global attribute only installs on types the
8926
+ // schema actually has, so naming it is inert when the table extension
8927
+ // is not loaded. Without it a table is the one block the block menu
8928
+ // cannot Copy link, and the one an id-anchored feature cannot address.
8929
+ "table"
8826
8930
  ],
8827
8931
  attributeName: "id",
8828
8932
  generateID: generateUUID,
@@ -8850,11 +8954,27 @@ var UniqueID = Extension.create({
8850
8954
  addProseMirrorPlugins() {
8851
8955
  const { types, attributeName, generateID, filterDuplicates } = this.options;
8852
8956
  const editor = this.editor;
8853
- const transformPastedSlice = (slice) => {
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
+ };
8969
+ const transformPastedSlice = (slice, view) => {
8970
+ if (view?.dragging?.move === true) return slice;
8854
8971
  const existingIDs = /* @__PURE__ */ new Set();
8855
- 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;
8856
8975
  const id = node.attrs[attributeName];
8857
8976
  if (id) existingIDs.add(id);
8977
+ return true;
8858
8978
  });
8859
8979
  const transformNode = (node) => {
8860
8980
  if (!types.includes(node.type.name)) {
@@ -8884,7 +9004,36 @@ var UniqueID = Extension.create({
8884
9004
  slice.openEnd
8885
9005
  );
8886
9006
  };
8887
- const assignMissingIDs = (doc, tr) => {
9007
+ const incumbentPositions = (oldDoc, mapPos) => {
9008
+ const incumbents = /* @__PURE__ */ new Map();
9009
+ oldDoc.descendants((node, pos) => {
9010
+ if (!types.includes(node.type.name)) return;
9011
+ const id = node.attrs[attributeName];
9012
+ if (id && !incumbents.has(id)) incumbents.set(id, mapPos(pos));
9013
+ });
9014
+ return incumbents;
9015
+ };
9016
+ const assignMissingIDs = (doc, tr, incumbents) => {
9017
+ const winners = /* @__PURE__ */ new Map();
9018
+ if (incumbents && incumbents.size > 0) {
9019
+ const occurrences = /* @__PURE__ */ new Map();
9020
+ doc.descendants((node, pos) => {
9021
+ if (!types.includes(node.type.name)) return;
9022
+ const id = node.attrs[attributeName];
9023
+ if (!id) return;
9024
+ const list = occurrences.get(id);
9025
+ if (list) list.push(pos);
9026
+ else occurrences.set(id, [pos]);
9027
+ });
9028
+ for (const [id, positions] of occurrences) {
9029
+ if (positions.length < 2) continue;
9030
+ const incumbentPos = incumbents.get(id);
9031
+ winners.set(
9032
+ id,
9033
+ incumbentPos !== void 0 && positions.includes(incumbentPos) ? incumbentPos : positions[0]
9034
+ );
9035
+ }
9036
+ }
8888
9037
  const seen = /* @__PURE__ */ new Set();
8889
9038
  doc.descendants((node, pos) => {
8890
9039
  if (!types.includes(node.type.name)) return;
@@ -8899,7 +9048,9 @@ var UniqueID = Extension.create({
8899
9048
  });
8900
9049
  return;
8901
9050
  }
8902
- if (seen.has(existingID)) {
9051
+ const winner = winners.get(existingID);
9052
+ const yieldsToWinner = winner !== void 0 && winner !== pos;
9053
+ if (seen.has(existingID) || yieldsToWinner) {
8903
9054
  let id = generateID();
8904
9055
  while (seen.has(id)) id = generateID();
8905
9056
  seen.add(id);
@@ -8921,6 +9072,7 @@ var UniqueID = Extension.create({
8921
9072
  const tr = editorView.state.tr;
8922
9073
  assignMissingIDs(editorView.state.doc, tr);
8923
9074
  if (tr.docChanged) {
9075
+ tr.setMeta("addToHistory", false);
8924
9076
  editorView.dispatch(tr);
8925
9077
  }
8926
9078
  }, 0);
@@ -8931,12 +9083,18 @@ var UniqueID = Extension.create({
8931
9083
  };
8932
9084
  },
8933
9085
  // Ensure new nodes get IDs
8934
- appendTransaction(transactions, _oldState, newState) {
9086
+ appendTransaction(transactions, oldState, newState) {
8935
9087
  const docChanged = transactions.some((tr2) => tr2.docChanged);
8936
9088
  if (!docChanged) return null;
9089
+ const mapForward = (pos) => transactions.reduce((p, transaction) => transaction.mapping.map(p), pos);
8937
9090
  const tr = newState.tr;
8938
- assignMissingIDs(newState.doc, tr);
8939
- return tr.docChanged ? tr : null;
9091
+ assignMissingIDs(newState.doc, tr, incumbentPositions(oldState.doc, mapForward));
9092
+ if (!tr.docChanged) return null;
9093
+ const ridesAlongWithAnEdit = transactions.some(
9094
+ (transaction) => transaction.docChanged && transaction.getMeta("addToHistory") !== false
9095
+ );
9096
+ if (!ridesAlongWithAnEdit) tr.setMeta("addToHistory", false);
9097
+ return tr;
8940
9098
  },
8941
9099
  // Handle paste - filter duplicates
8942
9100
  props: filterDuplicates ? {
@@ -9666,13 +9824,13 @@ var Highlight = Extension.create({
9666
9824
  let hasHighlight = false;
9667
9825
  if (empty) {
9668
9826
  const marks = state.storedMarks ?? state.doc.resolve(from).marks();
9669
- const mark = markType.isInSet(marks);
9670
- hasHighlight = !!mark?.attrs["backgroundColor"] || !!mark?.attrs["backgroundColorToken"];
9827
+ const mark2 = markType.isInSet(marks);
9828
+ hasHighlight = !!mark2?.attrs["backgroundColor"] || !!mark2?.attrs["backgroundColorToken"];
9671
9829
  } else {
9672
9830
  state.doc.nodesBetween(from, to, (node) => {
9673
9831
  if (hasHighlight) return false;
9674
- const mark = markType.isInSet(node.marks);
9675
- if (mark?.attrs["backgroundColor"] || mark?.attrs["backgroundColorToken"]) {
9832
+ const mark2 = markType.isInSet(node.marks);
9833
+ if (mark2?.attrs["backgroundColor"] || mark2?.attrs["backgroundColorToken"]) {
9676
9834
  hasHighlight = true;
9677
9835
  return false;
9678
9836
  }
@@ -9719,7 +9877,8 @@ var Highlight = Extension.create({
9719
9877
  const content = match[1];
9720
9878
  if (!content) return null;
9721
9879
  const { tr } = state;
9722
- 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));
9723
9882
  tr.addMark(
9724
9883
  start,
9725
9884
  start + content.length,
@@ -9905,14 +10064,14 @@ var FontSize = Extension.create({
9905
10064
  addToolbarItems() {
9906
10065
  if (this.options.fontSizes.length === 0) return [];
9907
10066
  const sizes = this.options.fontSizes;
9908
- const items = sizes.map((size, i) => ({
10067
+ const items = sizes.map((size2, i) => ({
9909
10068
  type: "button",
9910
- name: `fontSize-${size}`,
10069
+ name: `fontSize-${size2}`,
9911
10070
  command: "setFontSize",
9912
- commandArgs: [size],
9913
- isActive: { name: "textStyle", attributes: { fontSize: size } },
10071
+ commandArgs: [size2],
10072
+ isActive: { name: "textStyle", attributes: { fontSize: size2 } },
9914
10073
  icon: "textSize",
9915
- label: size,
10074
+ label: size2,
9916
10075
  priority: 200 - i
9917
10076
  }));
9918
10077
  if (this.options.showReset) {
@@ -10015,6 +10174,153 @@ var ClearFormatting = Extension.create({
10015
10174
  ];
10016
10175
  }
10017
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
+ }
10018
10324
  var linkPopoverPluginKey = new state.PluginKey("linkPopover");
10019
10325
  function linkPopoverPlugin({ editor, markType, protocols }) {
10020
10326
  const el = document.createElement("div");
@@ -10352,9 +10658,10 @@ function createBubbleMenuPlugin(options) {
10352
10658
  const onDocumentMousedown = (e) => {
10353
10659
  const target = e.target;
10354
10660
  if (!target) return;
10661
+ if (!target.isConnected) return;
10355
10662
  if (element.contains(target)) return;
10356
10663
  if (editor.view.dom.contains(target)) return;
10357
- if (target instanceof HTMLElement && target.closest("[data-dm-editor-ui]")) return;
10664
+ if (target instanceof Element && target.closest("[data-dm-editor-ui]")) return;
10358
10665
  hideMenu();
10359
10666
  suppressed = true;
10360
10667
  };
@@ -10376,7 +10683,7 @@ function createBubbleMenuPlugin(options) {
10376
10683
  if (from !== prevValue.from || to !== prevValue.to) {
10377
10684
  suppressed = false;
10378
10685
  }
10379
- const visible = !suppressed && !selection.empty && shouldShow({
10686
+ const visible = !suppressed && !selection.empty && editor.isEditable && shouldShow({
10380
10687
  editor,
10381
10688
  view: editor.view,
10382
10689
  state: newState,
@@ -10416,7 +10723,7 @@ function createBubbleMenuPlugin(options) {
10416
10723
  if (mouseDown) return;
10417
10724
  const { selection } = editor.view.state;
10418
10725
  const { from, to } = selection;
10419
- const show = !selection.empty && shouldShow({
10726
+ const show = !selection.empty && editor.isEditable && shouldShow({
10420
10727
  editor,
10421
10728
  view: editor.view,
10422
10729
  state: editor.view.state,
@@ -10452,6 +10759,10 @@ function createBubbleMenuPlugin(options) {
10452
10759
  return {
10453
10760
  update: (view, prevState) => {
10454
10761
  if (view.composing) return;
10762
+ if (!view.editable) {
10763
+ hideMenu();
10764
+ return;
10765
+ }
10455
10766
  const state = pluginKey.getState(view.state);
10456
10767
  const prevPluginState = pluginKey.getState(prevState);
10457
10768
  if (state?.visible === prevPluginState?.visible && state?.from === prevPluginState?.from && state?.to === prevPluginState?.to && !(state?.visible && view.state.doc !== prevState.doc)) {
@@ -10584,7 +10895,7 @@ var StarterKit = Extension.create({
10584
10895
  });
10585
10896
 
10586
10897
  // src/index.ts
10587
- var VERSION = "0.1.0";
10898
+ var VERSION = "0.15.0";
10588
10899
 
10589
10900
  Object.defineProperty(exports, "PluginKey", {
10590
10901
  enumerable: true,
@@ -10642,6 +10953,7 @@ exports.NotionColorPicker = NotionColorPicker;
10642
10953
  exports.OrderedList = OrderedList;
10643
10954
  exports.Paragraph = Paragraph;
10644
10955
  exports.Placeholder = Placeholder;
10956
+ exports.Print = Print;
10645
10957
  exports.Selection = Selection5;
10646
10958
  exports.SelectionDecoration = SelectionDecoration;
10647
10959
  exports.StarterKit = StarterKit;