@domternal/core 0.8.0 → 0.9.1

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.js CHANGED
@@ -1,11 +1,11 @@
1
- import { Plugin, PluginKey, TextSelection, Selection, NodeSelection, AllSelection, EditorState } from '@domternal/pm/state';
1
+ import { PluginKey, Plugin, TextSelection, Selection, NodeSelection, AllSelection, EditorState } from '@domternal/pm/state';
2
2
  export { PluginKey } from '@domternal/pm/state';
3
3
  import { DecorationSet, Decoration, EditorView } from '@domternal/pm/view';
4
4
  import { Slice, Fragment, Schema, Node as Node$1, DOMSerializer, DOMParser } from '@domternal/pm/model';
5
5
  import { keymap } from '@domternal/pm/keymap';
6
6
  import { splitBlock, chainCommands, newlineInCode, createParagraphNear, liftEmptyBlock, baseKeymap, selectNodeBackward as selectNodeBackward$1 } from '@domternal/pm/commands';
7
7
  import { InputRule } from '@domternal/pm/inputrules';
8
- import { canJoin, findWrapping, liftTarget } from '@domternal/pm/transform';
8
+ import { canJoin, canSplit, findWrapping, liftTarget } from '@domternal/pm/transform';
9
9
  import { liftListItem, sinkListItem, splitListItem, wrapRangeInList } from '@domternal/pm/schema-list';
10
10
  import { offset, flip, shift, autoUpdate, hide, computePosition } from '@floating-ui/dom';
11
11
  import { find } from 'linkifyjs';
@@ -1017,6 +1017,27 @@ var deleteSelection = () => ({ tr, dispatch }) => {
1017
1017
  dispatch(tr);
1018
1018
  return true;
1019
1019
  };
1020
+ var LIST_WRAPPER_TYPES = /* @__PURE__ */ new Set(["bulletList", "orderedList", "taskList"]);
1021
+ function isEmptyPlaceholderList(node) {
1022
+ if (!LIST_WRAPPER_TYPES.has(node.type.name) || node.childCount !== 1) return false;
1023
+ const item = node.child(0);
1024
+ return item.childCount === 1 && item.child(0).type.name === "paragraph" && item.child(0).content.size === 0;
1025
+ }
1026
+ function stripFitterArtifacts(node) {
1027
+ if (!node.isBlock || node.isLeaf) return node;
1028
+ const children = [];
1029
+ node.forEach((child) => children.push(stripFitterArtifacts(child)));
1030
+ const kept = [];
1031
+ children.forEach((cur, i) => {
1032
+ const next = children[i + 1];
1033
+ if (isEmptyPlaceholderList(cur) && next && LIST_WRAPPER_TYPES.has(next.type.name) && next.type !== cur.type) {
1034
+ return;
1035
+ }
1036
+ kept.push(cur);
1037
+ });
1038
+ const unchanged = kept.length === node.childCount && kept.every((c, i) => c === node.child(i));
1039
+ return unchanged ? node : node.copy(Fragment.fromArray(kept));
1040
+ }
1020
1041
  function isJSONContent(content) {
1021
1042
  return typeof content === "object" && content !== null && "type" in content && typeof content.type === "string";
1022
1043
  }
@@ -1039,7 +1060,7 @@ function parseHTMLContent(html, schema, options) {
1039
1060
  const element = document.createElement("div");
1040
1061
  element.innerHTML = html;
1041
1062
  const parser = DOMParser.fromSchema(schema);
1042
- return parser.parse(element, options?.parseOptions);
1063
+ return stripFitterArtifacts(parser.parse(element, options?.parseOptions));
1043
1064
  }
1044
1065
  function createDocument(content, schema, options) {
1045
1066
  if (content === null || content === void 0 || content === "") {
@@ -2278,7 +2299,7 @@ function joinListForwards(tr, listType) {
2278
2299
  }
2279
2300
  }
2280
2301
  }
