From b974683715155ac4555ab3deafecc04681d7353a Mon Sep 17 00:00:00 2001 From: onzag Date: Wed, 6 May 2020 20:40:27 +0300 Subject: [PATCH 1/3] added clipboard matchers disabler/enabler on update --- README.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- src/index.tsx | 40 ++++++++++++++++++++++++++++++++++++- 3 files changed, 95 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 05fe556c..26dc09c3 100644 --- a/README.md +++ b/README.md @@ -414,6 +414,14 @@ The Mixin has been considered an anti-pattern for a long time now, so we have de There is no upgrade path. If you have a use case that relied on the Mixin, you're encouraged to open an issue, and we will try to provide you with a new feature to make it possible, or dedicated support to migrate out of it. +## Clipboard Matchers + +Clipboards matchers are used for converting values in controlled components, in v2 you are able to disable and enable matchers during an update event in order to be able to have control +over the way values are converted to deltas vs during an update event where the editor contents is being set + +This is specially useful if for instance you have content which value can change (eg. a realtime text editor) but you want to only allow plaintext to be pasted via a matcher, you can disable +the plaintext matcher from the clipboard during the update event + ### Toolbar component Quill has long provided built-in support for custom toolbars, which replaced ReactQuill's (quite inflexible) Toolbar component. @@ -510,6 +518,53 @@ const {Quill} = ReactQuill; : If true, a `pre` tag is used for the editor area instead of the default `div` tag. This prevents Quill from collapsing continuous whitespaces on paste. [Related issue](https://github.com/quilljs/quill/issues/1751). +`disableClipboardMatchersOnUpdate` +: An array of matchers similar in shape to the matchers that exist in your module clipboard config that will be disabled during an update event + +
+Example + +~~~tsx +function plainTextOnly(node: Node) { + return new Delta().insert(node.textContent); +} +const PLAINTEXT_ONLY_MATCHERS: ReactQuill.ClipboardMatcher[] = [ + [Node.ELEMENT_NODE, plainTextOnly], +]; +// prevent modules from updating in each render +const DEFAULT_MODULES = { + clipboard: { + matchVisual: false, + matchers: PLAINTEXT_ONLY_MATCHERS, + } +} + +class RealtimeEditor extends React.Component { + constructor(props: RealtimeEditorProps) { + super(props) + } + + public render() { + return ( +
+ +
+ ) + } +} +~~~ + +
+ +`enableClipboardMatchersOnUpdate` +: An array of matchers, similar to the matchers in the module clipboard, this time they will be added during an update event, and only exist during such event, you might use +this to have matching functionality that you don't want to have on a paste event + ### Methods If you have [a ref](https://facebook.github.io/react/docs/more-about-refs.html) to a ReactQuill node, you will be able to invoke the following methods: diff --git a/package.json b/package.json index abd2ba86..69c71078 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-quill", - "version": "2.0.0-beta.2", + "version": "2.0.0-beta.3", "description": "The Quill rich-text editor as a React component.", "author": "zenoamaro ", "license": "MIT", diff --git a/src/index.tsx b/src/index.tsx index 74d15d35..d2204544 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -21,6 +21,7 @@ import Quill, { namespace ReactQuill { export type Value = string | DeltaStatic; export type Range = RangeStatic | null; + export type ClipboardMatcher = [number | string, (node: any, delta: DeltaStatic) => DeltaStatic]; export interface QuillOptions extends QuillOptionsStatic { tabIndex?: number, @@ -66,6 +67,8 @@ namespace ReactQuill { tabIndex?: number, theme?: string, value?: Value, + disableClipboardMatchersOnUpdate?: ClipboardMatcher[], + enableClipboardMatchersOnUpdate?: ClipboardMatcher[], } export interface UnprivilegedEditor { @@ -382,7 +385,42 @@ class ReactQuill extends React.Component { this.value = value; const sel = this.getEditorSelection(); if (typeof value === 'string') { - editor.setContents(editor.clipboard.convert(value)); + // if we have matchers to disable during an update event in order to sanitize our data differently than on a paste event + if (this.props.disableClipboardMatchersOnUpdate || this.props.enableClipboardMatchersOnUpdate) { + + // first we grab our old clipboard matchers as we want to restore them after the update + // sadly the type of this is a ClipboardStatic type which doesn't expect to be modified + // however it can indded be manually modified, the issue is that Quill lacks removeMatcher functionality + // and as so it has to be performed manually + const oldClipboardMatchers: ReactQuill.ClipboardMatcher[] = (editor.clipboard as any).matchers; + // now we build new clipboard matchers based on these, as we filter we create a new array in place + let newClipboardMatchers: ReactQuill.ClipboardMatcher[] = + (editor.clipboard as any).matchers.filter((matcher: ReactQuill.ClipboardMatcher) => { + // if we have no matchers to disable we return true and it goes in + if (!this.props.disableClipboardMatchersOnUpdate) { + return true; + } + // otherwise we check if it exists within the list + return !this.props.disableClipboardMatchersOnUpdate + .find((disabledMatcher) => disabledMatcher[0] === matcher[0] && disabledMatcher[1] === matcher[1]) + }); + + // now if we have matchers to enable we add them + if (this.props.enableClipboardMatchersOnUpdate) { + newClipboardMatchers = newClipboardMatchers.concat(this.props.enableClipboardMatchersOnUpdate) + } + + // now we update the matcher list + (editor.clipboard as any).matchers = newClipboardMatchers; + + // perform the set contents action + editor.setContents(editor.clipboard.convert(value)); + + // and then restore the matchers + (editor.clipboard as any).matchers = oldClipboardMatchers; + } else { + editor.setContents(editor.clipboard.convert(value)); + } } else { editor.setContents(value); } From e680ae262e01d7020561751dbffbd8ffac129ce2 Mon Sep 17 00:00:00 2001 From: onzag Date: Wed, 6 May 2020 20:41:20 +0300 Subject: [PATCH 2/3] updated readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 26dc09c3..03ead57f 100644 --- a/README.md +++ b/README.md @@ -414,7 +414,7 @@ The Mixin has been considered an anti-pattern for a long time now, so we have de There is no upgrade path. If you have a use case that relied on the Mixin, you're encouraged to open an issue, and we will try to provide you with a new feature to make it possible, or dedicated support to migrate out of it. -## Clipboard Matchers +### Clipboard Matchers Clipboards matchers are used for converting values in controlled components, in v2 you are able to disable and enable matchers during an update event in order to be able to have control over the way values are converted to deltas vs during an update event where the editor contents is being set From 94866d61e2c65b3446bdac8503c44fe429e48623 Mon Sep 17 00:00:00 2001 From: onzag Date: Thu, 4 Jun 2020 16:01:44 +0300 Subject: [PATCH 3/3] added beforeChange function --- README.md | 130 + package-lock.json | 9387 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- src/index.tsx | 19 + 4 files changed, 9537 insertions(+), 1 deletion(-) create mode 100644 package-lock.json diff --git a/README.md b/README.md index 03ead57f..6737bb54 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ See a [live demo] or [Codepen](http://codepen.io/alexkrolick/pen/xgyOXQ/left?edi - [Usage](#usage) - [Controlled mode caveats](#controlled-mode-caveats) - [Using Deltas](#using-deltas) + - [SSR](#ssr) - [Themes](#themes) - [Custom Toolbar](#custom-toolbar) - [Default Toolbar Elements](#default-toolbar-elements) @@ -107,6 +108,11 @@ Because Quill handles its own changes, and does not allow preventing edits, Reac If you frequently need to manipulate the DOM or use the [Quill API](https://quilljs.com/docs/api/)s imperatively, you might consider switching to fully uncontrolled mode. ReactQuill will initialize the editor using `defaultValue`, but won't try to reset it after that. The `onChange` callback will still work as expected. +If you need to control what is considered a change you might use the `beforeChange` method which takes exactly the same arguments as the +`onChange` method but returns a boolean, a truthful value means that it is considered a change and `onChange` triggers afterwards updating +the internal state of the react component, otherwise it is considered that the old state is what remains true, despite the editor containing +an actual different value, this is very powerful when combined with the source type + Read more about uncontrolled components in the [React docs](https://facebook.github.io/react/docs/uncontrolled-components.html#default-values). ### Using Deltas @@ -117,6 +123,72 @@ Note that switching `value` from an HTML string to a Delta, or vice-versa, will ⚠️ Do not use the `delta` object you receive from the `onChange` event as `value`. This object does not contain the full document, but only the last modifications, and doing so will most likely trigger an infinite loop where the same changes are applied over and over again. Use `editor.getContents()` during the event to obtain a Delta of the full document instead. ReactQuill will prevent you from making such a mistake, however if you are absolutely sure that this is what you want, you can pass the object through `new Delta()` again to un-taint it. +### SSR + +Quill doesn't support SSR (Server side rendering), In order to use Server Side rendering you might be able to do so using this technique, unlike other techniques this one allows the same code to be rendered both in the server and client side but it needs a double pass, otherwise you will get a warning about content mismatch. + +~~~tsx +import OriginalReactQuill from "react-quill"; +let ReactQuill: typeof OriginalReactQuill; +if (!__SERVER__) { // this vairable must be populated by your builder, you might also check for document instead + ReactQuill = require("react-quill"); +} else { + const dead: any = () => null as any; + // you might need to add more functions here as you see fit, this is in order + // to be able to register blots, add the functionality that you use inside the + // dead function + ReactQuill = { + Quill: { + import: dead, + register: dead, + } + } as any; +} + +// You might have your custom blots and imports here +const BlockEmbed = ReactQuill.Quill.import("blots/block/embed"); +const Embed = ReactQuill.Quill.import("blots/embed"); +const Delta = ReactQuill.Quill.import("delta"); + +// etc... watch out that these can and will be null in the server, note +// that null is still extendable to build your custom blots even if they will +// do nothing in the server, it won't crash + +// should work just fine +class MyEmbed extends BlockEmbed { +} +~~~ + +However this is not enough, because this will cause an error when ReactQuill isn't a component if you try to use it that's why it must be used within a double pass, in your component that uses react-quill you must do the equivalent of + +~~~tsx +class MyComponent extends React.Component<{}, {isReady: boolean}> { + constructor(props: {}) { + super(props: {}); + + this.state = { + isReady: false, + } + } + + componentDidMount() { + this.setState({ + isReady: true, + }); + } + + render() { + return ( +
+ {this.isReady ? : true} +
+ ) + } +} +~~~ + +This will ensure that no `div` which contains react quill is rendered in the server side, and the client side will execute a double pass in it during the hydration process, mantaining consistency, this is the recommended way react uses. + ### Themes The Quill editor supports [themes](http://quilljs.com/docs/themes/). It includes a full-fledged theme, called _snow_, that is Quill's standard appearance, and a _bubble_ theme that is similar to the inline editor on Medium. At the very least, the _core_ theme must be included for modules like toolbars or tooltips to work. @@ -490,6 +562,64 @@ const {Quill} = ReactQuill; `children` : A single React element that will be used as the editing area for Quill in place of the default, which is a `
`. Note that you cannot use a `