@slash-editor/core 0.0.6 → 0.0.8

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.d.mts CHANGED
@@ -1,7 +1,9 @@
1
1
  import { Editor, Extension, Extensions, Mark, Node, Range } from "@tiptap/core";
2
2
  import { Node as Node$1, NodeType } from "@tiptap/pm/model";
3
3
  import { EditorState, PluginKey } from "@tiptap/pm/state";
4
+ import { DetailsOptions } from "@tiptap/extension-details";
4
5
  import { SuggestionProps } from "@tiptap/suggestion";
6
+ import { BlockquoteOptions } from "@tiptap/extension-blockquote";
5
7
  import { TableKit, TableKitOptions } from "@tiptap/extension-table";
6
8
  import { Doc } from "yjs";
7
9
  //#region src/slash-items.d.ts
@@ -22,6 +24,12 @@ interface SlashItem {
22
24
  aliases?: string[];
23
25
  /** Lower-weight search terms that are never displayed. */
24
26
  keywords?: string[];
27
+ /**
28
+ * Markdown input-rule shorthand shown as a hint in the menu (`#`, `---`).
29
+ * Display only — ranking never reads it, and it must mirror a rule the
30
+ * editor actually registers.
31
+ */
32
+ shortcut?: string;
25
33
  /** Icon key resolved by the UI layer; the core ships no components. */
26
34
  icon?: string;
27
35
  /** Hides the item when the current editor cannot run it. */
@@ -913,6 +921,54 @@ declare const Mention: Node<MentionOptions, MentionStorage>;
913
921
  /** Configures the mention node. `items` is required — there is no default provider. */
914
922
  declare function mention(options: Partial<MentionOptions> & Pick<MentionOptions, "items">): Node<MentionOptions, MentionStorage>;
915
923
  //#endregion
924
+ //#region src/placeholder.d.ts
925
+ /**
926
+ * Slot a placeholder string is looked up under. Slots are resolved from the
927
+ * empty node *and its parent*: the empty node inside a list item, a task item,
928
+ * a quote or a callout is a plain `paragraph`, so node type alone cannot tell
929
+ * those cases apart.
930
+ */
931
+ type PlaceholderKey = "paragraph" | "heading1" | "heading2" | "heading3" | "listItem" | "taskItem" | "blockquote" | "callout" | "details" | "toggleHeading1" | "toggleHeading2" | "toggleHeading3";
932
+ interface PlaceholderContext {
933
+ editor: Editor;
934
+ /** The empty block holding the caret. */
935
+ node: Node$1;
936
+ /** Its parent, which distinguishes a bare paragraph from a list/quote child. */
937
+ parent: Node$1 | null;
938
+ /** Position directly before `node`. */
939
+ pos: number;
940
+ }
941
+ interface PlaceholderOptions {
942
+ /** Per-slot overrides; unset slots keep the default string. */
943
+ text?: Partial<Record<PlaceholderKey, string>>;
944
+ /**
945
+ * Full override. Wins over `text`; returning `null` suppresses the
946
+ * placeholder for that block.
947
+ */
948
+ resolve?: (context: PlaceholderContext) => string | null;
949
+ }
950
+ declare const defaultPlaceholderText: Record<PlaceholderKey, string>;
951
+ /**
952
+ * Maps an empty block to its placeholder slot, or `null` when it should stay
953
+ * blank. Code blocks are excluded: their content is literal, and a hint would
954
+ * read as source.
955
+ */
956
+ declare function placeholderKeyFor(node: Node$1, parent: Node$1 | null): PlaceholderKey | null;
957
+ declare const placeholderPluginKey: PluginKey<any>;
958
+ /**
959
+ * Empty-block hints, Notion style: only the block holding the caret shows one.
960
+ *
961
+ * Renders `data-placeholder` and no class names — the UI layer styles
962
+ * `[data-placeholder]::before`, matching how every other core node is styled.
963
+ *
964
+ * The decoration is derived from the state it is drawn against rather than
965
+ * from `editor.state`: during a transaction those are different documents, and
966
+ * resolving a parent in the stale one mislabels every nested block.
967
+ */
968
+ declare const Placeholder: Extension<PlaceholderOptions, any>;
969
+ /** Configures empty-block placeholders. */
970
+ declare function placeholder(options?: PlaceholderOptions): Extension<PlaceholderOptions, any>;
971
+ //#endregion
916
972
  //#region src/slash-command.d.ts
917
973
  interface SlashMenuState {
918
974
  open: boolean;
@@ -947,6 +1003,12 @@ interface SlashCommandOptions {
947
1003
  char: string;
948
1004
  /** Item registry, or a resolver called with the live editor. */
949
1005
  items: SlashItem[] | ((editor: Editor) => SlashItem[]);
1006
+ /**
1007
+ * Inline hint rendered next to the trigger character while the query is
1008
+ * still empty, the way Notion prompts after `/`. Set to `""` to drop it.
1009
+ * The UI layer renders it from `data-decoration-content`.
1010
+ */
1011
+ hint: string;
950
1012
  /**
951
1013
  * Called when an item's `run` throws. The failing transaction is never
952
1014
  * applied, so the document keeps the state it had before the item ran.
@@ -976,6 +1038,40 @@ declare function slashCommand(options?: Partial<SlashCommandOptions>): Extension
976
1038
  */
977
1039
  declare function table(options?: Partial<TableKitOptions>): import("@tiptap/core").Extension<TableKitOptions, any>;
978
1040
  //#endregion
1041
+ //#region src/toggle.d.ts
1042
+ type ToggleOptions = DetailsOptions;
1043
+ /** Heading level a toggle renders its title at; `0` is a plain toggle list. */
1044
+ type ToggleLevel = 0 | 1 | 2 | 3;
1045
+ declare module "@tiptap/core" {
1046
+ interface Commands<ReturnType> {
1047
+ toggle: {
1048
+ /**
1049
+ * Turns the current block into a toggle whose title is that block's
1050
+ * text, or re-levels the toggle already holding the selection.
1051
+ */
1052
+ setToggle: (level?: ToggleLevel) => ReturnType;
1053
+ /** Sets the heading level of the toggle holding the selection. */
1054
+ setToggleLevel: (level: ToggleLevel) => ReturnType;
1055
+ };
1056
+ }
1057
+ }
1058
+ /** `>` + space: the toggle shorthand. */
1059
+ declare const toggleInputRegex: RegExp;
1060
+ /** `#`…`###` + space inside a toggle title: the toggle-heading shorthand. */
1061
+ declare const toggleHeadingInputRegex: RegExp;
1062
+ /**
1063
+ * Tiptap's `Details` with a `level` attribute, the `>` shorthand, and the
1064
+ * toggle-heading shorthands.
1065
+ *
1066
+ * `level` lives on the container rather than the title because the node view
1067
+ * renders the disclosure button as a *sibling* of the summary: with
1068
+ * `data-level` on the wrapper, one selector sizes both, and the marker stays
1069
+ * centred on a title of any size.
1070
+ */
1071
+ declare const Toggle: import("@tiptap/core").Node<DetailsOptions, any>;
1072
+ /** Configures the toggle (details) node. */
1073
+ declare function toggle(options?: Partial<ToggleOptions>): import("@tiptap/core").Node<DetailsOptions, any>;
1074
+ //#endregion
979
1075
  //#region src/video.d.ts
980
1076
  interface VideoOptions {
981
1077
  HTMLAttributes: Record<string, unknown>;
@@ -1045,6 +1141,13 @@ interface BlockKitOptions {
1045
1141
  * @default { char: "/", items: defaultSlashItems }
1046
1142
  */
1047
1143
  slash?: Partial<SlashCommandOptions> | false;
1144
+ /**
1145
+ * Empty-block placeholder hints, or `false` to render none. Only the block
1146
+ * holding the caret shows one.
1147
+ *
1148
+ * @default {}
1149
+ */
1150
+ placeholder?: PlaceholderOptions | false;
1048
1151
  /**
1049
1152
  * Stable per-block id configuration, or `false` to opt out. Drag
1050
1153
  * targeting and the future comment/collaboration surfaces depend on it.
@@ -1145,6 +1248,14 @@ interface BlockKitOptions {
1145
1248
  * @default {}
1146
1249
  */
1147
1250
  comment?: Partial<CommentOptions> | false;
1251
+ /**
1252
+ * Toggle (details) node configuration. Options only — the node itself is
1253
+ * unconditional, like blockquote. This is the seam a UI layer uses to
1254
+ * render its own disclosure marker via `renderToggleButton`.
1255
+ *
1256
+ * @default { persist: true }
1257
+ */
1258
+ toggle?: Partial<ToggleOptions>;
1148
1259
  }
1149
1260
  /**
1150
1261
  * The baseline block schema: document, text, paragraph, headings, lists,
@@ -1188,4 +1299,20 @@ declare const Callout: Node<CalloutOptions, any>;
1188
1299
  /** Configures the callout node. */
1189
1300
  declare function callout(options?: Partial<CalloutOptions>): Node<CalloutOptions, any>;
1190
1301
  //#endregion
1191
- export { type AiActionStatus, AiBlock, type AiBlockOptions, type AiBlockStorage, type AiKitOptions, type AiRequest, type AiSlashAction, BLOCK_ID_REMOTE_META, BlockDrag, type BlockDragOptions, type BlockDragState, type BlockDragStorage, BlockId, type BlockIdOptions, type BlockKitOptions, type BlockLocation, type BlockRect, type BlockTarget, BubbleToolbar, type BubbleToolbarItem, type BubbleToolbarOptions, type BubbleToolbarState, type BubbleToolbarStorage, Callout, type CalloutOptions, type CollaborationOptions, type CollaborationProvider, type CollaborationUser, Column, type ColumnOptions, Columns, type ColumnsOptions, Comment, type CommentMessage, type CommentOptions, type CommentState, type CommentStorage, type CommentThread, type CommentThreadStore, type DropMode, type DropTarget, Embed, type EmbedMode, type EmbedOptions, File$1 as File, type FileOptions, type FileStorage, type HeadingLevel, Image, type ImageOptions, type ImageStorage, LinkEditor, type LinkEditorOptions, type LinkEditorState, type LinkEditorStorage, Mention, type MentionItem, type MentionOptions, type MentionState, type MentionStorage, PendingAiRegistry, PendingUploadRegistry, type RetryUploadOptions, type RunAiActionOptions, type RunUploadOptions, type SetEmbedOptions, type SetFileFromFile, type SetFileFromSrc, type SetFileOptions, type SetImageFromFile, type SetImageFromSrc, type SetImageOptions, type SetVideoFromFile, type SetVideoFromSrc, type SetVideoOptions, SlashCommand, type SlashCommandOptions, type SlashCommandStorage, type SlashContext, type SlashItem, type SlashMenuState, type StreamAdapter, type StreamContext, TableKit, type TableKitOptions, type UploadAdapter, type UploadContext, type UploadResult, type UploadStatus, Video, type VideoOptions, type VideoStorage, activeThreadIds, aiBlock, blockDrag, blockDragPluginKey, blockId, blockIdPluginKey, bubbleToolbar, callout, canAppendChild, canOpenLinkEditor, collaboration, column, columns, comment, createAiSlashItems, createBlockKit, defaultAiSlashActions, defaultBubbleToolbarItems, defaultSlashItems, embed, file, filterBubbleToolbarItems, filterSlashItems, findNodeById, image, linkEditor, mention, mentionPluginKey, resolveDropTarget, retryUpload, runUpload, slashCommand, slashCommandPluginKey, table, video };
1302
+ //#region src/quote.d.ts
1303
+ type QuoteOptions = BlockquoteOptions;
1304
+ /** `"` + space at the start of a block, Notion style. */
1305
+ declare const quoteInputRegex: RegExp;
1306
+ /**
1307
+ * Blockquote with its markdown shorthand moved from `>` to `"`. The node name
1308
+ * stays `blockquote`: only the input rule changes, so stored documents,
1309
+ * clipboard HTML, and `Mod+Shift+B` are untouched.
1310
+ *
1311
+ * `>` belongs to the toggle (see `toggle.ts`), which is what Notion users
1312
+ * reach for it expecting.
1313
+ */
1314
+ declare const Quote: import("@tiptap/core").Node<BlockquoteOptions, any>;
1315
+ /** Configures the quote (blockquote) node. */
1316
+ declare function quote(options?: Partial<QuoteOptions>): import("@tiptap/core").Node<BlockquoteOptions, any>;
1317
+ //#endregion
1318
+ export { type AiActionStatus, AiBlock, type AiBlockOptions, type AiBlockStorage, type AiKitOptions, type AiRequest, type AiSlashAction, BLOCK_ID_REMOTE_META, BlockDrag, type BlockDragOptions, type BlockDragState, type BlockDragStorage, BlockId, type BlockIdOptions, type BlockKitOptions, type BlockLocation, type BlockRect, type BlockTarget, BubbleToolbar, type BubbleToolbarItem, type BubbleToolbarOptions, type BubbleToolbarState, type BubbleToolbarStorage, Callout, type CalloutOptions, type CollaborationOptions, type CollaborationProvider, type CollaborationUser, Column, type ColumnOptions, Columns, type ColumnsOptions, Comment, type CommentMessage, type CommentOptions, type CommentState, type CommentStorage, type CommentThread, type CommentThreadStore, type DropMode, type DropTarget, Embed, type EmbedMode, type EmbedOptions, File$1 as File, type FileOptions, type FileStorage, type HeadingLevel, Image, type ImageOptions, type ImageStorage, LinkEditor, type LinkEditorOptions, type LinkEditorState, type LinkEditorStorage, Mention, type MentionItem, type MentionOptions, type MentionState, type MentionStorage, PendingAiRegistry, PendingUploadRegistry, Placeholder, type PlaceholderContext, type PlaceholderKey, type PlaceholderOptions, Quote, type QuoteOptions, type RetryUploadOptions, type RunAiActionOptions, type RunUploadOptions, type SetEmbedOptions, type SetFileFromFile, type SetFileFromSrc, type SetFileOptions, type SetImageFromFile, type SetImageFromSrc, type SetImageOptions, type SetVideoFromFile, type SetVideoFromSrc, type SetVideoOptions, SlashCommand, type SlashCommandOptions, type SlashCommandStorage, type SlashContext, type SlashItem, type SlashMenuState, type StreamAdapter, type StreamContext, TableKit, type TableKitOptions, Toggle, type ToggleLevel, type ToggleOptions, type UploadAdapter, type UploadContext, type UploadResult, type UploadStatus, Video, type VideoOptions, type VideoStorage, activeThreadIds, aiBlock, blockDrag, blockDragPluginKey, blockId, blockIdPluginKey, bubbleToolbar, callout, canAppendChild, canOpenLinkEditor, collaboration, column, columns, comment, createAiSlashItems, createBlockKit, defaultAiSlashActions, defaultBubbleToolbarItems, defaultPlaceholderText, defaultSlashItems, embed, file, filterBubbleToolbarItems, filterSlashItems, findNodeById, image, linkEditor, mention, mentionPluginKey, placeholder, placeholderKeyFor, placeholderPluginKey, quote, quoteInputRegex, resolveDropTarget, retryUpload, runUpload, slashCommand, slashCommandPluginKey, table, toggle, toggleHeadingInputRegex, toggleInputRegex, video };
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { Extension, Mark, Node, callOrReturn, combineTransactionSteps, findChildrenInRange, getChangedRanges, getExtensionField, isTextSelection, mergeAttributes, posToDOMRect } from "@tiptap/core";
1
+ import { Extension, InputRule, Mark, Node, callOrReturn, combineTransactionSteps, findChildrenInRange, findParentNode, getChangedRanges, getExtensionField, isTextSelection, mergeAttributes, posToDOMRect, wrappingInputRule } from "@tiptap/core";
2
2
  import { Fragment } from "@tiptap/pm/model";
3
- import { Plugin, PluginKey } from "@tiptap/pm/state";
3
+ import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
4
4
  import { Decoration, DecorationSet } from "@tiptap/pm/view";
5
5
  import { StarterKit } from "@tiptap/starter-kit";
6
6
  import { Details, DetailsContent, DetailsSummary } from "@tiptap/extension-details";
@@ -8,6 +8,7 @@ import { TaskItem, TaskList } from "@tiptap/extension-list";
8
8
  import { Collaboration } from "@tiptap/extension-collaboration";
9
9
  import { CollaborationCaret } from "@tiptap/extension-collaboration-caret";
10
10
  import { Suggestion } from "@tiptap/suggestion";
11
+ import { Blockquote } from "@tiptap/extension-blockquote";
11
12
  import { TableKit } from "@tiptap/extension-table";
12
13
  //#region src/upload.ts
13
14
  /**
@@ -293,28 +294,28 @@ const defaultAiSlashActions = [
293
294
  id: "continue-writing",
294
295
  title: "Continue writing",
295
296
  description: "AI extends the text above the cursor",
296
- icon: "sparkles",
297
+ icon: "pencil-sparkles",
297
298
  prompt: "Continue writing the document naturally, matching its tone and style. Write only the continuation, with no preamble."
298
299
  },
299
300
  {
300
301
  id: "summarize",
301
302
  title: "Summarize",
302
303
  description: "AI summarizes the text above the cursor",
303
- icon: "sparkles",
304
+ icon: "broom-sparkles",
304
305
  prompt: "Summarize the following text in a few concise sentences."
305
306
  },
306
307
  {
307
308
  id: "brainstorm-ideas",
308
309
  title: "Brainstorm ideas",
309
310
  description: "AI lists ideas related to the text above the cursor",
310
- icon: "sparkles",
311
+ icon: "brain",
311
312
  prompt: "Brainstorm a short bullet list of ideas related to the following text."
312
313
  },
313
314
  {
314
315
  id: "fix-spelling-grammar",
315
316
  title: "Fix spelling & grammar",
316
317
  description: "AI rewrites the text above the cursor, correcting mistakes",
317
- icon: "sparkles",
318
+ icon: "spell-check",
318
319
  prompt: "Rewrite the following text, correcting spelling and grammar mistakes only, and preserve its meaning and tone."
319
320
  }
320
321
  ];
@@ -1904,6 +1905,120 @@ function mention(options) {
1904
1905
  return Mention.configure(options);
1905
1906
  }
1906
1907
  //#endregion
1908
+ //#region src/placeholder.ts
1909
+ const defaultPlaceholderText = {
1910
+ paragraph: "Press '/' for commands…",
1911
+ heading1: "Heading 1",
1912
+ heading2: "Heading 2",
1913
+ heading3: "Heading 3",
1914
+ listItem: "List",
1915
+ taskItem: "To-do",
1916
+ blockquote: "Quote",
1917
+ callout: "Callout",
1918
+ details: "Toggle",
1919
+ toggleHeading1: "Toggle heading 1",
1920
+ toggleHeading2: "Toggle heading 2",
1921
+ toggleHeading3: "Toggle heading 3"
1922
+ };
1923
+ const PARENT_KEYS = {
1924
+ listItem: "listItem",
1925
+ taskItem: "taskItem",
1926
+ blockquote: "blockquote",
1927
+ callout: "callout",
1928
+ detailsContent: "details"
1929
+ };
1930
+ /**
1931
+ * Maps an empty block to its placeholder slot, or `null` when it should stay
1932
+ * blank. Code blocks are excluded: their content is literal, and a hint would
1933
+ * read as source.
1934
+ */
1935
+ function placeholderKeyFor(node, parent) {
1936
+ const name = node.type.name;
1937
+ if (name === "heading") {
1938
+ const { level } = node.attrs;
1939
+ return level === 1 || level === 2 || level === 3 ? `heading${level}` : null;
1940
+ }
1941
+ if (name === "detailsSummary") {
1942
+ const level = parent?.attrs.level;
1943
+ return level === 1 || level === 2 || level === 3 ? `toggleHeading${level}` : "details";
1944
+ }
1945
+ if (name !== "paragraph") return null;
1946
+ return parent ? PARENT_KEYS[parent.type.name] ?? "paragraph" : "paragraph";
1947
+ }
1948
+ const placeholderPluginKey = new PluginKey("placeholder");
1949
+ /**
1950
+ * Empty-block hints, Notion style: only the block holding the caret shows one.
1951
+ *
1952
+ * Renders `data-placeholder` and no class names — the UI layer styles
1953
+ * `[data-placeholder]::before`, matching how every other core node is styled.
1954
+ *
1955
+ * The decoration is derived from the state it is drawn against rather than
1956
+ * from `editor.state`: during a transaction those are different documents, and
1957
+ * resolving a parent in the stale one mislabels every nested block.
1958
+ */
1959
+ const Placeholder = Extension.create({
1960
+ name: "placeholder",
1961
+ addOptions() {
1962
+ return {};
1963
+ },
1964
+ addProseMirrorPlugins() {
1965
+ const { editor } = this;
1966
+ const text = {
1967
+ ...defaultPlaceholderText,
1968
+ ...this.options.text
1969
+ };
1970
+ const { resolve } = this.options;
1971
+ return [new Plugin({
1972
+ key: placeholderPluginKey,
1973
+ props: { decorations: (state) => {
1974
+ if (!editor.isEditable) return null;
1975
+ const { doc, selection } = state;
1976
+ const $anchor = doc.resolve(selection.anchor);
1977
+ const node = $anchor.parent;
1978
+ if (!node.type.isTextblock || node.content.size > 0) return null;
1979
+ const parent = $anchor.depth > 0 ? $anchor.node($anchor.depth - 1) : null;
1980
+ const pos = $anchor.before($anchor.depth);
1981
+ const context = {
1982
+ editor,
1983
+ node,
1984
+ parent,
1985
+ pos
1986
+ };
1987
+ const key = placeholderKeyFor(node, parent);
1988
+ const value = resolve ? resolve(context) : key && text[key];
1989
+ if (!value) return null;
1990
+ return DecorationSet.create(doc, [Decoration.node(pos, pos + node.nodeSize, { "data-placeholder": value })]);
1991
+ } }
1992
+ })];
1993
+ }
1994
+ });
1995
+ /** Configures empty-block placeholders. */
1996
+ function placeholder(options = {}) {
1997
+ return Placeholder.configure(options);
1998
+ }
1999
+ //#endregion
2000
+ //#region src/quote.ts
2001
+ /** `"` + space at the start of a block, Notion style. */
2002
+ const quoteInputRegex = /^\s*"\s$/;
2003
+ /**
2004
+ * Blockquote with its markdown shorthand moved from `>` to `"`. The node name
2005
+ * stays `blockquote`: only the input rule changes, so stored documents,
2006
+ * clipboard HTML, and `Mod+Shift+B` are untouched.
2007
+ *
2008
+ * `>` belongs to the toggle (see `toggle.ts`), which is what Notion users
2009
+ * reach for it expecting.
2010
+ */
2011
+ const Quote = Blockquote.extend({ addInputRules() {
2012
+ return [wrappingInputRule({
2013
+ find: quoteInputRegex,
2014
+ type: this.type
2015
+ })];
2016
+ } });
2017
+ /** Configures the quote (blockquote) node. */
2018
+ function quote(options = {}) {
2019
+ return Quote.configure(options);
2020
+ }
2021
+ //#endregion
1907
2022
  //#region src/slash-items.ts
1908
2023
  const TITLE_PREFIX = 100;
1909
2024
  const TITLE_WORD_PREFIX = 80;
@@ -1933,6 +2048,7 @@ function filterSlashItems(items, query, editor) {
1933
2048
  })).filter((entry) => entry.score > 0).sort((a, b) => b.score - a.score).map((entry) => entry.item);
1934
2049
  }
1935
2050
  const BASIC = "Basic blocks";
2051
+ const ADVANCED = "Advanced blocks";
1936
2052
  const MEDIA = "Media";
1937
2053
  const STRUCTURE = "Structure";
1938
2054
  function hasNode(editor, name) {
@@ -1959,6 +2075,7 @@ const defaultSlashItems = [
1959
2075
  description: "Large section title",
1960
2076
  aliases: ["h1", "title"],
1961
2077
  keywords: ["#"],
2078
+ shortcut: "#",
1962
2079
  icon: "heading-1",
1963
2080
  when: (editor) => hasNode(editor, "heading"),
1964
2081
  run: ({ editor, range }) => {
@@ -1972,6 +2089,7 @@ const defaultSlashItems = [
1972
2089
  description: "Medium section title",
1973
2090
  aliases: ["h2", "subtitle"],
1974
2091
  keywords: ["##"],
2092
+ shortcut: "##",
1975
2093
  icon: "heading-2",
1976
2094
  when: (editor) => hasNode(editor, "heading"),
1977
2095
  run: ({ editor, range }) => {
@@ -1985,6 +2103,7 @@ const defaultSlashItems = [
1985
2103
  description: "Small section title",
1986
2104
  aliases: ["h3"],
1987
2105
  keywords: ["###"],
2106
+ shortcut: "###",
1988
2107
  icon: "heading-3",
1989
2108
  when: (editor) => hasNode(editor, "heading"),
1990
2109
  run: ({ editor, range }) => {
@@ -1998,6 +2117,7 @@ const defaultSlashItems = [
1998
2117
  description: "Unordered list",
1999
2118
  aliases: ["ul", "bullet"],
2000
2119
  keywords: ["-", "*"],
2120
+ shortcut: "-",
2001
2121
  icon: "list",
2002
2122
  when: (editor) => hasNode(editor, "bulletList"),
2003
2123
  run: ({ editor, range }) => {
@@ -2011,6 +2131,7 @@ const defaultSlashItems = [
2011
2131
  description: "Ordered list",
2012
2132
  aliases: ["ol", "numbered"],
2013
2133
  keywords: ["1."],
2134
+ shortcut: "1.",
2014
2135
  icon: "list-ordered",
2015
2136
  when: (editor) => hasNode(editor, "orderedList"),
2016
2137
  run: ({ editor, range }) => {
@@ -2028,6 +2149,7 @@ const defaultSlashItems = [
2028
2149
  "checkbox"
2029
2150
  ],
2030
2151
  keywords: ["[]", "[ ]"],
2152
+ shortcut: "[]",
2031
2153
  icon: "list-checks",
2032
2154
  when: (editor) => hasNode(editor, "taskList"),
2033
2155
  run: ({ editor, range }) => {
@@ -2040,7 +2162,8 @@ const defaultSlashItems = [
2040
2162
  group: BASIC,
2041
2163
  description: "Capture a quotation",
2042
2164
  aliases: ["quote", "citation"],
2043
- keywords: [">"],
2165
+ keywords: ["\""],
2166
+ shortcut: "\"",
2044
2167
  icon: "quote",
2045
2168
  when: (editor) => hasNode(editor, "blockquote"),
2046
2169
  run: ({ editor, range }) => {
@@ -2075,6 +2198,7 @@ const defaultSlashItems = [
2075
2198
  "dropdown"
2076
2199
  ],
2077
2200
  keywords: ["toggle"],
2201
+ shortcut: ">",
2078
2202
  icon: "chevron-right",
2079
2203
  when: (editor) => hasNode(editor, "details"),
2080
2204
  run: ({ editor, range }) => {
@@ -2088,6 +2212,7 @@ const defaultSlashItems = [
2088
2212
  description: "Monospaced code",
2089
2213
  aliases: ["code", "snippet"],
2090
2214
  keywords: ["```"],
2215
+ shortcut: "```",
2091
2216
  icon: "code",
2092
2217
  when: (editor) => hasNode(editor, "codeBlock"),
2093
2218
  run: ({ editor, range }) => {
@@ -2105,12 +2230,55 @@ const defaultSlashItems = [
2105
2230
  "rule"
2106
2231
  ],
2107
2232
  keywords: ["---"],
2233
+ shortcut: "---",
2108
2234
  icon: "minus",
2109
2235
  when: (editor) => hasNode(editor, "horizontalRule"),
2110
2236
  run: ({ editor, range }) => {
2111
2237
  editor.chain().focus().deleteRange(range).setHorizontalRule().run();
2112
2238
  }
2113
2239
  },
2240
+ {
2241
+ id: "toggle-heading-1",
2242
+ title: "Toggle heading 1",
2243
+ group: ADVANCED,
2244
+ description: "Collapsible large title",
2245
+ aliases: ["th1", "toggleheading1"],
2246
+ keywords: ["collapsible", "details"],
2247
+ shortcut: "# >",
2248
+ icon: "heading-1",
2249
+ when: (editor) => hasNode(editor, "details"),
2250
+ run: ({ editor, range }) => {
2251
+ editor.chain().focus().deleteRange(range).setToggle(1).run();
2252
+ }
2253
+ },
2254
+ {
2255
+ id: "toggle-heading-2",
2256
+ title: "Toggle heading 2",
2257
+ group: ADVANCED,
2258
+ description: "Collapsible medium title",
2259
+ aliases: ["th2", "toggleheading2"],
2260
+ keywords: ["collapsible", "details"],
2261
+ shortcut: "## >",
2262
+ icon: "heading-2",
2263
+ when: (editor) => hasNode(editor, "details"),
2264
+ run: ({ editor, range }) => {
2265
+ editor.chain().focus().deleteRange(range).setToggle(2).run();
2266
+ }
2267
+ },
2268
+ {
2269
+ id: "toggle-heading-3",
2270
+ title: "Toggle heading 3",
2271
+ group: ADVANCED,
2272
+ description: "Collapsible small title",
2273
+ aliases: ["th3", "toggleheading3"],
2274
+ keywords: ["collapsible", "details"],
2275
+ shortcut: "### >",
2276
+ icon: "heading-3",
2277
+ when: (editor) => hasNode(editor, "details"),
2278
+ run: ({ editor, range }) => {
2279
+ editor.chain().focus().deleteRange(range).setToggle(3).run();
2280
+ }
2281
+ },
2114
2282
  {
2115
2283
  id: "image",
2116
2284
  title: "Image",
@@ -2222,6 +2390,7 @@ const SlashCommand = Extension.create({
2222
2390
  addOptions() {
2223
2391
  return {
2224
2392
  char: "/",
2393
+ hint: "Type to search",
2225
2394
  items: defaultSlashItems
2226
2395
  };
2227
2396
  },
@@ -2283,6 +2452,7 @@ const SlashCommand = Extension.create({
2283
2452
  return [Suggestion({
2284
2453
  editor,
2285
2454
  char: this.options.char,
2455
+ decorationContent: this.options.hint,
2286
2456
  pluginKey: slashCommandPluginKey,
2287
2457
  allowSpaces: false,
2288
2458
  allow: ({ state, range }) => !state.doc.resolve(range.from).parent.type.spec.code,
@@ -2348,6 +2518,182 @@ function table(options = {}) {
2348
2518
  });
2349
2519
  }
2350
2520
  //#endregion
2521
+ //#region src/toggle.ts
2522
+ /** `>` + space: the toggle shorthand. */
2523
+ const toggleInputRegex = /^>\s$/;
2524
+ /** `#`…`###` + space inside a toggle title: the toggle-heading shorthand. */
2525
+ const toggleHeadingInputRegex = /^(#{1,3})\s$/;
2526
+ function asToggleLevel(value) {
2527
+ return value === 1 || value === 2 || value === 3 ? value : 0;
2528
+ }
2529
+ function canReplaceBlockWithToggle($pos, detailsType) {
2530
+ if ($pos.depth === 0 || !$pos.parent.isTextblock) return false;
2531
+ const index = $pos.index($pos.depth - 1);
2532
+ return $pos.node($pos.depth - 1).canReplaceWith(index, index + 1, detailsType);
2533
+ }
2534
+ /**
2535
+ * Replaces the textblock holding `pos` with a toggle: that block's inline
2536
+ * content becomes the title, the body starts as one empty paragraph, and the
2537
+ * caret lands at the end of the title.
2538
+ *
2539
+ * Titles are `text*`, so inline nodes a summary cannot hold (a mention chip)
2540
+ * degrade to their text rather than failing the whole conversion.
2541
+ */
2542
+ function replaceBlockWithToggle(tr, pos, level) {
2543
+ const $pos = tr.doc.resolve(pos);
2544
+ const { schema } = $pos.parent.type;
2545
+ const detailsType = schema.nodes.details;
2546
+ const summaryType = schema.nodes.detailsSummary;
2547
+ const contentType = schema.nodes.detailsContent;
2548
+ const paragraphType = schema.nodes.paragraph;
2549
+ if (!detailsType || !summaryType || !contentType || !paragraphType) return false;
2550
+ if (!canReplaceBlockWithToggle($pos, detailsType)) return false;
2551
+ const inline = $pos.parent.content;
2552
+ const text = inline.textBetween(0, inline.size);
2553
+ const title = summaryType.contentMatch.matchFragment(inline)?.validEnd ? inline : text ? Fragment.from(schema.text(text)) : Fragment.empty;
2554
+ const summary = summaryType.create(null, title);
2555
+ const body = contentType.create(null, paragraphType.create());
2556
+ const from = $pos.before($pos.depth);
2557
+ tr.replaceWith(from, $pos.after($pos.depth), detailsType.create({ level }, [summary, body]));
2558
+ tr.setSelection(TextSelection.create(tr.doc, from + 2 + summary.content.size));
2559
+ return true;
2560
+ }
2561
+ /**
2562
+ * Tiptap's `Details` with a `level` attribute, the `>` shorthand, and the
2563
+ * toggle-heading shorthands.
2564
+ *
2565
+ * `level` lives on the container rather than the title because the node view
2566
+ * renders the disclosure button as a *sibling* of the summary: with
2567
+ * `data-level` on the wrapper, one selector sizes both, and the marker stays
2568
+ * centred on a title of any size.
2569
+ */
2570
+ const Toggle = Details.extend({
2571
+ addAttributes() {
2572
+ return {
2573
+ ...this.parent?.(),
2574
+ level: {
2575
+ default: 0,
2576
+ parseHTML: (element) => asToggleLevel(Number(element.getAttribute("data-level"))),
2577
+ renderHTML: ({ level }) => level ? { "data-level": String(level) } : {}
2578
+ }
2579
+ };
2580
+ },
2581
+ renderHTML({ HTMLAttributes }) {
2582
+ return [
2583
+ "details",
2584
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
2585
+ 0
2586
+ ];
2587
+ },
2588
+ addNodeView() {
2589
+ const parent = this.parent?.();
2590
+ if (!parent) return null;
2591
+ return (props) => {
2592
+ const view = parent(props);
2593
+ const dom = view.dom;
2594
+ const applyLevel = (node) => {
2595
+ const level = asToggleLevel(node.attrs.level);
2596
+ if (level) dom.setAttribute("data-level", String(level));
2597
+ else dom.removeAttribute("data-level");
2598
+ };
2599
+ applyLevel(props.node);
2600
+ if (this.options.persist) dom.addEventListener("click", (event) => {
2601
+ const { editor, getPos } = props;
2602
+ const target = event.target;
2603
+ if (!editor.isEditable || target?.closest("button")?.parentElement !== dom) return;
2604
+ event.stopPropagation();
2605
+ const pos = getPos();
2606
+ const node = typeof pos === "number" ? editor.state.doc.nodeAt(pos) : null;
2607
+ if (!node || node.type !== this.type) return;
2608
+ const { from, to } = editor.state.selection;
2609
+ editor.chain().command(({ tr }) => {
2610
+ tr.setNodeMarkup(pos, void 0, {
2611
+ ...node.attrs,
2612
+ open: !node.attrs.open
2613
+ });
2614
+ return true;
2615
+ }).setTextSelection({
2616
+ from,
2617
+ to
2618
+ }).focus(void 0, { scrollIntoView: false }).run();
2619
+ }, true);
2620
+ return {
2621
+ ...view,
2622
+ update: (node, decorations, innerDecorations) => {
2623
+ const updated = view.update?.(node, decorations, innerDecorations) ?? true;
2624
+ if (updated) applyLevel(node);
2625
+ return updated;
2626
+ }
2627
+ };
2628
+ };
2629
+ },
2630
+ addCommands() {
2631
+ return {
2632
+ ...this.parent?.(),
2633
+ setToggle: (level = 0) => ({ state, tr, dispatch }) => {
2634
+ const detailsType = state.schema.nodes.details;
2635
+ if (!detailsType) return false;
2636
+ const details = findParentNode((node) => node.type === detailsType)(state.selection);
2637
+ if (details) {
2638
+ if (dispatch) tr.setNodeMarkup(details.pos, void 0, {
2639
+ ...details.node.attrs,
2640
+ level
2641
+ });
2642
+ return true;
2643
+ }
2644
+ if (!canReplaceBlockWithToggle(state.selection.$from, detailsType)) return false;
2645
+ return dispatch ? replaceBlockWithToggle(tr, state.selection.from, level) : true;
2646
+ },
2647
+ setToggleLevel: (level) => ({ state, tr, dispatch }) => {
2648
+ const detailsType = state.schema.nodes.details;
2649
+ const details = detailsType ? findParentNode((node) => node.type === detailsType)(state.selection) : void 0;
2650
+ if (!details) return false;
2651
+ if (dispatch) tr.setNodeMarkup(details.pos, void 0, {
2652
+ ...details.node.attrs,
2653
+ level
2654
+ });
2655
+ return true;
2656
+ }
2657
+ };
2658
+ },
2659
+ addInputRules() {
2660
+ return [
2661
+ ...this.parent?.() ?? [],
2662
+ new InputRule({
2663
+ find: toggleInputRegex,
2664
+ handler: ({ state, range }) => {
2665
+ const { tr } = state;
2666
+ const block = tr.doc.resolve(range.from).parent;
2667
+ const name = block.type.name;
2668
+ if (name !== "paragraph" && name !== "heading") return null;
2669
+ const level = name === "heading" ? asToggleLevel(block.attrs.level) : 0;
2670
+ tr.delete(range.from, range.to);
2671
+ return replaceBlockWithToggle(tr, range.from, level) ? void 0 : null;
2672
+ }
2673
+ }),
2674
+ new InputRule({
2675
+ find: toggleHeadingInputRegex,
2676
+ handler: ({ state, range, match }) => {
2677
+ const { tr } = state;
2678
+ const $from = tr.doc.resolve(range.from);
2679
+ if ($from.parent.type.name !== "detailsSummary") return null;
2680
+ const details = $from.node($from.depth - 1);
2681
+ const level = asToggleLevel(match[1]?.length);
2682
+ tr.delete(range.from, range.to);
2683
+ tr.setNodeMarkup($from.before($from.depth - 1), void 0, {
2684
+ ...details.attrs,
2685
+ level
2686
+ });
2687
+ }
2688
+ })
2689
+ ];
2690
+ }
2691
+ });
2692
+ /** Configures the toggle (details) node. */
2693
+ function toggle(options = {}) {
2694
+ return Toggle.configure(options);
2695
+ }
2696
+ //#endregion
2351
2697
  //#region src/video.ts
2352
2698
  function initialAttrs(id, options) {
2353
2699
  if (options && "file" in options) return {
@@ -2467,7 +2813,7 @@ const DEFAULT_HEADING_LEVELS = [
2467
2813
  * the `data-*` attributes rendered by slash-editor nodes.
2468
2814
  */
2469
2815
  function createBlockKit(options = {}) {
2470
- const { headingLevels = DEFAULT_HEADING_LEVELS, history = true, slash, blockId: blockIdOptions, drag, bubbleToolbar: bubbleToolbarOptions, image: imageOptions, file: fileOptions, video: videoOptions, embed: embedOptions, table: tableOptions, columns: columnsOptions, linkEditor: linkEditorOptions, mention: mentionOptions, ai: aiOptions, collaboration: collaborationOptions, comment: commentOptions, extend = [] } = options;
2816
+ const { headingLevels = DEFAULT_HEADING_LEVELS, history = true, slash, placeholder: placeholderOptions, blockId: blockIdOptions, drag, bubbleToolbar: bubbleToolbarOptions, image: imageOptions, file: fileOptions, video: videoOptions, embed: embedOptions, table: tableOptions, columns: columnsOptions, linkEditor: linkEditorOptions, mention: mentionOptions, ai: aiOptions, collaboration: collaborationOptions, comment: commentOptions, toggle: toggleOptions, extend = [] } = options;
2471
2817
  const resolvedAi = aiOptions ? {
2472
2818
  actions: defaultAiSlashActions,
2473
2819
  node: true,
@@ -2481,12 +2827,17 @@ function createBlockKit(options = {}) {
2481
2827
  link: {
2482
2828
  openOnClick: false,
2483
2829
  enableClickSelection: true
2484
- }
2830
+ },
2831
+ blockquote: false
2485
2832
  }),
2833
+ quote(),
2486
2834
  TaskList,
2487
2835
  TaskItem.configure({ nested: true }),
2488
2836
  Callout,
2489
- Details.configure({ persist: true }),
2837
+ toggle({
2838
+ persist: true,
2839
+ ...toggleOptions
2840
+ }),
2490
2841
  DetailsSummary,
2491
2842
  DetailsContent,
2492
2843
  ...imageOptions === false ? [] : [image(imageOptions)],
@@ -2501,6 +2852,7 @@ function createBlockKit(options = {}) {
2501
2852
  ...slash,
2502
2853
  items: slash?.items ?? (() => [...defaultSlashItems, ...resolvedAi ? createAiSlashItems(resolvedAi) : []])()
2503
2854
  })],
2855
+ ...placeholderOptions === false ? [] : [placeholder(placeholderOptions)],
2504
2856
  ...blockIdOptions === false ? [] : [blockId(blockIdOptions)],
2505
2857
  ...drag === false ? [] : [blockDrag(drag)],
2506
2858
  ...bubbleToolbarOptions === false ? [] : [bubbleToolbar(bubbleToolbarOptions)],
@@ -2511,4 +2863,4 @@ function createBlockKit(options = {}) {
2511
2863
  ];
2512
2864
  }
2513
2865
  //#endregion
2514
- export { AiBlock, BLOCK_ID_REMOTE_META, BlockDrag, BlockId, BubbleToolbar, Callout, Column, Columns, Comment, Embed, File, Image, LinkEditor, Mention, PendingAiRegistry, PendingUploadRegistry, SlashCommand, TableKit, Video, activeThreadIds, aiBlock, blockDrag, blockDragPluginKey, blockId, blockIdPluginKey, bubbleToolbar, callout, canAppendChild, canOpenLinkEditor, collaboration, column, columns, comment, createAiSlashItems, createBlockKit, defaultAiSlashActions, defaultBubbleToolbarItems, defaultSlashItems, embed, file, filterBubbleToolbarItems, filterSlashItems, findNodeById, image, linkEditor, mention, mentionPluginKey, resolveDropTarget, retryUpload, runUpload, slashCommand, slashCommandPluginKey, table, video };
2866
+ export { AiBlock, BLOCK_ID_REMOTE_META, BlockDrag, BlockId, BubbleToolbar, Callout, Column, Columns, Comment, Embed, File, Image, LinkEditor, Mention, PendingAiRegistry, PendingUploadRegistry, Placeholder, Quote, SlashCommand, TableKit, Toggle, Video, activeThreadIds, aiBlock, blockDrag, blockDragPluginKey, blockId, blockIdPluginKey, bubbleToolbar, callout, canAppendChild, canOpenLinkEditor, collaboration, column, columns, comment, createAiSlashItems, createBlockKit, defaultAiSlashActions, defaultBubbleToolbarItems, defaultPlaceholderText, defaultSlashItems, embed, file, filterBubbleToolbarItems, filterSlashItems, findNodeById, image, linkEditor, mention, mentionPluginKey, placeholder, placeholderKeyFor, placeholderPluginKey, quote, quoteInputRegex, resolveDropTarget, retryUpload, runUpload, slashCommand, slashCommandPluginKey, table, toggle, toggleHeadingInputRegex, toggleInputRegex, video };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slash-editor/core",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "Headless block-editor core: ProseMirror/Tiptap extensions, schema, commands.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -24,6 +24,7 @@
24
24
  "dev": "vp pack --watch"
25
25
  },
26
26
  "dependencies": {
27
+ "@tiptap/extension-blockquote": "^3.31.3",
27
28
  "@tiptap/extension-collaboration": "^3.31.3",
28
29
  "@tiptap/extension-collaboration-caret": "^3.31.3",
29
30
  "@tiptap/extension-details": "^3.31.3",