prosemirror-paste-rules
Advanced tools
Comparing version 2.0.7 to 3.0.0-beta.0
@@ -1,175 +0,11 @@ | ||
import { MarkType, NodeType, Fragment, Node } from 'prosemirror-model'; | ||
import { Plugin, Selection } from 'prosemirror-state'; | ||
import { EditorView } from 'prosemirror-view'; | ||
import { ExtensionPriority } from '@remirror/core-constants'; | ||
/** | ||
* Create the paste plugin handler. | ||
*/ | ||
declare function pasteRules(pasteRules: PasteRule[]): Plugin; | ||
interface BasePasteRule { | ||
/** | ||
* The priority for the extension. Can be a number, or if you're using it with | ||
* `remirror` then use the `ExtensionPriority` enum. | ||
* | ||
* @defaultValue 10 | ||
*/ | ||
priority?: ExtensionPriority; | ||
} | ||
interface BaseRegexPasteRule extends BasePasteRule { | ||
/** | ||
* The regular expression to test against. | ||
*/ | ||
regexp: RegExp; | ||
/** | ||
* Only match at the start of the text block. | ||
*/ | ||
startOfTextBlock?: boolean; | ||
/** | ||
* Ignore the match when all characters in the capture group are whitespace. | ||
* | ||
* This helps stop situations from occurring where the a capture group matches | ||
* but you don't want an update if it's all whitespace. | ||
* | ||
* @defaultValue false | ||
*/ | ||
ignoreWhitespace?: boolean; | ||
/** | ||
* The names of nodes for which this paste rule can be ignored. This means | ||
* that if content is within any of the nodes provided the transformation will | ||
* be ignored. | ||
*/ | ||
ignoredNodes?: string[]; | ||
/** | ||
* The names of marks for which this paste rule can be ignored. This means | ||
* that if the matched content contains this mark it will be ignored. | ||
*/ | ||
ignoredMarks?: string[]; | ||
} | ||
interface BaseContentPasteRule extends BaseRegexPasteRule { | ||
/** | ||
* A helper function for setting the attributes for a transformation. | ||
* | ||
* The second parameter is `true` when the attributes are retrieved for a replacement. | ||
*/ | ||
getAttributes?: Record<string, unknown> | ((match: RegExpExecArray, isReplacement: boolean) => Record<string, unknown> | undefined); | ||
} | ||
/** | ||
* For adding marks to text when a paste rule is activated. | ||
*/ | ||
interface MarkPasteRule extends BaseContentPasteRule { | ||
/** | ||
* The type of rule. | ||
*/ | ||
type: 'mark'; | ||
/** | ||
* The prosemirror mark type instance. | ||
*/ | ||
markType: MarkType; | ||
/** | ||
* Set to `true` to replace the selection. When the regex matches for the | ||
* selected text. | ||
* | ||
* Can be a function which receives the text that will be replaced. | ||
*/ | ||
replaceSelection?: boolean | ((replacedText: string) => boolean); | ||
/** | ||
* A function that transforms the match into the desired text value. | ||
* | ||
* Return an empty string to delete all content. | ||
* | ||
* Return `false` to invalidate the match. | ||
*/ | ||
transformMatch?: (match: RegExpExecArray) => string | null | undefined | false; | ||
} | ||
interface NodePasteRule extends BaseContentPasteRule { | ||
/** | ||
* The type of rule. | ||
*/ | ||
type: 'node'; | ||
/** | ||
* The node type to create. | ||
*/ | ||
nodeType: NodeType; | ||
/** | ||
* A function that transforms the match into the content to use when creating | ||
* a node. | ||
* | ||
* Pass `() => {}` to remove the matched text. | ||
* | ||
* If this function is undefined, then the text node that is cut from the match | ||
* will be used as the content. | ||
*/ | ||
getContent?: (match: RegExpExecArray) => Fragment | Node | Node[] | undefined | void; | ||
} | ||
/** | ||
* For handling simpler text updates. | ||
*/ | ||
interface TextPasteRule extends BaseRegexPasteRule { | ||
/** | ||
* The type of rule. | ||
*/ | ||
type: 'text'; | ||
/** | ||
* A function that transforms the match into the desired text value. | ||
* | ||
* Return an empty string to delete all content. | ||
* | ||
* Return `false` to invalidate the match. | ||
*/ | ||
transformMatch?: (match: RegExpExecArray) => string | null | undefined | false; | ||
} | ||
type FileHandlerProps = FilePasteHandlerProps | FileDropHandlerProps; | ||
interface FilePasteHandlerProps { | ||
type: 'paste'; | ||
/** All the matching files */ | ||
files: File[]; | ||
event: ClipboardEvent; | ||
view: EditorView; | ||
selection: Selection; | ||
} | ||
interface FileDropHandlerProps { | ||
type: 'drop'; | ||
/** All the matching files */ | ||
files: File[]; | ||
event: DragEvent; | ||
view: EditorView; | ||
pos: number; | ||
} | ||
/** | ||
* For handling pasting files and also file drops. | ||
*/ | ||
interface FilePasteRule extends BasePasteRule { | ||
type: 'file'; | ||
/** | ||
* A regex test for the file type. | ||
*/ | ||
regexp?: RegExp; | ||
/** | ||
* The names of nodes for which this paste rule can be ignored. This means | ||
* that if content is within any of the nodes provided the transformation will | ||
* be ignored. | ||
*/ | ||
ignoredNodes?: string[]; | ||
/** | ||
* Return `false` to defer to the next image handler. | ||
* | ||
* The file | ||
*/ | ||
fileHandler: (props: FileHandlerProps) => boolean; | ||
} | ||
type PasteRule = FilePasteRule | TextPasteRule | NodePasteRule | MarkPasteRule; | ||
interface IsInCodeOptions { | ||
/** | ||
* When this is set to true ensure the selection is fully contained within a code block. This means that selections that span multiple characters must all be within a code region for it to return true. | ||
* | ||
* @defaultValue true | ||
*/ | ||
contained?: boolean; | ||
} | ||
/** | ||
* Check whether the current selection is completely contained within a code block or mark. | ||
*/ | ||
declare function isInCode(selection: Selection, { contained }?: IsInCodeOptions): boolean; | ||
export { FileDropHandlerProps, FileHandlerProps, FilePasteHandlerProps, FilePasteRule, IsInCodeOptions, MarkPasteRule, NodePasteRule, PasteRule, TextPasteRule, isInCode, pasteRules }; | ||
export { pasteRules } from './_tsup-dts-rollup'; | ||
export { isInCode } from './_tsup-dts-rollup'; | ||
export { MarkPasteRule } from './_tsup-dts-rollup'; | ||
export { NodePasteRule } from './_tsup-dts-rollup'; | ||
export { TextPasteRule } from './_tsup-dts-rollup'; | ||
export { FileHandlerProps } from './_tsup-dts-rollup'; | ||
export { FilePasteHandlerProps } from './_tsup-dts-rollup'; | ||
export { FileDropHandlerProps } from './_tsup-dts-rollup'; | ||
export { FilePasteRule } from './_tsup-dts-rollup'; | ||
export { PasteRule } from './_tsup-dts-rollup'; | ||
export { IsInCodeOptions } from './_tsup-dts-rollup'; |
@@ -1,6 +0,3 @@ | ||
// packages/prosemirror-paste-rules/src/paste-rules-plugin.ts | ||
import { | ||
Fragment, | ||
Slice | ||
} from "prosemirror-model"; | ||
// src/paste-rules-plugin.ts | ||
import { Fragment, Slice } from "prosemirror-model"; | ||
import { Plugin, PluginKey } from "prosemirror-state"; | ||
@@ -10,6 +7,3 @@ import { ExtensionPriority } from "@remirror/core-constants"; | ||
function pasteRules(pasteRules2) { | ||
const sortedPasteRules = sort( | ||
pasteRules2, | ||
(a, z) => (z.priority ?? ExtensionPriority.Low) - (a.priority ?? ExtensionPriority.Low) | ||
); | ||
const sortedPasteRules = sort(pasteRules2, (a, z) => (z.priority ?? ExtensionPriority.Low) - (a.priority ?? ExtensionPriority.Low)); | ||
const regexPasteRules2 = []; | ||
@@ -27,3 +21,3 @@ const filePasteRules = []; | ||
key: pastePluginKey, | ||
view: (editorView) => { | ||
view: editorView => { | ||
view = editorView; | ||
@@ -34,20 +28,22 @@ return {}; | ||
// The regex based paste rules are passed into this function to take care of. | ||
transformPasted: (slice) => { | ||
var _a, _b, _c; | ||
transformPasted: slice => { | ||
const $pos = view.state.selection.$from; | ||
const nodeName = $pos.node().type.name; | ||
const markNames = new Set($pos.marks().map((mark) => mark.type.name)); | ||
const markNames = new Set($pos.marks().map(mark => mark.type.name)); | ||
for (const rule of regexPasteRules2) { | ||
if ( | ||
// The parent node is ignored. | ||
((_a = rule.ignoredNodes) == null ? void 0 : _a.includes(nodeName)) || // The current position contains ignored marks. | ||
((_b = rule.ignoredMarks) == null ? void 0 : _b.some((ignored) => markNames.has(ignored))) | ||
) { | ||
// The parent node is ignored. | ||
rule.ignoredNodes?.includes(nodeName) || | ||
// The current position contains ignored marks. | ||
rule.ignoredMarks?.some(ignored => markNames.has(ignored))) { | ||
continue; | ||
} | ||
const textContent = ((_c = slice.content.firstChild) == null ? void 0 : _c.textContent) ?? ""; | ||
const textContent = slice.content.firstChild?.textContent ?? ""; | ||
const canBeReplaced = !view.state.selection.empty && slice.content.childCount === 1 && textContent; | ||
const match = findMatches(textContent, rule.regexp)[0]; | ||
if (canBeReplaced && match && rule.type === "mark" && rule.replaceSelection) { | ||
const { from, to } = view.state.selection; | ||
const { | ||
from, | ||
to | ||
} = view.state.selection; | ||
const textSlice = view.state.doc.slice(from, to); | ||
@@ -57,6 +53,9 @@ const textContent2 = textSlice.content.textBetween(0, textSlice.content.size); | ||
const newTextNodes = []; | ||
const { getAttributes, markType } = rule; | ||
const { | ||
getAttributes, | ||
markType | ||
} = rule; | ||
const attributes = isFunction(getAttributes) ? getAttributes(match, true) : getAttributes; | ||
const mark = markType.create(attributes); | ||
textSlice.content.forEach((textNode) => { | ||
textSlice.content.forEach(textNode => { | ||
if (textNode.isText) { | ||
@@ -70,7 +69,6 @@ const marks = mark.addToSet(textNode.marks); | ||
} | ||
const { nodes: transformedNodes, transformed } = regexPasteRuleHandler( | ||
slice.content, | ||
rule, | ||
view.state.schema | ||
); | ||
const { | ||
nodes: transformedNodes, | ||
transformed | ||
} = regexPasteRuleHandler(slice.content, rule, view.state.schema); | ||
if (transformed) { | ||
@@ -85,22 +83,34 @@ slice = rule.type === "node" && rule.nodeType.isBlock ? new Slice(Fragment.fromArray(transformedNodes), 0, 0) : new Slice(Fragment.fromArray(transformedNodes), slice.openStart, slice.openEnd); | ||
paste: (view2, clipboardEvent) => { | ||
var _a, _b; | ||
const event = clipboardEvent; | ||
if (!((_b = (_a = view2.props).editable) == null ? void 0 : _b.call(_a, view2.state))) { | ||
if (!view2.props.editable?.(view2.state)) { | ||
return false; | ||
} | ||
const { clipboardData } = event; | ||
const { | ||
clipboardData | ||
} = event; | ||
if (!clipboardData) { | ||
return false; | ||
} | ||
const allFiles = [...clipboardData.items].map((data) => data.getAsFile()).filter((file) => !!file); | ||
const allFiles = [...clipboardData.items].map(data => data.getAsFile()).filter(file => !!file); | ||
if (allFiles.length === 0) { | ||
return false; | ||
} | ||
const { selection } = view2.state; | ||
for (const { fileHandler, regexp } of filePasteRules) { | ||
const files = regexp ? allFiles.filter((file) => regexp.test(file.type)) : allFiles; | ||
const { | ||
selection | ||
} = view2.state; | ||
for (const { | ||
fileHandler, | ||
regexp | ||
} of filePasteRules) { | ||
const files = regexp ? allFiles.filter(file => regexp.test(file.type)) : allFiles; | ||
if (files.length === 0) { | ||
continue; | ||
} | ||
if (fileHandler({ event, files, selection, view: view2, type: "paste" })) { | ||
if (fileHandler({ | ||
event, | ||
files, | ||
selection, | ||
view: view2, | ||
type: "paste" | ||
})) { | ||
event.preventDefault(); | ||
@@ -114,8 +124,11 @@ return true; | ||
drop: (view2, dragEvent) => { | ||
var _a, _b, _c; | ||
const event = dragEvent; | ||
if (!((_b = (_a = view2.props).editable) == null ? void 0 : _b.call(_a, view2.state))) { | ||
if (!view2.props.editable?.(view2.state)) { | ||
return false; | ||
} | ||
const { dataTransfer, clientX, clientY } = event; | ||
const { | ||
dataTransfer, | ||
clientX, | ||
clientY | ||
} = event; | ||
if (!dataTransfer) { | ||
@@ -128,9 +141,21 @@ return false; | ||
} | ||
const pos = ((_c = view2.posAtCoords({ left: clientX, top: clientY })) == null ? void 0 : _c.pos) ?? view2.state.selection.anchor; | ||
for (const { fileHandler, regexp } of filePasteRules) { | ||
const files = regexp ? allFiles.filter((file) => regexp.test(file.type)) : allFiles; | ||
const pos = view2.posAtCoords({ | ||
left: clientX, | ||
top: clientY | ||
})?.pos ?? view2.state.selection.anchor; | ||
for (const { | ||
fileHandler, | ||
regexp | ||
} of filePasteRules) { | ||
const files = regexp ? allFiles.filter(file => regexp.test(file.type)) : allFiles; | ||
if (files.length === 0) { | ||
continue; | ||
} | ||
if (fileHandler({ event, files, pos, view: view2, type: "drop" })) { | ||
if (fileHandler({ | ||
event, | ||
files, | ||
pos, | ||
view: view2, | ||
type: "drop" | ||
})) { | ||
event.preventDefault(); | ||
@@ -149,7 +174,16 @@ return true; | ||
return function handler(props) { | ||
const { fragment, rule, nodes } = props; | ||
const { regexp, ignoreWhitespace, ignoredMarks, ignoredNodes } = rule; | ||
const { | ||
fragment, | ||
rule, | ||
nodes | ||
} = props; | ||
const { | ||
regexp, | ||
ignoreWhitespace, | ||
ignoredMarks, | ||
ignoredNodes | ||
} = rule; | ||
let transformed = false; | ||
fragment.forEach((child) => { | ||
if ((ignoredNodes == null ? void 0 : ignoredNodes.includes(child.type.name)) || isCodeNode(child)) { | ||
fragment.forEach(child => { | ||
if (ignoredNodes?.includes(child.type.name) || isCodeNode(child)) { | ||
nodes.push(child); | ||
@@ -159,4 +193,8 @@ return; | ||
if (!child.isText) { | ||
const childResult = handler({ fragment: child.content, rule, nodes: [] }); | ||
transformed || (transformed = childResult.transformed); | ||
const childResult = handler({ | ||
fragment: child.content, | ||
rule, | ||
nodes: [] | ||
}); | ||
transformed ||= childResult.transformed; | ||
const content = Fragment.fromArray(childResult.nodes); | ||
@@ -170,3 +208,3 @@ if (child.type.validContent(content)) { | ||
} | ||
if (child.marks.some((mark) => isCodeMark(mark) || (ignoredMarks == null ? void 0 : ignoredMarks.includes(mark.type.name)))) { | ||
if (child.marks.some(mark => isCodeMark(mark) || ignoredMarks?.includes(mark.type.name))) { | ||
nodes.push(child); | ||
@@ -181,6 +219,5 @@ return; | ||
if ( | ||
// This helps prevent matches which are only whitespace from triggering | ||
// an update. | ||
ignoreWhitespace && (capturedValue == null ? void 0 : capturedValue.trim()) === "" || !fullValue | ||
) { | ||
// This helps prevent matches which are only whitespace from triggering | ||
// an update. | ||
ignoreWhitespace && capturedValue?.trim() === "" || !fullValue) { | ||
return; | ||
@@ -203,3 +240,9 @@ } | ||
} | ||
transformer({ nodes, rule, textNode, match, schema }); | ||
transformer({ | ||
nodes, | ||
rule, | ||
textNode, | ||
match, | ||
schema | ||
}); | ||
transformed = true; | ||
@@ -212,12 +255,25 @@ pos = end; | ||
}); | ||
return { nodes, transformed }; | ||
return { | ||
nodes, | ||
transformed | ||
}; | ||
}; | ||
} | ||
function markRuleTransformer(props) { | ||
const { nodes, rule, textNode, match, schema } = props; | ||
const { transformMatch, getAttributes, markType } = rule; | ||
const { | ||
nodes, | ||
rule, | ||
textNode, | ||
match, | ||
schema | ||
} = props; | ||
const { | ||
transformMatch, | ||
getAttributes, | ||
markType | ||
} = rule; | ||
const attributes = isFunction(getAttributes) ? getAttributes(match, false) : getAttributes; | ||
const text = textNode.text ?? ""; | ||
const mark = markType.create(attributes); | ||
const transformedCapturedValue = transformMatch == null ? void 0 : transformMatch(match); | ||
const transformedCapturedValue = transformMatch?.(match); | ||
if (transformedCapturedValue === "") { | ||
@@ -234,5 +290,13 @@ return; | ||
function textRuleTransformer(props) { | ||
const { nodes, rule, textNode, match, schema } = props; | ||
const { transformMatch } = rule; | ||
const transformedCapturedValue = transformMatch == null ? void 0 : transformMatch(match); | ||
const { | ||
nodes, | ||
rule, | ||
textNode, | ||
match, | ||
schema | ||
} = props; | ||
const { | ||
transformMatch | ||
} = rule; | ||
const transformedCapturedValue = transformMatch?.(match); | ||
if (transformedCapturedValue === "" || transformedCapturedValue === false) { | ||
@@ -245,4 +309,13 @@ return; | ||
function nodeRuleTransformer(props) { | ||
const { nodes, rule, textNode, match } = props; | ||
const { getAttributes, nodeType, getContent } = rule; | ||
const { | ||
nodes, | ||
rule, | ||
textNode, | ||
match | ||
} = props; | ||
const { | ||
getAttributes, | ||
nodeType, | ||
getContent | ||
} = rule; | ||
const attributes = isFunction(getAttributes) ? getAttributes(match, false) : getAttributes; | ||
@@ -256,7 +329,19 @@ const content = (getContent ? getContent(match) : textNode) || void 0; | ||
case "mark": | ||
return createPasteRuleHandler(markRuleTransformer, schema)({ fragment, nodes, rule }); | ||
return createPasteRuleHandler(markRuleTransformer, schema)({ | ||
fragment, | ||
nodes, | ||
rule | ||
}); | ||
case "node": | ||
return createPasteRuleHandler(nodeRuleTransformer, schema)({ fragment, nodes, rule }); | ||
return createPasteRuleHandler(nodeRuleTransformer, schema)({ | ||
fragment, | ||
nodes, | ||
rule | ||
}); | ||
default: | ||
return createPasteRuleHandler(textRuleTransformer, schema)({ fragment, nodes, rule }); | ||
return createPasteRuleHandler(textRuleTransformer, schema)({ | ||
fragment, | ||
nodes, | ||
rule | ||
}); | ||
} | ||
@@ -268,3 +353,5 @@ } | ||
} | ||
function isInCode(selection, { contained = true } = {}) { | ||
function isInCode(selection, { | ||
contained = true | ||
} = {}) { | ||
if (selection.empty) { | ||
@@ -292,20 +379,19 @@ return resolvedPosInCode(selection.$head); | ||
function isCodeNode(node) { | ||
var _a; | ||
return node.type.spec.code || ((_a = node.type.spec.group) == null ? void 0 : _a.split(" ").includes("code")); | ||
return node.type.spec.code || node.type.spec.group?.split(" ").includes("code"); | ||
} | ||
function isCodeMark(mark) { | ||
var _a; | ||
return mark.type.name === "code" || ((_a = mark.type.spec.group) == null ? void 0 : _a.split(" ").includes("code")); | ||
return mark.type.name === "code" || mark.type.spec.group?.split(" ").includes("code"); | ||
} | ||
function getDataTransferFiles(event) { | ||
var _a, _b; | ||
const { dataTransfer } = event; | ||
const { | ||
dataTransfer | ||
} = event; | ||
if (!dataTransfer) { | ||
return []; | ||
} | ||
if (((_a = dataTransfer.files) == null ? void 0 : _a.length) > 0) { | ||
if (dataTransfer.files?.length > 0) { | ||
return [...dataTransfer.files]; | ||
} | ||
if ((_b = dataTransfer.items) == null ? void 0 : _b.length) { | ||
return [...dataTransfer.items].map((item) => item.getAsFile()).filter((item) => !!item); | ||
if (dataTransfer.items?.length) { | ||
return [...dataTransfer.items].map(item => item.getAsFile()).filter(item => !!item); | ||
} | ||
@@ -318,5 +404,2 @@ return []; | ||
} | ||
export { | ||
isInCode, | ||
pasteRules | ||
}; | ||
export { isInCode, pasteRules }; |
{ | ||
"name": "prosemirror-paste-rules", | ||
"version": "2.0.7", | ||
"version": "3.0.0-beta.0", | ||
"description": "Better handling of pasted content in your prosemirror editor.", | ||
@@ -34,7 +34,8 @@ "homepage": "https://github.com/remirror/remirror/tree/HEAD/packages/prosemirror-paste-rules", | ||
"@babel/runtime": "^7.22.3", | ||
"@remirror/core-constants": "^2.0.2", | ||
"@remirror/core-helpers": "^3.0.0", | ||
"@remirror/core-constants": "3.0.0-beta.0", | ||
"@remirror/core-helpers": "4.0.0-beta.0", | ||
"escape-string-regexp": "^4.0.0" | ||
}, | ||
"devDependencies": { | ||
"@remirror/cli": "1.0.1", | ||
"prosemirror-model": "^1.19.3", | ||
@@ -55,3 +56,6 @@ "prosemirror-state": "^1.4.3", | ||
"sizeLimit": "10 KB" | ||
}, | ||
"scripts": { | ||
"build": "remirror-cli build" | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
99271
13
1606
4
1
+ Added@remirror/core-constants@3.0.0-beta.0(transitive)
+ Added@remirror/core-helpers@4.0.0-beta.0(transitive)
+ Added@remirror/types@2.0.0-beta.0(transitive)
+ Addedprosemirror-view@1.37.0(transitive)
+ Addedtype-fest@3.13.1(transitive)
- Removed@remirror/core-constants@2.0.2(transitive)
- Removed@remirror/core-helpers@3.0.0(transitive)
- Removed@remirror/types@1.0.1(transitive)
- Removedprosemirror-view@1.36.0(transitive)
- Removedtype-fest@2.19.0(transitive)