2281
- var toggleList = (listNodeName, listItemNodeName, attributes) => ({ state, tr, dispatch }) => {
2302
+ var toggleList = (listNodeName, listItemNodeName, attributes, options) => ({ state, tr, dispatch }) => {
2282
2303
  const listType = state.schema.nodes[listNodeName];
2283
2304
  const listItemType = state.schema.nodes[listItemNodeName];
2284
2305
  if (!listType || !listItemType) {
@@ -2410,6 +2431,17 @@ var toggleList = (listNodeName, listItemNodeName, attributes) => ({ state, tr, d
2410
2431
  const contentBlocks = collectListContext(tr.doc, from, to);
2411
2432
  const allInTargetList = contentBlocks.length > 0 && contentBlocks.every((b) => b.inTargetList);
2412
2433
  const allInSomeList = contentBlocks.length > 0 && contentBlocks.every((b) => b.inSomeList);
2434
+ if (options?.perItem && from === to && allInSomeList && !allInTargetList) {
2435
+ if (!dispatch) return true;
2436
+ if (liftCurrentListItem(state, tr)) {
2437
+ const blockRange = tr.selection.$from.blockRange();
2438
+ if (blockRange) wrapRangeInList(tr, blockRange, listType, attributes);
2439
+ joinListBackwards(tr, listType);
2440
+ joinListForwards(tr, listType);
2441
+ dispatch(tr.scrollIntoView());
2442
+ return true;
2443
+ }
2444
+ }
2413
2445
  if (allInTargetList) {
2414
2446
  const first = contentBlocks[0];
2415
2447
  const last = contentBlocks[contentBlocks.length - 1];
@@ -3496,7 +3528,7 @@ var Editor = class extends EventEmitter {
3496
3528
  * Checks if the editor is editable
3497
3529
  */
3498
3530
  get isEditable() {
3499
- return this.view.editable;
3531
+ return this.view ? this.view.editable : this.options.editable ?? true;
3500
3532
  }
3501
3533
  /**
3502
3534
  * Checks if the editor content is empty
@@ -3993,10 +4025,11 @@ var Editor = class extends EventEmitter {
3993
4025
  function positionFloating(reference, floating, options) {
3994
4026
  const placementOpt = options?.placement ?? "bottom";
3995
4027
  const paddingOpt = options?.padding ?? 10;
4028
+ const overflowOpts = options?.boundary ? { padding: paddingOpt, boundary: options.boundary } : { padding: paddingOpt };
3996
4029
  const middleware = [
3997
4030
  offset(options?.offsetValue ?? 4),
3998
- flip({ padding: paddingOpt }),
3999
- shift({ padding: paddingOpt }),
4031
+ flip(overflowOpts),
4032
+ shift(overflowOpts),
4000
4033
  hide()
4001
4034
  ];
4002
4035
  const update = () => {
@@ -4027,10 +4060,11 @@ function positionFloating(reference, floating, options) {
4027
4060
  function positionFloatingOnce(reference, floating, options) {
4028
4061
  const placementOpt = options?.placement ?? "bottom";
4029
4062
  const paddingOpt = options?.padding ?? 10;
4063
+ const overflowOpts = options?.boundary ? { padding: paddingOpt, boundary: options.boundary } : { padding: paddingOpt };
4030
4064
  const middleware = [
4031
4065
  offset(options?.offsetValue ?? 4),
4032
- flip({ padding: paddingOpt }),
4033
- shift({ padding: paddingOpt })
4066
+ flip(overflowOpts),
4067
+ shift(overflowOpts)
4034
4068
  ];
4035
4069
  const update = () => {
4036
4070
  void computePosition(
@@ -4129,12 +4163,12 @@ function defaultBubbleContexts(editor) {
4129
4163
 
4130
4164
  // src/utils/insertAsListItemChild.ts
4131
4165
  var LIST_ITEM_TYPES3 = /* @__PURE__ */ new Set(["listItem", "taskItem"]);
4132
- var LIST_WRAPPER_TYPES = /* @__PURE__ */ new Set(["bulletList", "orderedList", "taskList"]);
4166
+ var LIST_WRAPPER_TYPES2 = /* @__PURE__ */ new Set(["bulletList", "orderedList", "taskList"]);
4133
4167
  function insertAsListItemChild(args) {
4134
4168
  const { tr, wrapperPos, targetItemPos, blockNode, sourceRange, childIndex } = args;
4135
4169
  if (wrapperPos < 0 || wrapperPos >= tr.doc.content.size) return { ok: false };
4136
4170
  const wrapper = tr.doc.nodeAt(wrapperPos);
4137
- if (!wrapper || !LIST_WRAPPER_TYPES.has(wrapper.type.name)) return { ok: false };
4171
+ if (!wrapper || !LIST_WRAPPER_TYPES2.has(wrapper.type.name)) return { ok: false };
4138
4172
  if (wrapper.childCount === 0) return { ok: false };
4139
4173
  let targetItem;
4140
4174
  let targetItemStart;
@@ -5122,6 +5156,226 @@ var FloatingMenuController = class _FloatingMenuController {
5122
5156
  if (changed) this.onChange();
5123
5157
  }
5124
5158
  };
5159
+ var floatingMenuPluginKey = new PluginKey("floatingMenu");
5160
+ function defaultFloatingMenuShouldShow({
5161
+ editor,
5162
+ state
5163
+ }) {
5164
+ if (!editor.isEditable) return false;
5165
+ const { selection } = state;
5166
+ const { $from, empty } = selection;
5167
+ if (!empty) return false;
5168
+ if ($from.parent.type.name !== "paragraph") return false;
5169
+ if ($from.parent.content.size !== 0) return false;
5170
+ if ($from.parentOffset !== 0) return false;
5171
+ return true;
5172
+ }
5173
+ var DEFAULT_ENTER_MENU_SHORTCUTS = ["Alt-F10", "Mod-/"];
5174
+ var FLOATING_MENU_META = "dm:floatingMenuTrigger";
5175
+ function showFloatingMenu(view) {
5176
+ view.dispatch(view.state.tr.setMeta(FLOATING_MENU_META, "show"));
5177
+ }
5178
+ function hideFloatingMenu(view) {
5179
+ view.dispatch(view.state.tr.setMeta(FLOATING_MENU_META, "hide"));
5180
+ }
5181
+ var IS_MAC = typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/i.test(navigator.userAgent);
5182
+ function matchShortcut(event, shortcut) {
5183
+ const parts = shortcut.split("-");
5184
+ let keyPart = parts.pop();
5185
+ if (!keyPart) return false;
5186
+ if (keyPart === "Space") keyPart = " ";
5187
+ let needCtrl = false;
5188
+ let needMeta = false;
5189
+ let needAlt = false;
5190
+ let needShift = false;
5191
+ for (const mod of parts) {
5192
+ switch (mod) {
5193
+ case "Mod":
5194
+ case "mod":
5195
+ if (IS_MAC) needMeta = true;
5196
+ else needCtrl = true;
5197
+ break;
5198
+ case "Cmd":
5199
+ case "cmd":
5200
+ case "Meta":
5201
+ case "meta":
5202
+ case "m":
5203
+ needMeta = true;
5204
+ break;
5205
+ case "Ctrl":
5206
+ case "ctrl":
5207
+ case "Control":
5208
+ case "control":
5209
+ case "c":
5210
+ needCtrl = true;
5211
+ break;
5212
+ case "Alt":
5213
+ case "alt":
5214
+ case "a":
5215
+ needAlt = true;
5216
+ break;
5217
+ case "Shift":
5218
+ case "shift":
5219
+ case "s":
5220
+ needShift = true;
5221
+ break;
5222
+ default:
5223
+ return false;
5224
+ }
5225
+ }
5226
+ if (event.altKey !== needAlt) return false;
5227
+ if (event.ctrlKey !== needCtrl) return false;
5228
+ if (event.metaKey !== needMeta) return false;
5229
+ if (keyPart.length > 1 && event.shiftKey !== needShift) return false;
5230
+ return event.key === keyPart || event.key.toLowerCase() === keyPart.toLowerCase();
5231
+ }
5232
+ function createFloatingMenuPlugin(options) {
5233
+ const {
5234
+ pluginKey,
5235
+ editor,
5236
+ element,
5237
+ shouldShow = defaultFloatingMenuShouldShow,
5238
+ offset: offset2 = 0,
5239
+ keymap: keymap4,
5240
+ requireExplicitTrigger = false
5241
+ } = options;
5242
+ const enterShortcuts = keymap4?.enterMenu ?? DEFAULT_ENTER_MENU_SHORTCUTS;
5243
+ if (!element.getAttribute("role")) {
5244
+ element.setAttribute("role", "menu");
5245
+ element.setAttribute("aria-label", "Insert block");
5246
+ }
5247
+ let cleanupFloating = null;
5248
+ let editorEl = null;
5249
+ let clickOutsideHandler = null;
5250
+ let dismissOverlayHandler = null;
5251
+ const isVisible = () => element.hasAttribute("data-show");
5252
+ const updatePosition = (view) => {
5253
+ const { selection } = view.state;
5254
+ const { $from } = selection;
5255
+ const depth = $from.depth;
5256
+ const startPos = $from.start(depth);
5257
+ const domNode = view.nodeDOM(startPos - 1);
5258
+ if (domNode instanceof HTMLElement) {
5259
+ cleanupFloating?.();
5260
+ cleanupFloating = positionFloatingOnce(domNode, element, {
5261
+ placement: "bottom-start",
5262
+ offsetValue: offset2
5263
+ });
5264
+ element.setAttribute("data-show", "");
5265
+ }
5266
+ };
5267
+ const hideMenu = () => {
5268
+ cleanupFloating?.();
5269
+ cleanupFloating = null;
5270
+ element.removeAttribute("data-show");
5271
+ };
5272
+ const clearTriggeredFlag = (view) => {
5273
+ if (!requireExplicitTrigger) return;
5274
+ const triggered = pluginKey.getState(view.state)?.triggered ?? false;
5275
+ if (triggered) view.dispatch(view.state.tr.setMeta(FLOATING_MENU_META, "hide"));
5276
+ };
5277
+ const focusFirstItem = () => {
5278
+ const first = element.querySelector('[data-floating-menu-item]:not([aria-disabled="true"])') ?? element.querySelector('button, [tabindex]:not([tabindex="-1"])');
5279
+ if (!first) return false;
5280
+ first.focus();
5281
+ return true;
5282
+ };
5283
+ hideMenu();
5284
+ const isVisibleNow = (view) => {
5285
+ const wantsShow = shouldShow({ editor, view, state: view.state });
5286
+ if (!requireExplicitTrigger) return wantsShow;
5287
+ const triggered = pluginKey.getState(view.state)?.triggered ?? false;
5288
+ return triggered && wantsShow;
5289
+ };
5290
+ return new Plugin({
5291
+ key: pluginKey,
5292
+ state: {
5293
+ init: () => ({ triggered: false }),
5294
+ apply: (tr, prev) => {
5295
+ const meta = tr.getMeta(FLOATING_MENU_META);
5296
+ if (meta === "show") return { triggered: true };
5297
+ if (meta === "hide") return { triggered: false };
5298
+ return prev;
5299
+ }
5300
+ },
5301
+ props: {
5302
+ // Keyboard entry. handleKeyDown fires before browser defaults; we only
5303
+ // act while the menu is visible.
5304
+ handleKeyDown(_view, event) {
5305
+ if (!isVisible()) return false;
5306
+ for (const shortcut of enterShortcuts) {
5307
+ if (matchShortcut(event, shortcut)) {
5308
+ if (focusFirstItem()) {
5309
+ event.preventDefault();
5310
+ return true;
5311
+ }
5312
+ return false;
5313
+ }
5314
+ }
5315
+ return false;
5316
+ }
5317
+ },
5318
+ view: (editorView) => {
5319
+ editorEl = editorView.dom.closest(".dm-editor");
5320
+ if (editorEl && element.parentElement !== editorEl) {
5321
+ editorEl.appendChild(element);
5322
+ }
5323
+ const dismiss = () => {
5324
+ hideMenu();
5325
+ clearTriggeredFlag(editor.view);
5326
+ };
5327
+ const onFocus = () => {
5328
+ if (isVisibleNow(editor.view)) updatePosition(editor.view);
5329
+ else dismiss();
5330
+ };
5331
+ const onBlur = ({ event }) => {
5332
+ const related = event.relatedTarget;
5333
+ if (related instanceof Node && element.contains(related)) return;
5334
+ dismiss();
5335
+ };
5336
+ clickOutsideHandler = (e) => {
5337
+ if (!isVisible()) return;
5338
+ const target = e.target;
5339
+ if (!(target instanceof Node)) return;
5340
+ if (element.contains(target)) return;
5341
+ if (editor.view.dom.contains(target)) return;
5342
+ dismiss();
5343
+ };
5344
+ document.addEventListener("mousedown", clickOutsideHandler, true);
5345
+ if (editorEl) {
5346
+ dismissOverlayHandler = () => {
5347
+ dismiss();
5348
+ };
5349
+ editorEl.addEventListener("dm:dismiss-overlays", dismissOverlayHandler);
5350
+ }
5351
+ editor.on("focus", onFocus);
5352
+ editor.on("blur", onBlur);
5353
+ return {
5354
+ update: (view) => {
5355
+ if (isVisibleNow(view)) updatePosition(view);
5356
+ else {
5357
+ hideMenu();
5358
+ clearTriggeredFlag(view);
5359
+ }
5360
+ },
5361
+ destroy: () => {
5362
+ hideMenu();
5363
+ editor.off("focus", onFocus);
5364
+ editor.off("blur", onBlur);
5365
+ if (clickOutsideHandler) {
5366
+ document.removeEventListener("mousedown", clickOutsideHandler, true);
5367
+ clickOutsideHandler = null;
5368
+ }
5369
+ if (dismissOverlayHandler && editorEl) {
5370
+ editorEl.removeEventListener("dm:dismiss-overlays", dismissOverlayHandler);
5371
+ dismissOverlayHandler = null;
5372
+ }
5373
+ editorEl = null;
5374
+ }
5375
+ };
5376
+ }
5377
+ });
5378
+ }
5125
5379
 
5126
5380
  // src/icons/phosphor.ts
5127
5381
  var defaultIcons = {
@@ -5695,6 +5949,50 @@ var CodeBlock = Node2.create({
5695
5949
  ];
5696
5950
  }
5697
5951
  });
5952
+ var LIST_ITEM_TYPES4 = /* @__PURE__ */ new Set(["listItem", "taskItem"]);
5953
+ function liftCrossTypeListItem(state, dispatch) {
5954
+ const { $from } = state.selection;
5955
+ if (!state.selection.empty) return false;
5956
+ let itemDepth = -1;
5957
+ for (let d = $from.depth; d > 0; d--) {
5958
+ if (LIST_ITEM_TYPES4.has($from.node(d).type.name)) {
5959
+ itemDepth = d;
5960
+ break;
5961
+ }
5962
+ }
5963
+ if (itemDepth < 4) return false;
5964
+ const item = $from.node(itemDepth);
5965
+ const wrapper = $from.node(itemDepth - 1);
5966
+ const parentItem = $from.node(itemDepth - 2);
5967
+ if (!LIST_ITEM_TYPES4.has(parentItem.type.name)) return false;
5968
+ if (parentItem.type === item.type) return false;
5969
+ const parentWrapper = $from.node(itemDepth - 3);
5970
+ const grandParent = $from.node(itemDepth - 4);
5971
+ const gpIndex = $from.index(itemDepth - 4);
5972
+ if (!grandParent.canReplaceWith(gpIndex + 1, gpIndex + 1, wrapper.type)) return false;
5973
+ if (!dispatch) return true;
5974
+ const onlyChild = wrapper.childCount === 1;
5975
+ const removeFrom = onlyChild ? $from.before(itemDepth - 1) : $from.before(itemDepth);
5976
+ const removeTo = onlyChild ? $from.after(itemDepth - 1) : $from.after(itemDepth);
5977
+ const parentIsLast = $from.index(itemDepth - 3) === parentWrapper.childCount - 1;
5978
+ const tr = state.tr;
5979
+ tr.delete(removeFrom, removeTo);
5980
+ const newList = wrapper.type.create(wrapper.attrs, item);
5981
+ if (parentIsLast) {
5982
+ const insertAt = tr.mapping.map($from.after(itemDepth - 3));
5983
+ tr.insert(insertAt, newList);
5984
+ tr.setSelection(Selection.near(tr.doc.resolve(insertAt + 2)));
5985
+ } else {
5986
+ const splitAt = tr.mapping.map($from.after(itemDepth - 2));
5987
+ tr.split(splitAt, 1);
5988
+ tr.insert(splitAt + 1, newList);
5989
+ tr.setSelection(Selection.near(tr.doc.resolve(splitAt + 2)));
5990
+ }
5991
+ dispatch(tr.scrollIntoView());
5992
+ return true;
5993
+ }
5994
+
5995
+ // src/extensions/ListKeymap.ts
5698
5996
  var LIST_GROUP_TYPES = /* @__PURE__ */ new Set(["bulletList", "orderedList", "taskList"]);
5699
5997
  function getListItemContext(editor, listItemName) {
5700
5998
  const { state, view } = editor;
@@ -5736,6 +6034,7 @@ var ListKeymap = Extension.create({
5736
6034
  if (!this.editor) return false;
5737
6035
  const ctx = getListItemContext(this.editor, this.options.listItem);
5738
6036
  if (!ctx) return false;
6037
+ if (liftCrossTypeListItem(ctx.state, ctx.view.dispatch)) return true;
5739
6038
  return liftListItem(ctx.listItemType)(ctx.state, ctx.view.dispatch);
5740
6039
  },
5741
6040
  // Backspace at start of list item to lift
@@ -5789,6 +6088,7 @@ var ListKeymap = Extension.create({
5789
6088
  if (firstChild?.isTextblock) {
5790
6089
  const posInListItem = $from.pos - $from.start(listItemDepth);
5791
6090
  if (posInListItem <= 1) {
6091
+ if (liftCrossTypeListItem(state, view.dispatch)) return true;
5792
6092
  return liftListItem(listItemType)(state, view.dispatch);
5793
6093
  }
5794
6094
  }
@@ -5835,7 +6135,22 @@ var ListItem = Node2.create({
5835
6135
  if (splitBlock(state, view.dispatch)) return true;
5836
6136
  }
5837
6137
  }
5838
- if (splitListItem(this.nodeType)(state, view.dispatch)) return true;
6138
+ const item = $from.node(-1);
6139
+ const hasChildren = item.childCount > 1;
6140
+ const atEnd = $from.parentOffset === $from.parent.content.size;
6141
+ const labelEmpty = $from.parent.content.size === 0;
6142
+ if (!ctx?.isInChildrenZone && hasChildren && atEnd && !labelEmpty) {
6143
+ const newItem = this.nodeType.createAndFill();
6144
+ if (newItem) {
6145
+ const insertAt = $from.after($from.depth - 1);
6146
+ const tr = state.tr.insert(insertAt, newItem);
6147
+ tr.setSelection(Selection.near(tr.doc.resolve(insertAt + 2)));
6148
+ view.dispatch(tr.scrollIntoView());
6149
+ return true;
6150
+ }
6151
+ }
6152
+ const skipSplit = !ctx?.isInChildrenZone && labelEmpty && hasChildren;
6153
+ if (!skipSplit && splitListItem(this.nodeType)(state, view.dispatch)) return true;
5839
6154
  const listDepth = $from.depth - 2;
5840
6155
  const taskItemType = state.schema.nodes["taskItem"];
5841
6156
  if ($from.parent.content.size === 0 && listDepth > 0 && taskItemType && $from.node(listDepth - 1).type === taskItemType) {
@@ -5844,9 +6159,9 @@ var ListItem = Node2.create({
5844
6159
  tr.delete($from.before(delDepth), $from.after(delDepth));
5845
6160
  const taskItemDepth = listDepth - 1;
5846
6161
  const end = tr.mapping.map($from.after(taskItemDepth));
5847
- const item = taskItemType.createAndFill();
5848
- if (item) {
5849
- tr.insert(end, item);
6162
+ const item2 = taskItemType.createAndFill();
6163
+ if (item2) {
6164
+ tr.insert(end, item2);
5850
6165
  tr.setSelection(Selection.near(tr.doc.resolve(end + 2)));
5851
6166
  view.dispatch(tr.scrollIntoView());
5852
6167
  return true;
@@ -5892,6 +6207,11 @@ var BulletList = Node2.create({
5892
6207
  return {
5893
6208
  toggleBulletList: () => ({ commands }) => {
5894
6209
  return commands.toggleList(name, options.itemTypeName);
6210
+ },
6211
+ // Notion "turn into" (slash menu / block menu): convert only the cursor's
6212
+ // item and split the run, instead of retyping the whole list.
6213
+ turnIntoBulletList: () => ({ commands }) => {
6214
+ return commands.toggleList(name, options.itemTypeName, void 0, { perItem: true });
5895
6215
  }
5896
6216
  };
5897
6217
  },
@@ -5921,7 +6241,7 @@ var BulletList = Node2.create({
5921
6241
  priority: 200,
5922
6242
  keywords: ["bullet", "list", "unordered", "ul"],
5923
6243
  shortcut: "- ",
5924
- command: "toggleBulletList",
6244
+ command: "turnIntoBulletList",
5925
6245
  // Don't offer "Bulleted list" while cursor is already inside one,
5926
6246
  // otherwise picking it lifts the user out of the list.
5927
6247
  hideWhenInside: ["bulletList"]
@@ -5970,16 +6290,20 @@ var OrderedList = Node2.create({
5970
6290
  return {
5971
6291
  start: {
5972
6292
  default: 1,
6293
+ // Clamp to a finite integer >= 1. Without this, a malformed
6294
+ // `start="abc"` parses to NaN; `JSON.stringify(NaN)` is `null`, so
6295
+ // getJSON() serializes `start: null` and a save/load cycle
6296
+ // permanently dirties the document (reloads as `<ol start="null">`).
5973
6297
  parseHTML: (element) => {
5974
- const start = element.getAttribute("start");
5975
- return start ? parseInt(start, 10) : 1;
6298
+ const n = parseInt(element.getAttribute("start") ?? "", 10);
6299
+ return Number.isFinite(n) && n >= 1 ? Math.floor(n) : 1;
5976
6300
  },
5977
6301
  renderHTML: (attributes) => {
5978
6302
  const start = attributes["start"];
5979
- if (start === 1) {
6303
+ if (!Number.isFinite(start) || start <= 1) {
5980
6304
  return {};
5981
6305
  }
5982
- return { start: String(start) };
6306
+ return { start: String(Math.floor(start)) };
5983
6307
  }
5984
6308
  }
5985
6309
  };
@@ -5995,6 +6319,11 @@ var OrderedList = Node2.create({
5995
6319
  return {
5996
6320
  toggleOrderedList: () => ({ commands }) => {
5997
6321
  return commands.toggleList(name, options.itemTypeName);
6322
+ },
6323
+ // Notion "turn into" (slash menu / block menu): convert only the cursor's
6324
+ // item and split the run, instead of retyping the whole list.
6325
+ turnIntoOrderedList: () => ({ commands }) => {
6326
+ return commands.toggleList(name, options.itemTypeName, void 0, { perItem: true });
5998
6327
  }
5999
6328
  };
6000
6329
  },
@@ -6024,7 +6353,7 @@ var OrderedList = Node2.create({
6024
6353
  priority: 190,
6025
6354
  keywords: ["ordered", "numbered", "list", "ol", "1."],
6026
6355
  shortcut: "1. ",
6027
- command: "toggleOrderedList",
6356
+ command: "turnIntoOrderedList",
6028
6357
  hideWhenInside: ["orderedList"]
6029
6358
  }
6030
6359
  ];
@@ -6053,8 +6382,8 @@ var OrderedList = Node2.create({
6053
6382
  guard: notInsideList,
6054
6383
  joinForward: true,
6055
6384
  getAttributes: (match) => {
6056
- const num = match[1];
6057
- return { start: num ? parseInt(num, 10) : 1 };
6385
+ const n = parseInt(match[1] ?? "", 10);
6386
+ return { start: Number.isFinite(n) && n >= 1 ? Math.floor(n) : 1 };
6058
6387
  }
6059
6388
  })
6060
6389
  ];
@@ -6326,7 +6655,10 @@ var TaskItem = Node2.create({
6326
6655
  keepOnSplit: false,
6327
6656
  parseHTML: (element) => {
6328
6657
  const dataChecked = element.getAttribute("data-checked");
6329
- return dataChecked === "true" || dataChecked === "";
6658
+ if (dataChecked !== null) {
6659
+ return dataChecked.toLowerCase() === "true" || dataChecked === "";
6660
+ }
6661
+ return element.querySelector('input[type="checkbox"]')?.hasAttribute("checked") ?? false;
6330
6662
  },
6331
6663
  renderHTML: (attributes) => ({
6332
6664
  "data-checked": attributes["checked"] ? "true" : "false"
@@ -6339,6 +6671,14 @@ var TaskItem = Node2.create({
6339
6671
  {
6340
6672
  tag: `li[data-type="${this.name}"]`,
6341
6673
  priority: 51
6674
+ },
6675
+ // GFM / markdown task lists: `<li class="task-list-item">` carries no
6676
+ // data-type. Priority 51 keeps it ahead of the generic `li` (listItem,
6677
+ // 50); class-scoped so it never swallows ordinary bullet items. The
6678
+ // `checked` attribute is derived from the descendant `<input>` above.
6679
+ {
6680
+ tag: "li.task-list-item",
6681
+ priority: 51
6342
6682
  }
6343
6683
  ];
6344
6684
  },
@@ -6413,7 +6753,33 @@ var TaskItem = Node2.create({
6413
6753
  if (splitBlock(state, view.dispatch)) return true;
6414
6754
  }
6415
6755
  }
6416
- if (splitListItem(this.nodeType, { checked: false })(state, view.dispatch)) return true;
6756
+ if (!ctx?.isInChildrenZone && $from.node(-1).attrs["checked"] && $from.parentOffset > 0 && $from.parentOffset < $from.parent.content.size) {
6757
+ const tr = state.tr;
6758
+ const types = [
6759
+ { type: this.nodeType, attrs: { ...$from.node(-1).attrs, checked: false } },
6760
+ { type: $from.parent.type }
6761
+ ];
6762
+ if (canSplit(tr.doc, $from.pos, 2, types)) {
6763
+ view.dispatch(tr.split($from.pos, 2, types).scrollIntoView());
6764
+ return true;
6765
+ }
6766
+ }
6767
+ const item = $from.node(-1);
6768
+ const hasChildren = item.childCount > 1;
6769
+ const labelEmpty = $from.parent.content.size === 0;
6770
+ const atEnd = $from.parentOffset === $from.parent.content.size;
6771
+ if (!ctx?.isInChildrenZone && hasChildren && atEnd && !labelEmpty) {
6772
+ const newItem = this.nodeType.createAndFill({ checked: false });
6773
+ if (newItem) {
6774
+ const insertAt = $from.after($from.depth - 1);
6775
+ const tr = state.tr.insert(insertAt, newItem);
6776
+ tr.setSelection(Selection.near(tr.doc.resolve(insertAt + 2)));
6777
+ view.dispatch(tr.scrollIntoView());
6778
+ return true;
6779
+ }
6780
+ }
6781
+ const skipSplit = !ctx?.isInChildrenZone && labelEmpty && hasChildren;
6782
+ if (!skipSplit && splitListItem(this.nodeType, { checked: false })(state, view.dispatch)) return true;
6417
6783
  if ($from.parent.content.size === 0) {
6418
6784
  const listItemType = state.schema.nodes["listItem"];
6419
6785
  if (listItemType) {
@@ -6460,9 +6826,11 @@ var TaskItem = Node2.create({
6460
6826
  },
6461
6827
  "Shift-Tab": () => {
6462
6828
  if (!this.editor || !this.nodeType) return false;
6463
- const { $from } = this.editor.state.selection;
6829
+ const { state, view } = this.editor;
6830
+ const { $from } = state.selection;
6464
6831
  if ($from.depth < 1 || $from.node(-1).type !== this.nodeType) return false;
6465
- return liftListItem(this.nodeType)(this.editor.state, this.editor.view.dispatch);
6832
+ if (liftCrossTypeListItem(state, view.dispatch)) return true;
6833
+ return liftListItem(this.nodeType)(state, view.dispatch);
6466
6834
  },
6467
6835
  Backspace: () => {
6468
6836
  if (!this.editor || !this.nodeType) return false;
@@ -6484,6 +6852,7 @@ var TaskItem = Node2.create({
6484
6852
  }
6485
6853
  if (taskItemDepth === -1) return false;
6486
6854
  if ($from.index(taskItemDepth) !== 0) return false;
6855
+ if (liftCrossTypeListItem(state, view.dispatch)) return true;
6487
6856
  return liftListItem(this.nodeType)(state, view.dispatch);
6488
6857
  },
6489
6858
  "Mod-Enter": () => {
@@ -6510,6 +6879,13 @@ var TaskList = Node2.create({
6510
6879
  tag: `ul[data-type="${this.name}"]`,
6511
6880
  priority: 51
6512
6881
  // Higher priority than regular bulletList
6882
+ },
6883
+ // GFM / markdown task lists: `<ul class="contains-task-list">`. Priority
6884
+ // 51 keeps it ahead of the generic `ul` (bulletList, 50); class-scoped so
6885
+ // it only matches real GFM task-list containers, not ordinary bullets.
6886
+ {
6887
+ tag: "ul.contains-task-list",
6888
+ priority: 51
6513
6889
  }
6514
6890
  ];
6515
6891
  },
@@ -6529,6 +6905,11 @@ var TaskList = Node2.create({
6529
6905
  return {
6530
6906
  toggleTaskList: () => ({ commands }) => {
6531
6907
  return commands.toggleList(name, options.itemTypeName);
6908
+ },
6909
+ // Notion "turn into" (slash menu / block menu): convert only the cursor's
6910
+ // item and split the run, instead of retyping the whole list.
6911
+ turnIntoTaskList: () => ({ commands }) => {
6912
+ return commands.toggleList(name, options.itemTypeName, void 0, { perItem: true });
6532
6913
  }
6533
6914
  };
6534
6915
  },
@@ -6566,7 +6947,7 @@ var TaskList = Node2.create({
6566
6947
  priority: 180,
6567
6948
  keywords: ["todo", "task", "checkbox", "check"],
6568
6949
  shortcut: "[ ] ",
6569
- command: "toggleTaskList",
6950
+ command: "turnIntoTaskList",
6570
6951
  hideWhenInside: ["taskList"]
6571
6952
  }
6572
6953
  ];
@@ -7732,9 +8113,8 @@ var Placeholder = Extension.create({
7732
8113
  props: {
7733
8114
  decorations: ({ doc, selection }) => {
7734
8115
  const editor = this.editor;
7735
- if (!editor?.view) return DecorationSet.empty;
7736
- const isEditable = editor.view.editable;
7737
- if (!isEditable && this.options.showOnlyWhenEditable) {
8116
+ if (!editor) return DecorationSet.empty;
8117
+ if (!editor.isEditable && this.options.showOnlyWhenEditable) {
7738
8118
  return DecorationSet.empty;
7739
8119
  }
7740
8120
  const isDocEmpty = doc.childCount === 1 && doc.firstChild?.isTextblock && doc.firstChild.content.size === 0;
@@ -7766,12 +8146,12 @@ var Placeholder = Extension.create({
7766
8146
  ];
7767
8147
  }
7768
8148
  });
7769
- var LIST_ITEM_TYPES4 = /* @__PURE__ */ new Set(["listItem", "taskItem"]);
7770
- var LIST_WRAPPER_TYPES2 = /* @__PURE__ */ new Set(["bulletList", "orderedList", "taskList"]);
8149
+ var LIST_ITEM_TYPES5 = /* @__PURE__ */ new Set(["listItem", "taskItem"]);
8150
+ var LIST_WRAPPER_TYPES3 = /* @__PURE__ */ new Set(["bulletList", "orderedList", "taskList"]);
7771
8151
  function isCursorInsideListItem(state) {
7772
8152
  const { $from } = state.selection;
7773
8153
  for (let d = $from.depth; d > 0; d--) {
7774
- if (LIST_ITEM_TYPES4.has($from.node(d).type.name)) return true;
8154
+ if (LIST_ITEM_TYPES5.has($from.node(d).type.name)) return true;
7775
8155
  }
7776
8156
  return false;
7777
8157
  }
@@ -7802,7 +8182,7 @@ function indentBlockAsListChild(state, dispatch) {
7802
8182
  }
7803
8183
  if (blockIndex === 0) return false;
7804
8184
  const prevSibling = state.doc.child(blockIndex - 1);
7805
- if (!LIST_WRAPPER_TYPES2.has(prevSibling.type.name)) return false;
8185
+ if (!LIST_WRAPPER_TYPES3.has(prevSibling.type.name)) return false;
7806
8186
  let wrapperPos = 0;
7807
8187
  for (let i = 0; i < blockIndex - 1; i++) {
7808
8188
  wrapperPos += state.doc.child(i).nodeSize;
@@ -7826,7 +8206,7 @@ function outdentBlockFromListItem(state, dispatch) {
7826
8206
  const { $from } = selection;
7827
8207
  let listItemDepth = -1;
7828
8208
  for (let d = $from.depth; d > 0; d--) {
7829
- if (LIST_ITEM_TYPES4.has($from.node(d).type.name)) {
8209
+ if (LIST_ITEM_TYPES5.has($from.node(d).type.name)) {
7830
8210
  listItemDepth = d;
7831
8211
  break;
7832
8212
  }
@@ -7842,7 +8222,7 @@ function outdentBlockFromListItem(state, dispatch) {
7842
8222
  if (wrapperDepth < 0) return false;
7843
8223
  const liIndexInWrapper = $from.index(wrapperDepth);
7844
8224
  const wrapper = $from.node(wrapperDepth);
7845
- if (liIndexInWrapper !== wrapper.childCount - 1) return false;
8225
+ const isLastItem = liIndexInWrapper === wrapper.childCount - 1;
7846
8226
  if (wrapperDepth - 1 < 0) return false;
7847
8227
  const wrapperParent = $from.node(wrapperDepth - 1);
7848
8228
  const wrapperIndexInParent = $from.index(wrapperDepth - 1);
@@ -7857,13 +8237,19 @@ function outdentBlockFromListItem(state, dispatch) {
7857
8237
  if (!dispatch) return true;
7858
8238
  const blockStart = $from.before(blockDepth);
7859
8239
  const blockEnd = $from.after(blockDepth);
7860
- const wrapperEnd = $from.after(wrapperDepth);
7861
- const tr = state.tr;
7862
8240
  const blockSize = blockEnd - blockStart;
8241
+ const tr = state.tr;
7863
8242
  tr.delete(blockStart, blockEnd);
7864
- const insertAt = wrapperEnd - blockSize;
7865
- tr.insert(insertAt, blockNode);
7866
- tr.setSelection(Selection.near(tr.doc.resolve(insertAt + 1)));
8243
+ if (isLastItem) {
8244
+ const insertAt = $from.after(wrapperDepth) - blockSize;
8245
+ tr.insert(insertAt, blockNode);
8246
+ tr.setSelection(Selection.near(tr.doc.resolve(insertAt + 1)));
8247
+ } else {
8248
+ const splitPos = $from.after(listItemDepth) - blockSize;
8249
+ tr.split(splitPos, 1);
8250
+ tr.insert(splitPos + 1, blockNode);
8251
+ tr.setSelection(Selection.near(tr.doc.resolve(splitPos + 2)));
8252
+ }
7867
8253
  dispatch(tr.scrollIntoView());
7868
8254
  return true;
7869
8255
  }
@@ -8600,7 +8986,7 @@ var BlockColor = Extension.create({
8600
8986
  };
8601
8987
  }
8602
8988
  });
8603
- var Selection4 = Extension.create({
8989
+ var Selection5 = Extension.create({
8604
8990
  name: "selection",
8605
8991
  addStorage() {
8606
8992
  return {
@@ -10099,6 +10485,6 @@ var StarterKit = Extension.create({
10099
10485
  // src/index.ts
10100
10486
  var VERSION = "0.1.0";
10101
10487
 
10102
- export { BaseKeymap, BlockColor, Blockquote, Bold, BubbleMenu, BulletList, CanChecker, ChainBuilder, CharacterCount, ClearFormatting, Code, CodeBlock, CommandManager, DEFAULT_BLOCK_COLORS, DEFAULT_BLOCK_COLOR_TYPES, DEFAULT_HIGHLIGHT_COLORS, DEFAULT_NOTION_COLOR_PALETTE, DEFAULT_TEXT_COLORS, Document, Dropcursor, Editor, EventEmitter, Extension, ExtensionManager, FLOATING_MENU_NO_FOCUS, FloatingMenuController, Focus, FontFamily, FontSize, Gapcursor, HardBreak, Heading, Highlight, History, HorizontalRule, InvisibleChars, Italic, LIST_ITEM_TYPE_NAMES, LineHeight, Link, LinkPopover, ListIndent, ListItem, ListKeymap, Mark, Node2 as Node, NotionColorPicker, OrderedList, Paragraph, Placeholder, Selection4 as Selection, SelectionDecoration, StarterKit, Strike, Subscript, Superscript, TaskItem, TaskList, Text, TextAlign, TextColor, TextStyle, ToolbarController, TrailingNode, Typography, Underline, UniqueID, VERSION, applyInlineStyles, autolinkPlugin, autolinkPluginKey, blur, bubbleMenuPluginKey, buildCommandProps, builtInCommands, callOrReturn, characterCountPluginKey, clearContent, copyThemeClass, createAccumulatingDispatch, createBubbleMenuPlugin, createCanChecker, createChainBuilder, createDocument, defaultBlockAt, defaultBubbleContexts, defaultIcons, deleteSelection, findChildren, findListItemAncestorDepth, findParentNode, focus, focusPluginKey, generateHTML, generateJSON, generateText, getListItemCursorContext, getMarkRange, groupFloatingMenuItems, indentBlockAsListChild, inlineStyles, insertAsListItemChild, insertChildrenZoneSibling, insertContent, insertText, invisibleCharsPluginKey, isDocumentEmpty, isInListItemLabel, isInsideListItem, isNodeEmpty, isValidUrl, lift, liftCurrentListItem, liftEmptyChildrenZoneParagraph, linkClickPlugin, linkClickPluginKey, linkExitPlugin, linkExitPluginKey, linkPastePlugin, linkPastePluginKey, markInputRule, markInputRulePatterns, nodeInputRule, outdentBlockFromListItem, placeholderPluginKey, positionFloating, positionFloatingOnce, resetAttributes, selectAll, selectNodeBackward, selectionDecorationPluginKey, setBlockType, setContent, setMark, splitListForInsert, stripInlineColorConflicts, textInputRule, textblockTypeInputRule, toggleBlockType, toggleList, toggleMark, toggleWrap, uniqueIDPluginKey, unsetAllMarks, unsetMark, updateAttributes, wrapIn, wrappingInputRule, writeToClipboard };
10488
+ export { BaseKeymap, BlockColor, Blockquote, Bold, BubbleMenu, BulletList, CanChecker, ChainBuilder, CharacterCount, ClearFormatting, Code, CodeBlock, CommandManager, DEFAULT_BLOCK_COLORS, DEFAULT_BLOCK_COLOR_TYPES, DEFAULT_HIGHLIGHT_COLORS, DEFAULT_NOTION_COLOR_PALETTE, DEFAULT_TEXT_COLORS, Document, Dropcursor, Editor, EventEmitter, Extension, ExtensionManager, FLOATING_MENU_META, FLOATING_MENU_NO_FOCUS, FloatingMenuController, Focus, FontFamily, FontSize, Gapcursor, HardBreak, Heading, Highlight, History, HorizontalRule, InvisibleChars, Italic, LIST_ITEM_TYPE_NAMES, LineHeight, Link, LinkPopover, ListIndent, ListItem, ListKeymap, Mark, Node2 as Node, NotionColorPicker, OrderedList, Paragraph, Placeholder, Selection5 as Selection, SelectionDecoration, StarterKit, Strike, Subscript, Superscript, TaskItem, TaskList, Text, TextAlign, TextColor, TextStyle, ToolbarController, TrailingNode, Typography, Underline, UniqueID, VERSION, applyInlineStyles, autolinkPlugin, autolinkPluginKey, blur, bubbleMenuPluginKey, buildCommandProps, builtInCommands, callOrReturn, characterCountPluginKey, clearContent, copyThemeClass, createAccumulatingDispatch, createBubbleMenuPlugin, createCanChecker, createChainBuilder, createDocument, createFloatingMenuPlugin, defaultBlockAt, defaultBubbleContexts, defaultFloatingMenuShouldShow, defaultIcons, deleteSelection, findChildren, findListItemAncestorDepth, findParentNode, floatingMenuPluginKey, focus, focusPluginKey, generateHTML, generateJSON, generateText, getListItemCursorContext, getMarkRange, groupFloatingMenuItems, hideFloatingMenu, indentBlockAsListChild, inlineStyles, insertAsListItemChild, insertChildrenZoneSibling, insertContent, insertText, invisibleCharsPluginKey, isDocumentEmpty, isInListItemLabel, isInsideListItem, isNodeEmpty, isValidUrl, lift, liftCurrentListItem, liftEmptyChildrenZoneParagraph, linkClickPlugin, linkClickPluginKey, linkExitPlugin, linkExitPluginKey, linkPastePlugin, linkPastePluginKey, markInputRule, markInputRulePatterns, nodeInputRule, outdentBlockFromListItem, placeholderPluginKey, positionFloating, positionFloatingOnce, resetAttributes, selectAll, selectNodeBackward, selectionDecorationPluginKey, setBlockType, setContent, setMark, showFloatingMenu, splitListForInsert, stripInlineColorConflicts, textInputRule, textblockTypeInputRule, toggleBlockType, toggleList, toggleMark, toggleWrap, uniqueIDPluginKey, unsetAllMarks, unsetMark, updateAttributes, wrapIn, wrappingInputRule, writeToClipboard };
10103
10489
  //# sourceMappingURL=index.js.map
10104
10490
  //# sourceMappingURL=index.js.map