Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(commands)!: Add insertMarkdownContentAt command #439

Merged
merged 1 commit into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { RawCommands, selectionToInsertionEnd } from '@tiptap/core'
import { DOMParser } from 'prosemirror-model'

import { parseHtmlToElement } from '../../../../helpers/dom'
import { getHTMLSerializerInstance } from '../../../../serializers/html/html'

import type { Range } from '@tiptap/core'
import type { ParseOptions } from 'prosemirror-model'

/**
* Augment the official `@tiptap/core` module with extra commands so that the compiler knows about
* them. For this to work externally, a wildcard export needs to be added to the root `index.ts`.
*/
declare module '@tiptap/core' {
interface Commands<ReturnType> {
insertMarkdownContentAt: {
/**
* Inserts the provided Markdown content as HTML into the editor at a specific position.
*
* @param position The position or range the Markdown will be inserted in.
* @param markdown The Markdown content to parse and insert as HTML.
* @param options An optional object with the following parameters:
* @param options.parseOptions The parse options to use when the HTML content is parsed by ProseMirror.
* @param options.updateSelection Whether the selection should move to the newly inserted content.
*/
insertMarkdownContentAt: (
position: number | Range,
markdown: string,
options?: {
parseOptions?: ParseOptions
updateSelection?: boolean
},
) => ReturnType
}
}
}

/**
* Inserts the provided Markdown content as HTML into the editor at a specific position.
*
* The solution for this function was inspired by how ProseMirror pastes content from the clipboard,
* and how Tiptap inserts content with the `insertContentAt` command.
*/
function insertMarkdownContentAt(
position: number | Range,
markdown: string,
options?: {
parseOptions?: ParseOptions
updateSelection?: boolean
},
): ReturnType<RawCommands['insertMarkdownContentAt']> {
return ({ editor, tr, dispatch }) => {
// Check if the transaction should be dispatched
// ref: https://tiptap.dev/api/commands#dry-run-for-commands
if (dispatch) {
// Default values for command options must be set here
// (they do not work if set in the function signature)
options = {
parseOptions: {},
updateSelection: true,
...options,
}

// Get the start and end positions from the provided position
let { from, to } =
typeof position === 'number'
? { from: position, to: position }
: { from: position.from, to: position.to }

// If the selection is empty, and we're in an empty textblock, expand the start and end
// positions to include the whole textblock (not leaving empty paragraphs behind)
if (from === to) {
const { parent } = tr.doc.resolve(from)

if (parent.isTextblock && parent.childCount === 0) {
from -= 1
to += 1
}
}

// Parse the Markdown to HTML and then then into ProseMirror nodes
const htmlContent = getHTMLSerializerInstance(editor.schema).serialize(markdown)
const content = DOMParser.fromSchema(editor.schema).parse(
parseHtmlToElement(htmlContent),
options.parseOptions,
)

// Inserts the content into the editor while preserving the current selection
tr.replaceWith(from, to, content)

// Set the text cursor to the end of the inserted content
if (options.updateSelection) {
selectionToInsertionEnd(tr, tr.steps.length - 1, -1)
}
}

return true
}
}

export { insertMarkdownContentAt }
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import { RawCommands } from '@tiptap/core'
import { DOMParser } from 'prosemirror-model'

import { parseHtmlToElement } from '../../../../helpers/dom'
import { getHTMLSerializerInstance } from '../../../../serializers/html/html'

import type { ParseOptions } from 'prosemirror-model'

