diff --git a/docs/api/locations/range.md b/docs/api/locations/range.md index f9d770f343..fd024eef4a 100644 --- a/docs/api/locations/range.md +++ b/docs/api/locations/range.md @@ -52,7 +52,7 @@ Check if a `range` is exactly equal to `another`. Check if a `range` includes a path, a point, or part of another range. -For clarity the definition of `includes` can mean partially includes. Another way to describe this is if one Range intersectns the other Range. +For clarity the definition of `includes` can mean partially includes. Another way to describe this is if one Range intersects the other Range. #### `Range.isBackward(range: Range) => boolean` diff --git a/docs/general/faq.md b/docs/general/faq.md index 77d9ce01c8..0076db6809 100644 --- a/docs/general/faq.md +++ b/docs/general/faq.md @@ -9,7 +9,7 @@ A series of common questions people have about Slate: One of Slate's core principles is that, unlike most other editors, it does **not** prescribe a specific "schema" to the content you are editing. This means that Slate's core has no concept of "block quotes" or "bold formatting". -For the most part, this leads to increased flexbility without many downsides, but there are certain cases where you have to do a bit more work. Pasting is one of those cases. +For the most part, this leads to increased flexibility without many downsides, but there are certain cases where you have to do a bit more work. Pasting is one of those cases. Since Slate knows nothing about your domain, it can't know how to parse pasted HTML content \(or other content\). So, by default whenever a user pastes content into a Slate editor, it will parse it as plain text. If you want it to be smarter about pasted content, you need to override the `insert_data` command and deserialize the `DataTransfer` object's `text/html` data as you wish. diff --git a/docs/libraries/slate-react/editable.md b/docs/libraries/slate-react/editable.md index cd3b100a94..514c964fd6 100644 --- a/docs/libraries/slate-react/editable.md +++ b/docs/libraries/slate-react/editable.md @@ -25,8 +25,179 @@ type EditableProps = { } & React.TextareaHTMLAttributes ``` -_NOTE: Detailed breakdown of Props not completed. Refer to the source code at the moment. Under construction._ +_NOTE: Detailed breakdown of Props not completed. Refer to the [source code](https://github.com/ianstormtaylor/slate/blob/main/packages/slate-react/src/components/editable.tsx) at the moment. Under construction._ + +#### `placeholder?: string = ""` + +The text to display as a placeholder when the Editor is empty. A typical value for `placeholder` would be "Enter text here..." or "Start typing...". The placeholder text will not be treated as an actual value and will disappear when the user starts typing in the Editor. + +#### `readOnly?: boolean = false` + +When set to true, renders the editor in a "read-only" state. In this state, user input and interactions will not modify the editor's content. + +If this prop is omitted or set to false, the editor remains in the default "editable" state, allowing users to interact with and modify the content. + +This prop is particularly useful when you want to display text or rich media content without allowing users to edit it, such as when displaying published content or a preview of the user's input. + +#### `renderElement?: (props: RenderElementProps) => JSX.Element` + +The `renderElement` prop is a function used to render a custom component for a specific type of Element node in the Slate.js document model. + +Here is the type of the `RenderElementProps` passed into the function. + +```typescript +export interface RenderElementProps { + children: any + element: Element + attributes: { + 'data-slate-node': 'element' + 'data-slate-inline'?: true + 'data-slate-void'?: true + dir?: 'rtl' + ref: any + } +} +``` + +The `attributes` must be added to the props of the top level HTML element returned from the function and the `children` must be rendered somewhere inside the returned JSX. + +Here is a typical usage of `renderElement` with two types of elements. + +```javascript +const initialValue = [ + { + type: 'paragraph', + children: [{ text: 'A line of text in a paragraph.' }], + }, +] + +const App = () => { + const [editor] = useState(() => withReact(createEditor())) + + // Define a rendering function based on the element passed to `props`. We use + // `useCallback` here to memoize the function for subsequent renders. + const renderElement = useCallback(props => { + switch (props.element.type) { + case 'code': + return + default: + return + } + }, []) + + return ( + + + + ) +} + +const CodeElement = props => { + return ( +
+      {props.children}
+    
+ ) +} + +const DefaultElement = props => { + return

{props.children}

+} +``` + +#### `renderLeaf?: (props: RenderLeafProps) => JSX.Element` + +The `renderLeaf` prop allows you to customize the rendering of leaf nodes in the document tree of your Slate editor. A "leaf" in Slate is the smallest chunk of text and its associated formatting attributes. + +The `renderLeaf` function receives an object of type `RenderLeafProps` as its argument: + +```typescript +export interface RenderLeafProps { + children: any + leaf: Text + text: Text + attributes: { + 'data-slate-leaf': true + } +} +``` + +Example usage: + +```typescript + { + return ( + + {children} + + ) + }} +/> +``` + +#### `renderPlaceholder?: (props: RenderPlaceholderProps) => JSX.Element` + +The `renderPlaceholder` prop allows you to customize how the placeholder of the Slate.js `Editable` component is rendered when the editor is empty. The placeholder will only be shown when the editor's content is empty. + +The `RenderPlaceholderProps` interface looks like this: + +```typescript +export type RenderPlaceholderProps = { + children: any + attributes: { + 'data-slate-placeholder': boolean + dir?: 'rtl' + contentEditable: boolean + ref: React.RefCallback + style: React.CSSProperties + } +} +``` + +An example usage might look like: + +```jsx + ( +
+ {children} +
+ )} +/> +``` #### `scrollSelectionIntoView?: (editor: ReactEditor, domRange: DOMRange) => void` Slate has its own default method to scroll a DOM selection into view that works for most cases; however, if the default behavior isn't working for you, possible due to some complex styling, you may need to override the default behavior by providing a different function here. + +#### `as?: React.ElementType = "div"` + +The as prop specifies the type of element that will be used to render the Editable component in your React application. By default, this is a `div`. + +#### `disableDefaultStyles?: boolean = false` + +The `disableDefaultStyles` prop determines whether the default styles of the Slate.js `Editable` component are applied or not. + +Please note that with this prop set to `true`, you will need to ensure that your styles cater to all the functionalities of the editor that rely on specific styles to work properly. + +Here are the default styles: + +```typescript +const defaultStyles = { + // Allow positioning relative to the editable element. + position: 'relative', + // Preserve adjacent whitespace and new lines. + whiteSpace: 'pre-wrap', + // Allow words to break if they are too long. + wordWrap: 'break-word', + // Make the minimum height that of the placeholder. + ...(placeholderHeight ? { minHeight: placeholderHeight } : {}), +} +``` diff --git a/docs/libraries/slate-react/react-editor.md b/docs/libraries/slate-react/react-editor.md index ad0841eeeb..0e73a5d4af 100644 --- a/docs/libraries/slate-react/react-editor.md +++ b/docs/libraries/slate-react/react-editor.md @@ -10,7 +10,7 @@ const [editor] = useState(() => withReact(withHistory(createEditor()))) - [Check methods](react-editor.md#check-methods) - [Focus and selection methods](react-editor.md#focus-and-selection-methods) - [DOM translation methods](react-editor.md#dom-translation-methods) - - [DataTranfer methods](react-editor.md#datatransfer-methods) + - [DataTransfer methods](react-editor.md#datatransfer-methods) ## Static methods diff --git a/docs/libraries/slate-react/with-react.md b/docs/libraries/slate-react/with-react.md index 616c82718f..e8f35a9376 100644 --- a/docs/libraries/slate-react/with-react.md +++ b/docs/libraries/slate-react/with-react.md @@ -14,4 +14,4 @@ const [editor] = useState(() => withReact(withHistory(createEditor()))) The `clipboardFormatKey` option allows you to customize the `DataTransfer` type when Slate data is copied to the clipboard. By default, it is `application/x-slate-fragment` but it can be customized using this option. -This can be useful when a user copies from one Slate editor to a differently configured Slate editor. This could cause nodes to be inserted which are not correctly typed for the receiving editor, corrupting the document. By customizing the `clipboardFormatKey` one can ensure that the raw JSON data isn't cpied between editors with different schemas. +This can be useful when a user copies from one Slate editor to a differently configured Slate editor. This could cause nodes to be inserted which are not correctly typed for the receiving editor, corrupting the document. By customizing the `clipboardFormatKey` one can ensure that the raw JSON data isn't copied between editors with different schemas. diff --git a/packages/slate-react/CHANGELOG.md b/packages/slate-react/CHANGELOG.md index 907f6e61f9..564565da5f 100644 --- a/packages/slate-react/CHANGELOG.md +++ b/packages/slate-react/CHANGELOG.md @@ -1,5 +1,25 @@ # slate-react +## 0.98.1 + +### Patch Changes + +- [#5491](https://github.com/ianstormtaylor/slate/pull/5491) [`a5576e56`](https://github.com/ianstormtaylor/slate/commit/a5576e56a73f061972775953f270b34081a5cad8) Thanks [@WcaleNieWolny](https://github.com/WcaleNieWolny)! - Fix firefox table selection if table is contentedtiable + +## 0.98.0 + +### Minor Changes + +- [#5486](https://github.com/ianstormtaylor/slate/pull/5486) [`8b548fb5`](https://github.com/ianstormtaylor/slate/commit/8b548fb53af861e1f391f2d5c052e3279f0a0b6c) Thanks [@WcaleNieWolny](https://github.com/WcaleNieWolny)! - Fix invalid usage of the selection API in firefox + +## 0.97.2 + +### Patch Changes + +- [#5462](https://github.com/ianstormtaylor/slate/pull/5462) [`a6b606d8`](https://github.com/ianstormtaylor/slate/commit/a6b606d804795d9b134784a35e3b00ac77f3ebbc) Thanks [@Ben-Wormald](https://github.com/Ben-Wormald)! - Update hotkeys util to use isHotkey for better support for non-latin keyboards + +* [#5470](https://github.com/ianstormtaylor/slate/pull/5470) [`4bd15ed3`](https://github.com/ianstormtaylor/slate/commit/4bd15ed3950e3a0871f5d0ecb391bb637c05e59d) Thanks [@josephmr](https://github.com/josephmr)! - Fix Android caret placement regression when inputting into empty editor + ## 0.97.1 ### Patch Changes diff --git a/packages/slate-react/package.json b/packages/slate-react/package.json index 0df6897f69..435b29c717 100644 --- a/packages/slate-react/package.json +++ b/packages/slate-react/package.json @@ -1,7 +1,7 @@ { "name": "slate-react", "description": "Tools for building completely customizable richtext editors with React.", - "version": "0.97.1", + "version": "0.98.1", "license": "MIT", "repository": "git://github.com/ianstormtaylor/slate.git", "main": "dist/index.js", diff --git a/packages/slate-react/src/components/editable.tsx b/packages/slate-react/src/components/editable.tsx index a45104eb73..85888e5999 100644 --- a/packages/slate-react/src/components/editable.tsx +++ b/packages/slate-react/src/components/editable.tsx @@ -321,12 +321,33 @@ export const Editable = (props: EditableProps) => { return } + // Get anchorNode and focusNode + const focusNode = domSelection.focusNode + let anchorNode + + // COMPAT: In firefox the normal seletion way does not work + // (https://github.com/ianstormtaylor/slate/pull/5486#issue-1820720223) + if (IS_FIREFOX && domSelection.rangeCount > 1) { + const firstRange = domSelection.getRangeAt(0) + const lastRange = domSelection.getRangeAt(domSelection.rangeCount - 1) + + // Right to left + if (firstRange.startContainer === focusNode) { + anchorNode = lastRange.endContainer + } else { + // Left to right + anchorNode = firstRange.startContainer + } + } else { + anchorNode = domSelection.anchorNode + } + // verify that the dom selection is in the editor const editorElement = EDITOR_TO_ELEMENT.get(editor)! let hasDomSelectionInEditor = false if ( - editorElement.contains(domSelection.anchorNode) && - editorElement.contains(domSelection.focusNode) + editorElement.contains(anchorNode) && + editorElement.contains(focusNode) ) { hasDomSelectionInEditor = true } @@ -352,7 +373,6 @@ export const Editable = (props: EditableProps) => { } // Ensure selection is inside the mark placeholder - const { anchorNode } = domSelection if ( anchorNode?.parentElement?.hasAttribute( 'data-slate-mark-placeholder' @@ -383,7 +403,7 @@ export const Editable = (props: EditableProps) => { selection && ReactEditor.toDOMRange(editor, selection) if (newDomRange) { - if (ReactEditor.isComposing(editor)) { + if (ReactEditor.isComposing(editor) && !IS_ANDROID) { domSelection.collapseToEnd() } else if (Range.isBackward(selection!)) { domSelection.setBaseAndExtent( @@ -408,27 +428,16 @@ export const Editable = (props: EditableProps) => { return newDomRange } - const newDomRange = setDomSelection() + // In firefox if there is more then 1 range and we call setDomSelection we remove the ability to select more cells in a table + if (domSelection.rangeCount <= 1) { + setDomSelection() + } + const ensureSelection = androidInputManagerRef.current?.isFlushing() === 'action' if (!IS_ANDROID || !ensureSelection) { setTimeout(() => { - // COMPAT: In Firefox, it's not enough to create a range, you also need - // to focus the contenteditable element too. (2016/11/16) - if (newDomRange && IS_FIREFOX) { - const el = ReactEditor.toDOMNode(editor, editor) - if (!el) { - editor.onError({ - key: 'Editable.toDOMNode', - message: 'Unable to find a DOM node', - }) - return - } - - el.focus() - } - state.isUpdatingSelection = false }) return diff --git a/packages/slate-react/src/plugin/react-editor.ts b/packages/slate-react/src/plugin/react-editor.ts index 3f0be824d0..2b5bfc9a33 100644 --- a/packages/slate-react/src/plugin/react-editor.ts +++ b/packages/slate-react/src/plugin/react-editor.ts @@ -1005,15 +1005,87 @@ export const ReactEditor: ReactEditorInterface = { if (el) { if (isDOMSelection(domRange)) { - anchorNode = domRange.anchorNode - anchorOffset = domRange.anchorOffset - focusNode = domRange.focusNode - focusOffset = domRange.focusOffset + // COMPAT: In firefox the normal seletion way does not work + // (https://github.com/ianstormtaylor/slate/pull/5486#issue-1820720223) + if (IS_FIREFOX && domRange.rangeCount > 1) { + focusNode = domRange.focusNode // Focus node works fine + const firstRange = domRange.getRangeAt(0) + const lastRange = domRange.getRangeAt(domRange.rangeCount - 1) + + // Here we are in the contenteditable mode of a table in firefox + if ( + focusNode instanceof HTMLTableRowElement && + firstRange.startContainer instanceof HTMLTableRowElement && + lastRange.startContainer instanceof HTMLTableRowElement + ) { + // HTMLElement, becouse Element is a slate element + function getLastChildren(element: HTMLElement): HTMLElement { + if (element.childElementCount > 0) { + return getLastChildren(element.children[0]) + } else { + return element + } + } + + const firstNodeRow = firstRange.startContainer + const lastNodeRow = lastRange.startContainer + + // This should never fail as "The HTMLElement interface represents any HTML element." + const firstNode = getLastChildren( + firstNodeRow.children[firstRange.startOffset] + ) + const lastNode = getLastChildren( + lastNodeRow.children[lastRange.startOffset] + ) + + // Zero, as we allways take the right one as the anchor point + focusOffset = 0 + + if (lastNode.childNodes.length > 0) { + anchorNode = lastNode.childNodes[0] + } else { + anchorNode = lastNode + } + + if (firstNode.childNodes.length > 0) { + focusNode = firstNode.childNodes[0] + } else { + focusNode = firstNode + } + + if (lastNode instanceof HTMLElement) { + anchorOffset = (lastNode).innerHTML.length + } else { + // Fallback option + anchorOffset = 0 + } + } else { + // This is the read only mode of a firefox table + // Right to left + if (firstRange.startContainer === focusNode) { + anchorNode = lastRange.endContainer + anchorOffset = lastRange.endOffset + focusOffset = firstRange.startOffset + } else { + // Left to right + anchorNode = firstRange.startContainer + anchorOffset = firstRange.endOffset + focusOffset = lastRange.startOffset + } + } + } else { + anchorNode = domRange.anchorNode + anchorOffset = domRange.anchorOffset + focusNode = domRange.focusNode + focusOffset = domRange.focusOffset + } + // COMPAT: There's a bug in chrome that always returns `true` for // `isCollapsed` for a Selection that comes from a ShadowRoot. // (2020/08/08) // https://bugs.chromium.org/p/chromium/issues/detail?id=447523 - if (IS_CHROME && hasShadowRoot(anchorNode)) { + // IsCollapsed might not work in firefox, but this will + if ((IS_CHROME && hasShadowRoot(anchorNode)) || IS_FIREFOX) { isCollapsed = domRange.anchorNode === domRange.focusNode && domRange.anchorOffset === domRange.focusOffset @@ -1054,15 +1126,19 @@ export const ReactEditor: ReactEditorInterface = { focusOffset = anchorNode.textContent?.length || 0 } - let anchor = ReactEditor.toSlatePoint(editor, [anchorNode, anchorOffset], { - exactMatch, - suppressThrow, - }) + const anchor = ReactEditor.toSlatePoint( + editor, + [anchorNode, anchorOffset], + { + exactMatch, + suppressThrow, + } + ) if (!anchor) { return null as T extends true ? Range | null : Range } - let focus = isCollapsed + const focus = isCollapsed ? anchor : ReactEditor.toSlatePoint(editor, [focusNode, focusOffset], { exactMatch, @@ -1072,46 +1148,6 @@ export const ReactEditor: ReactEditorInterface = { return null as T extends true ? Range | null : Range } - /** - * suppose we have this document: - * - * { type: 'paragraph', - * children: [ - * { text: 'foo ' }, - * { text: 'bar' }, - * { text: ' baz' } - * ] - * } - * - * a double click on "bar" on chrome will create this range: - * - * anchor -> [0,1] offset 0 - * focus -> [0,1] offset 3 - * - * while on firefox will create this range: - * - * anchor -> [0,0] offset 4 - * focus -> [0,2] offset 0 - * - * let's try to fix it... - */ - - if (IS_FIREFOX && !isCollapsed && anchorNode !== focusNode) { - const isEnd = Editor.isEnd(editor, anchor!, anchor.path) - const isStart = Editor.isStart(editor, focus!, focus.path) - - if (isEnd) { - const after = Editor.after(editor, anchor as Point) - // Editor.after() might return undefined - anchor = (after || anchor!) as T extends true ? Point | null : Point - } - - if (isStart) { - const before = Editor.before(editor, focus as Point) - focus = (before || focus!) as T extends true ? Point | null : Point - } - } - let range: Range = { anchor: anchor as Point, focus: focus as Point } // if the selection is a hanging range that ends in a void // and the DOM focus is an Element diff --git a/packages/slate-react/src/utils/hotkeys.ts b/packages/slate-react/src/utils/hotkeys.ts index 65de16d683..c4efaeb478 100644 --- a/packages/slate-react/src/utils/hotkeys.ts +++ b/packages/slate-react/src/utils/hotkeys.ts @@ -1,4 +1,4 @@ -import { isKeyHotkey } from 'is-hotkey' +import { isHotkey } from 'is-hotkey' import { IS_APPLE } from './environment' /** @@ -53,9 +53,9 @@ const create = (key: string) => { const generic = HOTKEYS[key] const apple = APPLE_HOTKEYS[key] const windows = WINDOWS_HOTKEYS[key] - const isGeneric = generic && isKeyHotkey(generic) - const isApple = apple && isKeyHotkey(apple) - const isWindows = windows && isKeyHotkey(windows) + const isGeneric = generic && isHotkey(generic) + const isApple = apple && isHotkey(apple) + const isWindows = windows && isHotkey(windows) return (event: KeyboardEvent) => { if (isGeneric && isGeneric(event)) return true