From 169e5fd18f9d5c00bf50b1e25b6067e028516ddb Mon Sep 17 00:00:00 2001 From: jordanl17 Date: Thu, 2 May 2024 16:58:19 +0100 Subject: [PATCH] fix(pte): tools are active only when all blocks use the tool (#6524) * fix(pte): show toolbar as active only if all blocks have tool active * fix(pte): show toolbar as active only if all blocks have tool active * fix(pte): correct toggling of marks when part mark is selected * fix(core): change scrollintoview block to be nearest (#6328) * fix(core): set scroll boundary to nearest (#6310) * fix(core): set scroll boundary to nearest * fix(core): add smooth scroll behavior * fix(sanity): remove smooth behavior --------- Co-authored-by: RitaDias * fix(@sanity): issue where hidden unicode characters were bloating document in PTE (#6440) * fix(portable-text-editor): issue shown in tests re stega. use duplicate code * test(playwright-ct): add test * chore(sanity): remove prettier linting * test(sanity): fix missing snapshot * test(sanity): update test after realising the test would pass always if comparing object number * chore: test unicode removal * chore: test unicode removal * chore(@sanity): remove old solution * fix(@sanity/block-tools): unicode issue. remove vercel/stega and move to block-tools * test(@sanity/block-tools): for unicode * fix(@sanity/block-tools): utf8 characters weren't beign filtered. using the vercel/stega * chore: update lock file * (chore): update pnpm lock * chore: add codeowners to block-tools * chore(deps): dedupe pnpm-lock.yaml (#6508) Co-authored-by: github-merge-queue[bot] <118344674+github-merge-queue[bot]@users.noreply.github.com> * fix(pte): tidying implementation * fix(@sanity): issue where hidden unicode characters were bloating document in PTE (#6440) * fix(portable-text-editor): issue shown in tests re stega. use duplicate code * test(playwright-ct): add test * chore(sanity): remove prettier linting * test(sanity): fix missing snapshot * test(sanity): update test after realising the test would pass always if comparing object number * chore: test unicode removal * chore: test unicode removal * chore(@sanity): remove old solution * fix(@sanity/block-tools): unicode issue. remove vercel/stega and move to block-tools * test(@sanity/block-tools): for unicode * fix(@sanity/block-tools): utf8 characters weren't beign filtered. using the vercel/stega * chore: update lock file * (chore): update pnpm lock * chore(deps): dedupe pnpm-lock.yaml (#6508) Co-authored-by: github-merge-queue[bot] <118344674+github-merge-queue[bot]@users.noreply.github.com> * chore(deps): update dependency @sanity/pkg-utils to v6.8.8 (#6509) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * feat: use prefersLatestPublished parameter in DocumentPaneProvider (#6486) * fix(@sanity): issue where hidden unicode characters were bloating document in PTE (#6440) * fix(portable-text-editor): issue shown in tests re stega. use duplicate code * test(playwright-ct): add test * chore(sanity): remove prettier linting * test(sanity): fix missing snapshot * test(sanity): update test after realising the test would pass always if comparing object number * chore: test unicode removal * chore: test unicode removal * chore(@sanity): remove old solution * fix(@sanity/block-tools): unicode issue. remove vercel/stega and move to block-tools * test(@sanity/block-tools): for unicode * fix(@sanity/block-tools): utf8 characters weren't beign filtered. using the vercel/stega * chore: update lock file * (chore): update pnpm lock * chore(deps): dedupe pnpm-lock.yaml (#6508) Co-authored-by: github-merge-queue[bot] <118344674+github-merge-queue[bot]@users.noreply.github.com> * chore(deps): update dependency @sanity/pkg-utils to v6.8.8 (#6509) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(pte): simplifying activeAnnotations logic * fix(pte): new isAnnotationActive static method * fix(pte): retoring activeAnnotations method in editor * fix(pte): retoring activeAnnotations method in editor * fix(pte): fixing issue with multiple annotations in a single block * fix(pte): fixing issue with multiple annotations in a single block * fix(pte): fixing typing issue with PortableTextSpan * fix(pte): fixing failing test since mark toggling has changed * fix(pte): improving usage of isTextBlock for createWithPortableTextLists * fix(portable-text-editor): fix issue where decoration would not target correctly (#6532) * fix(pte): remove unused function in createWithPortableTextMarkModel * exploration * fix(pte): reverting incorrect merge * fix(pte): testing new logic for tools in PTE * fix(pte): reorg test cases --------- Co-authored-by: Nina Andal Aarvik Co-authored-by: RitaDias Co-authored-by: RitaDias Co-authored-by: Per-Kristian Nordnes Co-authored-by: ecospark[bot] <128108030+ecospark[bot]@users.noreply.github.com> Co-authored-by: github-merge-queue[bot] <118344674+github-merge-queue[bot]@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: cngonzalez --- .../src/editor/PortableTextEditor.tsx | 6 + .../__tests__/withPortableTextLists.test.tsx | 65 ++++++++ .../withPortableTextMarkModel.test.tsx | 139 +++++++++++++++--- .../editor/plugins/createWithEditableAPI.ts | 42 ++++++ .../plugins/createWithPortableTextLists.ts | 7 +- .../createWithPortableTextMarkModel.ts | 43 +++--- .../portable-text-editor/src/types/editor.ts | 1 + .../form/inputs/PortableText/toolbar/hooks.ts | 6 +- 8 files changed, 261 insertions(+), 48 deletions(-) create mode 100644 packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withPortableTextLists.test.tsx diff --git a/packages/@sanity/portable-text-editor/src/editor/PortableTextEditor.tsx b/packages/@sanity/portable-text-editor/src/editor/PortableTextEditor.tsx index ef83b7ff6ef..30c6c3d87f6 100644 --- a/packages/@sanity/portable-text-editor/src/editor/PortableTextEditor.tsx +++ b/packages/@sanity/portable-text-editor/src/editor/PortableTextEditor.tsx @@ -181,6 +181,12 @@ export class PortableTextEditor extends Component { static activeAnnotations = (editor: PortableTextEditor): PortableTextObject[] => { return editor && editor.editable ? editor.editable.activeAnnotations() : [] } + static isAnnotationActive = ( + editor: PortableTextEditor, + annotationType: PortableTextObject['_type'], + ): boolean => { + return editor && editor.editable ? editor.editable.isAnnotationActive(annotationType) : false + } static addAnnotation = ( editor: PortableTextEditor, type: ObjectSchemaType, diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withPortableTextLists.test.tsx b/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withPortableTextLists.test.tsx new file mode 100644 index 00000000000..c833b8cdda9 --- /dev/null +++ b/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withPortableTextLists.test.tsx @@ -0,0 +1,65 @@ +import {describe, expect, it, jest} from '@jest/globals' +import {render, waitFor} from '@testing-library/react' +import {createRef, type RefObject} from 'react' + +import {PortableTextEditorTester, schemaType} from '../../__tests__/PortableTextEditorTester' +import {PortableTextEditor} from '../../PortableTextEditor' + +describe('plugin:withPortableTextLists', () => { + it('should return active list styles that cover the whole selection', async () => { + const editorRef: RefObject = createRef() + const initialValue = [ + { + _key: 'a', + _type: 'myTestBlockType', + children: [ + { + _key: 'a1', + _type: 'span', + marks: [], + text: '12', + }, + ], + markDefs: [], + style: 'normal', + }, + { + _key: 'b', + _type: 'myTestBlockType', + children: [ + { + _key: '2', + _type: 'span', + marks: [], + text: '34', + level: 1, + listItem: 'bullet', + }, + ], + markDefs: [], + style: 'normal', + }, + ] + const onChange = jest.fn() + await waitFor(() => { + render( + , + ) + }) + const editor = editorRef.current! + expect(editor).toBeDefined() + await waitFor(() => { + PortableTextEditor.focus(editor) + PortableTextEditor.select(editor, { + focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 0}, + anchor: {path: [{_key: '2'}, 'children', {_key: '2'}], offset: 2}, + }) + expect(PortableTextEditor.hasListStyle(editor, 'bullet')).toBe(false) + }) + }) +}) diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx b/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx index 26f71609aca..46e965d726c 100644 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx +++ b/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx @@ -360,23 +360,25 @@ describe('plugin:withPortableTextMarksModel', () => { }) PortableTextEditor.toggleMark(editor, 'bold') expect(PortableTextEditor.getValue(editor)).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "a", - "_type": "myTestBlockType", - "children": Array [ - Object { - "_key": "a1", - "_type": "span", - "marks": Array [], - "text": "1234", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) +Array [ + Object { + "_key": "a", + "_type": "myTestBlockType", + "children": Array [ + Object { + "_key": "a1", + "_type": "span", + "marks": Array [ + "bold", + ], + "text": "1234", + }, + ], + "markDefs": Array [], + "style": "normal", + }, +] +`) } }) }) @@ -765,7 +767,110 @@ describe('plugin:withPortableTextMarksModel', () => { expect(currentSelectionObject === nextSelectionObject).toBe(false) expect(onChange).toHaveBeenCalledWith({type: 'selection', selection: nextSelectionObject}) }) + + it('should return active marks that cover the whole selection', async () => { + const editorRef: RefObject = createRef() + const initialValue = [ + { + _key: 'a', + _type: 'myTestBlockType', + children: [ + { + _key: 'a1', + _type: 'span', + marks: ['bold'], + text: '12', + }, + { + _key: '2', + _type: 'span', + marks: [], + text: '34', + }, + ], + markDefs: [{_key: 'bold', _type: 'strong'}], + style: 'normal', + }, + ] + const onChange = jest.fn() + await waitFor(() => { + render( + , + ) + }) + const editor = editorRef.current! + expect(editor).toBeDefined() + await waitFor(() => { + PortableTextEditor.focus(editor) + PortableTextEditor.select(editor, { + focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 0}, + anchor: {path: [{_key: 'a'}, 'children', {_key: '2'}], offset: 2}, + }) + expect(PortableTextEditor.isMarkActive(editor, 'bold')).toBe(false) + PortableTextEditor.toggleMark(editor, 'bold') + expect(PortableTextEditor.isMarkActive(editor, 'bold')).toBe(true) + }) + }) + + it('should return active annotation types that cover the whole selection', async () => { + const editorRef: RefObject = createRef() + const initialValue = [ + { + _key: 'a', + _type: 'myTestBlockType', + children: [ + { + _key: 'a1', + _type: 'span', + marks: ['bab319ad3a9d'], + text: '12', + }, + { + _key: '2', + _type: 'span', + marks: [], + text: '34', + }, + ], + markDefs: [ + { + _key: 'bab319ad3a9d', + _type: 'link', + href: 'http://www.123.com', + }, + ], + style: 'normal', + }, + ] + const onChange = jest.fn() + await waitFor(() => { + render( + , + ) + }) + const editor = editorRef.current! + expect(editor).toBeDefined() + await waitFor(() => { + PortableTextEditor.focus(editor) + PortableTextEditor.select(editor, { + focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 0}, + anchor: {path: [{_key: 'a'}, 'children', {_key: '2'}], offset: 2}, + }) + expect(PortableTextEditor.isAnnotationActive(editor, 'link')).toBe(false) + }) + }) }) + describe('removing annotations', () => { it('removes the markDefs if the annotation is no longer in use', async () => { const editorRef: RefObject = createRef() diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithEditableAPI.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithEditableAPI.ts index a17b6542f40..4f285c7e802 100644 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithEditableAPI.ts +++ b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithEditableAPI.ts @@ -1,4 +1,5 @@ import { + isPortableTextSpan, type ObjectSchemaType, type Path, type PortableTextBlock, @@ -295,6 +296,47 @@ export function createWithEditableAPI( return [] } }, + isAnnotationActive: (annotationType: PortableTextObject['_type']): boolean => { + if (!editor.selection || editor.selection.focus.path.length < 2) { + return false + } + + try { + const spans = [ + ...Editor.nodes(editor, { + at: editor.selection, + match: (node) => Text.isText(node), + }), + ] + + if ( + spans.some( + ([span]) => !isPortableTextSpan(span) || !span.marks || span.marks?.length === 0, + ) + ) + return false + + const selectionMarkDefs = spans.reduce((accMarkDefs, [, path]) => { + const [block] = Editor.node(editor, path, {depth: 1}) + if (editor.isTextBlock(block) && block.markDefs) { + return [...accMarkDefs, ...block.markDefs] + } + return accMarkDefs + }, [] as PortableTextObject[]) + + return spans.every(([span]) => { + if (!isPortableTextSpan(span)) return false + + const spanMarkDefs = span.marks?.map( + (markKey) => selectionMarkDefs.find((def) => def?._key === markKey)?._type, + ) + + return spanMarkDefs?.includes(annotationType) + }) + } catch (err) { + return false + } + }, addAnnotation: ( type: ObjectSchemaType, value?: {[prop: string]: unknown}, diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextLists.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextLists.ts index 08884655676..6f15eb4c39d 100644 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextLists.ts +++ b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextLists.ts @@ -143,11 +143,14 @@ export function createWithPortableTextLists(types: PortableTextMemberSchemaTypes const selectedBlocks = [ ...Editor.nodes(editor, { at: editor.selection, - match: (node) => editor.isListBlock(node) && node.listItem === listStyle, + match: (node) => editor.isTextBlock(node), }), ] + if (selectedBlocks.length > 0) { - return true + return selectedBlocks.every( + ([node]) => editor.isListBlock(node) && node.listItem === listStyle, + ) } return false } diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextMarkModel.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextMarkModel.ts index 5cf62c5d8e1..36d187d6a6e 100644 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextMarkModel.ts +++ b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextMarkModel.ts @@ -5,18 +5,9 @@ * */ -import {flatten, isEqual, uniq} from 'lodash' +import {isEqual, uniq} from 'lodash' import {type Subject} from 'rxjs' -import { - type Descendant, - Editor, - Element, - type NodeEntry, - Path, - Range, - Text, - Transforms, -} from 'slate' +import {type Descendant, Editor, Element, Path, Range, Text, Transforms} from 'slate' import { type EditorChange, @@ -250,9 +241,8 @@ export function createWithPortableTextMarkModel( const splitTextNodes = [ ...Editor.nodes(editor, {at: editor.selection, match: Text.isText}), ] - const shouldRemoveMark = flatten( - splitTextNodes.map((item) => item[0]).map((node) => node.marks), - ).includes(mark) + const shouldRemoveMark = splitTextNodes.every((node) => node[0].marks?.includes(mark)) + if (shouldRemoveMark) { editor.removeMark(mark) return editor @@ -345,19 +335,24 @@ export function createWithPortableTextMarkModel( if (!editor.selection) { return false } - let existingMarks = + + const selectedNodes = Array.from( + Editor.nodes(editor, {match: Text.isText, at: editor.selection}), + ) + + if (Range.isExpanded(editor.selection)) { + return selectedNodes.every((n) => { + const [node] = n + + return node.marks?.includes(mark) + }) + } + + return ( { ...(Editor.marks(editor) || {}), }.marks || [] - if (Range.isExpanded(editor.selection)) { - Array.from(Editor.nodes(editor, {match: Text.isText, at: editor.selection})).forEach( - (n) => { - const [node] = n as NodeEntry - existingMarks = uniq([...existingMarks, ...((node.marks as string[]) || [])]) - }, - ) - } - return existingMarks.includes(mark) + ).includes(mark) } // Custom editor function to toggle a mark diff --git a/packages/@sanity/portable-text-editor/src/types/editor.ts b/packages/@sanity/portable-text-editor/src/types/editor.ts index 023701bef0e..0ae1dc2e9d9 100644 --- a/packages/@sanity/portable-text-editor/src/types/editor.ts +++ b/packages/@sanity/portable-text-editor/src/types/editor.ts @@ -39,6 +39,7 @@ export interface EditableAPIDeleteOptions { /** @beta */ export interface EditableAPI { activeAnnotations: () => PortableTextObject[] + isAnnotationActive: (annotationType: PortableTextObject['_type']) => boolean addAnnotation: ( type: ObjectSchemaType, value?: {[prop: string]: unknown}, diff --git a/packages/sanity/src/core/form/inputs/PortableText/toolbar/hooks.ts b/packages/sanity/src/core/form/inputs/PortableText/toolbar/hooks.ts index 809c0a5112a..8fd694aec05 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/toolbar/hooks.ts +++ b/packages/sanity/src/core/form/inputs/PortableText/toolbar/hooks.ts @@ -77,14 +77,10 @@ export function useActiveActionKeys({ return useUnique( useMemo( () => { - const activeAnnotationKeys = PortableTextEditor.activeAnnotations(editor).map( - (a) => a._type, - ) - return actions .filter((a) => { if (a.type === 'annotation') { - return activeAnnotationKeys.includes(a.key) + return PortableTextEditor.isAnnotationActive(editor, a.key) } if (a.type === 'listStyle') {