Expand All @@ -14,42 +10,43 @@ declare module '@tiptap/core' {
interface Commands<ReturnType> {
insertMarkdownContent: {
/**
* Inserts the provided Markdown as HTML into the editor.
* Inserts the provided Markdown as HTML into the editor at the current position.
*
* @param markdown The Markdown to parse and insert as HTML.
* @param parseOptions The parse options for ProseMirror's DOMParser.
* @param markdown The Markdown content to parse and insert as HTML.
* @param options An optional object with the following parameters:
* @param options.parseOptions The parse options to use when the HTML content is parsed by ProseMirror.
* @param options.updateSelection Whether the selection should move to the newly inserted content.
*/
insertMarkdownContent: (markdown: string, parseOptions?: ParseOptions) => ReturnType
insertMarkdownContent: (
markdown: string,
options?: {
parseOptions?: ParseOptions
updateSelection?: boolean
},
) => ReturnType
}
}
}

/**
* Inserts the provided Markdown as HTML into the editor.
* Inserts the provided Markdown as HTML into the editor at the current position.
*
* The solution for this function was inspired how ProseMirror pastes content from the clipboard,
* and how Tiptap inserts content with the `insertContentAt` command.
* The solution for this function was inspired by how Tiptap inserts content with the
* `insertContent` command.
*/
function insertMarkdownContent(
markdown: string,
parseOptions?: ParseOptions,
options?: {
parseOptions?: ParseOptions
updateSelection?: boolean
},
): ReturnType<RawCommands['insertMarkdownContent']> {
return ({ dispatch, editor, tr }) => {
// Check if the transaction should be dispatched
// ref: https://tiptap.dev/api/commands#dry-run-for-commands
if (dispatch) {
const htmlContent = getHTMLSerializerInstance(editor.schema).serialize(markdown)

// Inserts the HTML content into the editor while preserving the current selection
tr.replaceSelection(
DOMParser.fromSchema(editor.schema).parseSlice(
parseHtmlToElement(htmlContent),
parseOptions,
),
)
}

return true
return ({ commands, tr }) => {
return commands.insertMarkdownContentAt(
{ from: tr.selection.from, to: tr.selection.to },
markdown,
options,
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Extension } from '@tiptap/core'
import { createParagraphEnd } from './commands/create-paragraph-end'
import { extendWordRange } from './commands/extend-word-range'
import { insertMarkdownContent } from './commands/insert-markdown-content'
import { insertMarkdownContentAt } from './commands/insert-markdown-content-at'

/**
* The `ExtraEditorCommands` extension is a collection of editor commands that provide additional
Expand All @@ -16,6 +17,7 @@ const ExtraEditorCommands = Extension.create({
createParagraphEnd,
extendWordRange,
insertMarkdownContent,
insertMarkdownContentAt,
}
},
})
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export * from './constants/extension-priorities'
export * from './extensions/core/extra-editor-commands/commands/create-paragraph-end'
export * from './extensions/core/extra-editor-commands/commands/extend-word-range'
export * from './extensions/core/extra-editor-commands/commands/insert-markdown-content'
export * from './extensions/core/extra-editor-commands/commands/insert-markdown-content-at'
export { PlainTextKit } from './extensions/plain-text/plain-text-kit'
export type {
RichTextImageAttributes,
Expand Down
10 changes: 8 additions & 2 deletions stories/typist-editor/constants/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* @see https://jaspervdj.be/lorem-markdownum/
*/
const MARKDOWN_PLACEHOLDER = `# Volucrum operisque praepetis distulit
const MARKDOWN_PLACEHOLDER_LONG = `# Volucrum operisque praepetis distulit

## Et sponte tamen

Expand Down Expand Up @@ -48,4 +48,10 @@ python_cisc.smartSurfaceLeopard.encryptionOop(permalinkHardError - primary * tec

Erubuit cum caruisse et **passim** mentesque nulla, vox deus, est ut quis iracunda Propoetidas? Velis adeste parentis vincere, [opem](http://passis-locum.io/saepe), quos acceptaque atque.`

export { MARKDOWN_PLACEHOLDER }
const MARKDOWN_PLACEHOLDER_SHORT = `> ### Duobus concita cum
>
> Simul saevo subcrescit **aetherias non** Pisaeae Mater inquit miserabilis
attolle muneris mundi *vivacisque accepisse*. Petit ad nullo omni carbasa,
peccavimus [essem](http://incidere.com/) monte fluxerunt caeruleos subito.`

export { MARKDOWN_PLACEHOLDER_LONG, MARKDOWN_PLACEHOLDER_SHORT }
23 changes: 21 additions & 2 deletions stories/typist-editor/plain-text-functions.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import { useCallback, useRef } from 'react'
import { Button } from '@doist/reactist'

import { action } from '@storybook/addon-actions'
import { Selection } from '@tiptap/pm/state'

import { TypistEditor, TypistEditorRef } from '../../src'

import { DEFAULT_ARG_TYPES } from './constants/defaults'
import { MARKDOWN_PLACEHOLDER } from './constants/markdown'
import { MARKDOWN_PLACEHOLDER_LONG, MARKDOWN_PLACEHOLDER_SHORT } from './constants/markdown'
import { TypistEditorDecorator } from './decorators/typist-editor-decorator/typist-editor-decorator'
import { Default } from './plain-text.stories'

Expand Down Expand Up @@ -41,7 +42,19 @@ export const Commands: StoryObj<typeof TypistEditor> = {
?.getEditor()
.chain()
.focus()
.insertMarkdownContent(MARKDOWN_PLACEHOLDER)
.insertMarkdownContent(MARKDOWN_PLACEHOLDER_LONG)
.run()
}, [])

const handleInsertMarkdownContentAtClick = useCallback(() => {
typistEditorRef.current
?.getEditor()
.chain()
.focus()
.insertMarkdownContentAt(
Selection.atEnd(typistEditorRef.current?.getEditor().state.doc),
MARKDOWN_PLACEHOLDER_SHORT,
)
.run()
}, [])

Expand All @@ -65,6 +78,12 @@ export const Commands: StoryObj<typeof TypistEditor> = {
>
insertMarkdownContent
</Button>
<Button
variant="secondary"
onClick={handleInsertMarkdownContentAtClick}
>
insertMarkdownContentAt
</Button>
</>
)
}}
Expand Down
23 changes: 21 additions & 2 deletions stories/typist-editor/rich-text-functions.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import { useCallback, useRef } from 'react'
import { Button } from '@doist/reactist'

import { action } from '@storybook/addon-actions'
import { Selection } from '@tiptap/pm/state'

import { TypistEditor, TypistEditorRef } from '../../src'

import { DEFAULT_ARG_TYPES } from './constants/defaults'
import { MARKDOWN_PLACEHOLDER } from './constants/markdown'
import { MARKDOWN_PLACEHOLDER_LONG, MARKDOWN_PLACEHOLDER_SHORT } from './constants/markdown'
import { TypistEditorDecorator } from './decorators/typist-editor-decorator/typist-editor-decorator'
import { Default } from './rich-text.stories'

Expand Down Expand Up @@ -41,7 +42,19 @@ export const Commands: StoryObj<typeof TypistEditor> = {
?.getEditor()
.chain()
.focus()
.insertMarkdownContent(MARKDOWN_PLACEHOLDER)
.insertMarkdownContent(MARKDOWN_PLACEHOLDER_LONG)
.run()
}, [])

const handleInsertMarkdownContentAtClick = useCallback(() => {
typistEditorRef.current
?.getEditor()
.chain()
.focus()
.insertMarkdownContentAt(
Selection.atEnd(typistEditorRef.current?.getEditor().state.doc),
MARKDOWN_PLACEHOLDER_SHORT,
)
.run()
}, [])

Expand All @@ -65,6 +78,12 @@ export const Commands: StoryObj<typeof TypistEditor> = {
>
insertMarkdownContent
</Button>
<Button
variant="secondary"
onClick={handleInsertMarkdownContentAtClick}
>
insertMarkdownContentAt
</Button>
</>
)
}}
Expand Down