From 0a5bd279786d31dada8bc33d74e2a69c42fb1515 Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Sat, 24 Aug 2024 10:37:55 +0200 Subject: [PATCH] Add support for clipboardInputFilter/clipboardOutputFilter FEATURE: The new `EditorView.clipboardInputFilter` and `clipboardOutputFilter` facets allow you to register filter functions that change text taken from or sent to the clipboard. --- src/editorview.ts | 10 +++++++++- src/extension.ts | 3 +++ src/input.ts | 17 +++++++++++++---- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/editorview.ts b/src/editorview.ts index f6b81f4..39f8ecb 100644 --- a/src/editorview.ts +++ b/src/editorview.ts @@ -16,7 +16,8 @@ import {ViewUpdate, styleModule, exceptionSink, updateListener, logException, viewPlugin, ViewPlugin, PluginValue, PluginInstance, decorations, outerDecorations, atomicRanges, scrollMargins, MeasureRequest, editable, inputHandler, focusChangeEffect, perLineTextDirection, - scrollIntoView, UpdateFlag, ScrollTarget, bidiIsolatedRanges, getIsolatedRanges, scrollHandler} from "./extension" + scrollIntoView, UpdateFlag, ScrollTarget, bidiIsolatedRanges, getIsolatedRanges, scrollHandler, + clipboardInputFilter, clipboardOutputFilter} from "./extension" import {theme, darkTheme, buildTheme, baseThemeID, baseLightID, baseDarkID, lightDarkIDs, baseTheme} from "./theme" import {DOMObserver} from "./domobserver" import {Attrs, updateAttrs, combineAttrs} from "./attributes" @@ -966,6 +967,13 @@ export class EditorView { /// dispatching the custom behavior as a separate transaction. static inputHandler = inputHandler + /// Functions provided in this facet will be used to transform text + /// pasted or dropped into the editor. + static clipboardInputFilter = clipboardInputFilter + + /// Transform text copied or dragged from the editor. + static clipboardOutputFilter = clipboardOutputFilter + /// Scroll handlers can override how things are scrolled into view. /// If they return `true`, no further handling happens for the /// scrolling. If they return false, the default scroll behavior is diff --git a/src/extension.ts b/src/extension.ts index bad23a7..275ecf5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -30,6 +30,9 @@ export const inputHandler = Facet.define<(view: EditorView, from: number, to: nu export const focusChangeEffect = Facet.define<(state: EditorState, focusing: boolean) => StateEffect | null>() +export const clipboardInputFilter = Facet.define<(text: string, state: EditorState) => string>() +export const clipboardOutputFilter = Facet.define<(text: string, state: EditorState) => string>() + export const perLineTextDirection = Facet.define({ combine: values => values.some(x => x) }) diff --git a/src/input.ts b/src/input.ts index 6a0baba..d1b985f 100644 --- a/src/input.ts +++ b/src/input.ts @@ -1,9 +1,10 @@ -import {EditorSelection, EditorState, SelectionRange, RangeSet, Annotation, Text} from "@codemirror/state" +import {EditorSelection, EditorState, SelectionRange, RangeSet, Annotation, Text, Facet} from "@codemirror/state" import {EditorView} from "./editorview" import {ContentView} from "./contentview" import {LineView} from "./blockview" import {ViewUpdate, PluginValue, clickAddsSelectionRange, dragMovesSelection as dragBehavior, atomicRanges, - logException, mouseSelectionStyle, PluginInstance, focusChangeEffect, getScrollMargins} from "./extension" + logException, mouseSelectionStyle, PluginInstance, focusChangeEffect, getScrollMargins, + clipboardInputFilter, clipboardOutputFilter} from "./extension" import browser from "./browser" import {groupAt, skipAtomicRanges} from "./cursor" import {getSelection, focusPreventScroll, Rect, dispatchKey, scrollableParents} from "./dom" @@ -471,7 +472,13 @@ function capturePaste(view: EditorView) { }, 50) } +function textFilter(state: EditorState, facet: Facet<(value: string, state: EditorState) => string>, text: string) { + for (let filter of state.facet(facet)) text = filter(text, state) + return text +} + function doPaste(view: EditorView, input: string) { + input = textFilter(view.state, clipboardInputFilter, input) let {state} = view, changes, i = 1, text = state.toText(input) let byLine = text.lines == state.selection.ranges.length let linewise = lastLinewiseCopy != null && state.selection.ranges.every(r => r.empty) && lastLinewiseCopy == text.toString() @@ -653,7 +660,8 @@ handlers.dragstart = (view, event: DragEvent) => { inputState.draggedContent = range if (event.dataTransfer) { - event.dataTransfer.setData("Text", view.state.sliceDoc(range.from, range.to)) + event.dataTransfer.setData("Text", textFilter(view.state, clipboardOutputFilter, + view.state.sliceDoc(range.from, range.to))) event.dataTransfer.effectAllowed = "copyMove" } return false @@ -665,6 +673,7 @@ handlers.dragend = view => { } function dropText(view: EditorView, event: DragEvent, text: string, direct: boolean) { + text = textFilter(view.state, clipboardInputFilter, text) if (!text) return let dropPos = view.posAtCoords({x: event.clientX, y: event.clientY}, false) @@ -764,7 +773,7 @@ function copiedRange(state: EditorState) { linewise = true } - return {text: content.join(state.lineBreak), ranges, linewise} + return {text: textFilter(state, clipboardOutputFilter, content.join(state.lineBreak)), ranges, linewise} } let lastLinewiseCopy: string | null = null