diff --git a/demo/package.json b/demo/package.json index 70b8f30b8..73e98c815 100644 --- a/demo/package.json +++ b/demo/package.json @@ -15,6 +15,7 @@ "playwright:clear": "rm -rf ./tests/playwright/.cache", "playwright:report": "playwright show-report playwright-report", "playwright:docker": "./scripts/playwright-docker.sh test", + "playwright:docker:ui": "./scripts/playwright-docker.sh test --ui-port 8082 --ui-host 0.0.0.0", "playwright:docker:update": "./scripts/playwright-docker.sh update", "playwright:docker:clear": "./scripts/playwright-docker.sh clear", "playwright:docker:report": "playwright show-report playwright-report-docker" diff --git a/demo/scripts/playwright-docker.sh b/demo/scripts/playwright-docker.sh index e0be198ad..992ec5b19 100755 --- a/demo/scripts/playwright-docker.sh +++ b/demo/scripts/playwright-docker.sh @@ -20,7 +20,7 @@ command_exists() { } run_command() { - $CONTAINER_TOOL run --rm --network host -it -w /work \ + $CONTAINER_TOOL run --rm -it -w /work \ --platform linux/arm64 \ --ipc=host \ -v $(pwd):/work \ @@ -28,6 +28,7 @@ run_command() { -v "$PNPM_STORE_CACHE_DIR:/root/.local/share/pnpm/store" \ -e IS_DOCKER=1 \ -e NODE_OPTIONS="--max-old-space-size=8192" \ + -p 8082:8082 \ "$IMAGE_NAME:$IMAGE_TAG" \ /bin/bash -c "$*" } diff --git a/demo/src/components/Playground.tsx b/demo/src/components/Playground.tsx index 3bfa7b486..9fec366ed 100644 --- a/demo/src/components/Playground.tsx +++ b/demo/src/components/Playground.tsx @@ -16,6 +16,7 @@ import { type ToolbarsPreset, type UseMarkdownEditorProps, type WysiwygPlaceholderOptions, + type YfmMods, logger, useMarkdownEditor, wysiwygToolbarConfigs, @@ -92,6 +93,7 @@ export type PlaygroundProps = { markupParseHtmlOnPaste?: boolean; style?: React.CSSProperties; storyAdditionalControls?: Record; + yfmMods?: YfmMods; } & Pick & Pick< MarkdownEditorViewProps, @@ -149,6 +151,7 @@ export const Playground = memo((props) => { markupParseHtmlOnPaste, style, storyAdditionalControls, + yfmMods, } = props; const [editorMode, setEditorMode] = useState(initialEditor ?? 'wysiwyg'); const [mdRaw, setMdRaw] = useState(initial || ''); @@ -246,6 +249,9 @@ export const Playground = memo((props) => { if (wysiwygConfig?.extensions) builder.use(wysiwygConfig.extensions); }, extensionOptions: { + yfmConfigs: { + mods: yfmMods, + }, checkbox: {multiline: true}, commandMenu: {actions: wysiwygCommandMenuConfig ?? wCommandMenuConfig}, imgSize: { @@ -255,6 +261,7 @@ export const Playground = memo((props) => { lineWrapping: {enabled: true}, }, yfmTable: { + headerRows: true, table_ignoreSplittersInBlockCode: true, table_ignoreSplittersInBlockMath: true, table_ignoreSplittersInInlineCode: true, diff --git a/demo/src/stories/examples/yfm-table-dnd/YfmTableDnD.stories.tsx b/demo/src/stories/examples/yfm-table-dnd/YfmTableDnD.stories.tsx index 17339425c..7efabb63d 100644 --- a/demo/src/stories/examples/yfm-table-dnd/YfmTableDnD.stories.tsx +++ b/demo/src/stories/examples/yfm-table-dnd/YfmTableDnD.stories.tsx @@ -6,6 +6,7 @@ export const Story: StoryObj = { args: { mobile: false, dnd: true, + headerRows: true, }, }; Story.storyName = "YFM Table D'n'D"; diff --git a/demo/src/stories/examples/yfm-table-dnd/YfmTableDnD.tsx b/demo/src/stories/examples/yfm-table-dnd/YfmTableDnD.tsx index 6c05865ca..1df7cbbd8 100644 --- a/demo/src/stories/examples/yfm-table-dnd/YfmTableDnD.tsx +++ b/demo/src/stories/examples/yfm-table-dnd/YfmTableDnD.tsx @@ -9,9 +9,14 @@ import {markup} from './markup'; export type YfmTableDnDDemoProps = { mobile: boolean; dnd: boolean; + headerRows: boolean; }; -export const YfmTableDnDDemo = memo(function YfmTableDnDDemo({mobile, dnd}) { +export const YfmTableDnDDemo = memo(function YfmTableDnDDemo({ + mobile, + dnd, + headerRows, +}) { const editor = useMarkdownEditor( { mobile, @@ -21,13 +26,17 @@ export const YfmTableDnDDemo = memo(function YfmTableDnDDe }, wysiwygConfig: { extensionOptions: { + yfmConfigs: { + mods: {'no-stripe-table': true}, + }, yfmTable: { dnd, + headerRows, }, }, }, }, - [mobile, dnd], + [mobile, dnd, headerRows], ); return ( diff --git a/demo/tests/playwright/core/editor.ts b/demo/tests/playwright/core/editor.ts index 4d0c0360a..ce1fad677 100644 --- a/demo/tests/playwright/core/editor.ts +++ b/demo/tests/playwright/core/editor.ts @@ -10,7 +10,8 @@ type YfmTableActionKind = | 'add-column-before' | 'add-column-after' | 'add-row-before' - | 'add-row-after'; + | 'add-row-after' + | 'header-toggle'; type MarkdownEditorToolbarsLocators = Record< 'main' | 'additional' | 'selection' | 'commandMenu', @@ -167,6 +168,7 @@ class YfmTable { 'remove-column': page.getByTestId('g-md-yfm-table-action-remove-column'), 'remove-row': page.getByTestId('g-md-yfm-table-action-remove-row'), 'remove-table': page.getByTestId('g-md-yfm-table-action-remove-table'), + 'header-toggle': page.getByTestId('g-md-yfm-table-row-header-toggle'), }; } @@ -174,6 +176,10 @@ class YfmTable { return this.cellMenus[type]; } + getCellActionLocator(menuType: YfmTableCellMenuType, kind: YfmTableActionKind) { + return this.cellMenus[menuType].locator(this.cellMenuActions[kind]); + } + async getTable(locator?: Locator) { return locator?.locator(this.tableLocator) ?? this.tableLocator; } @@ -183,7 +189,9 @@ class YfmTable { } async getCells(table?: Locator) { - return (table || (await this.getTable())).first().locator('> tbody > tr > td'); + return (table || (await this.getTable())) + .first() + .locator('> tbody > tr > th, > tbody > tr > td'); } async getRowButtons(_table?: Locator) { diff --git a/demo/tests/visual-tests/__snapshots__/YfmTable.visual.test.tsx-snapshots/YfmTable-cell-menu-wysiwyg-row-menu-dark-chromium-linux.png b/demo/tests/visual-tests/__snapshots__/YfmTable.visual.test.tsx-snapshots/YfmTable-cell-menu-wysiwyg-row-menu-dark-chromium-linux.png index cae0d4dd0..37d0594f1 100644 Binary files a/demo/tests/visual-tests/__snapshots__/YfmTable.visual.test.tsx-snapshots/YfmTable-cell-menu-wysiwyg-row-menu-dark-chromium-linux.png and b/demo/tests/visual-tests/__snapshots__/YfmTable.visual.test.tsx-snapshots/YfmTable-cell-menu-wysiwyg-row-menu-dark-chromium-linux.png differ diff --git a/demo/tests/visual-tests/__snapshots__/YfmTable.visual.test.tsx-snapshots/YfmTable-cell-menu-wysiwyg-row-menu-light-chromium-linux.png b/demo/tests/visual-tests/__snapshots__/YfmTable.visual.test.tsx-snapshots/YfmTable-cell-menu-wysiwyg-row-menu-light-chromium-linux.png index bfb8c2fb6..9ef3582d5 100644 Binary files a/demo/tests/visual-tests/__snapshots__/YfmTable.visual.test.tsx-snapshots/YfmTable-cell-menu-wysiwyg-row-menu-light-chromium-linux.png and b/demo/tests/visual-tests/__snapshots__/YfmTable.visual.test.tsx-snapshots/YfmTable-cell-menu-wysiwyg-row-menu-light-chromium-linux.png differ diff --git a/demo/tests/visual-tests/__snapshots__/YfmTable.visual.test.tsx-snapshots/YfmTable-header-rows-wysiwyg-should-make-all-rows-header-when-first-cell-rowspan-covers-entire-table-dark-chromium-linux.png b/demo/tests/visual-tests/__snapshots__/YfmTable.visual.test.tsx-snapshots/YfmTable-header-rows-wysiwyg-should-make-all-rows-header-when-first-cell-rowspan-covers-entire-table-dark-chromium-linux.png new file mode 100644 index 000000000..4b9cf6343 Binary files /dev/null and b/demo/tests/visual-tests/__snapshots__/YfmTable.visual.test.tsx-snapshots/YfmTable-header-rows-wysiwyg-should-make-all-rows-header-when-first-cell-rowspan-covers-entire-table-dark-chromium-linux.png differ diff --git a/demo/tests/visual-tests/__snapshots__/YfmTable.visual.test.tsx-snapshots/YfmTable-header-rows-wysiwyg-should-make-all-rows-header-when-first-cell-rowspan-covers-entire-table-light-chromium-linux.png b/demo/tests/visual-tests/__snapshots__/YfmTable.visual.test.tsx-snapshots/YfmTable-header-rows-wysiwyg-should-make-all-rows-header-when-first-cell-rowspan-covers-entire-table-light-chromium-linux.png new file mode 100644 index 000000000..fbb8b84d5 Binary files /dev/null and b/demo/tests/visual-tests/__snapshots__/YfmTable.visual.test.tsx-snapshots/YfmTable-header-rows-wysiwyg-should-make-all-rows-header-when-first-cell-rowspan-covers-entire-table-light-chromium-linux.png differ diff --git a/demo/tests/visual-tests/__snapshots__/YfmTable.visual.test.tsx-snapshots/YfmTable-header-rows-wysiwyg-should-set-header-on-the-first-row-dark-chromium-linux.png b/demo/tests/visual-tests/__snapshots__/YfmTable.visual.test.tsx-snapshots/YfmTable-header-rows-wysiwyg-should-set-header-on-the-first-row-dark-chromium-linux.png new file mode 100644 index 000000000..0504a61eb Binary files /dev/null and b/demo/tests/visual-tests/__snapshots__/YfmTable.visual.test.tsx-snapshots/YfmTable-header-rows-wysiwyg-should-set-header-on-the-first-row-dark-chromium-linux.png differ diff --git a/demo/tests/visual-tests/__snapshots__/YfmTable.visual.test.tsx-snapshots/YfmTable-header-rows-wysiwyg-should-set-header-on-the-first-row-light-chromium-linux.png b/demo/tests/visual-tests/__snapshots__/YfmTable.visual.test.tsx-snapshots/YfmTable-header-rows-wysiwyg-should-set-header-on-the-first-row-light-chromium-linux.png new file mode 100644 index 000000000..d512a43a6 Binary files /dev/null and b/demo/tests/visual-tests/__snapshots__/YfmTable.visual.test.tsx-snapshots/YfmTable-header-rows-wysiwyg-should-set-header-on-the-first-row-light-chromium-linux.png differ diff --git a/demo/tests/visual-tests/__snapshots__/YfmTable.visual.test.tsx-snapshots/YfmTable-header-rows-wysiwyg-should-unset-header-on-the-first-row-dark-chromium-linux.png b/demo/tests/visual-tests/__snapshots__/YfmTable.visual.test.tsx-snapshots/YfmTable-header-rows-wysiwyg-should-unset-header-on-the-first-row-dark-chromium-linux.png new file mode 100644 index 000000000..1176a1587 Binary files /dev/null and b/demo/tests/visual-tests/__snapshots__/YfmTable.visual.test.tsx-snapshots/YfmTable-header-rows-wysiwyg-should-unset-header-on-the-first-row-dark-chromium-linux.png differ diff --git a/demo/tests/visual-tests/__snapshots__/YfmTable.visual.test.tsx-snapshots/YfmTable-header-rows-wysiwyg-should-unset-header-on-the-first-row-light-chromium-linux.png b/demo/tests/visual-tests/__snapshots__/YfmTable.visual.test.tsx-snapshots/YfmTable-header-rows-wysiwyg-should-unset-header-on-the-first-row-light-chromium-linux.png new file mode 100644 index 000000000..39f250be8 Binary files /dev/null and b/demo/tests/visual-tests/__snapshots__/YfmTable.visual.test.tsx-snapshots/YfmTable-header-rows-wysiwyg-should-unset-header-on-the-first-row-light-chromium-linux.png differ diff --git a/demo/tests/visual-tests/playground/YfmTable.visual.test.tsx b/demo/tests/visual-tests/playground/YfmTable.visual.test.tsx index 48a12ecde..b40c36a24 100644 --- a/demo/tests/visual-tests/playground/YfmTable.visual.test.tsx +++ b/demo/tests/visual-tests/playground/YfmTable.visual.test.tsx @@ -1,3 +1,4 @@ +import type {YfmMods} from '@gravity-ui/markdown-editor'; import dd from 'ts-dedent'; import {expect, test} from 'playwright/core'; @@ -507,4 +508,314 @@ test.describe('YfmTable', () => { return texts.map((str) => str.trimEnd()); } }); + + test.describe('header rows @wysiwyg', () => { + const yfmMods: YfmMods = {'no-stripe-table': true}; + + test('should set header on the first row', async ({ + page, + wait, + mount, + editor, + expectScreenshot, + }) => { + const initial = dd` + #| + || one | two || + || three | four || + |# + `; + + await mount(); + + const tableLocator = ( + await editor.yfmTable.getTable(editor.locators.contenteditable) + ).first(); + const rowsLocator = await editor.yfmTable.getRows(tableLocator); + const cellsLocator = await editor.yfmTable.getCells(tableLocator); + const firstCell = cellsLocator.first(); + const rowButton = (await editor.yfmTable.getRowButtons(tableLocator)).first(); + + await editor.yfmTable.focusFirstCell(tableLocator); + await firstCell.hover(); + await rowButton.click(); + await editor.yfmTable.doCellAction('row', 'header-toggle'); + + await expect(rowsLocator.nth(0)).toHaveAttribute('data-header', 'true'); + await expect(rowsLocator.nth(1)).not.toHaveAttribute('data-header', 'true'); + + await editor.focus(); + await page.keyboard.press('Escape'); + await editor.blur(); + + await page.mouse.move(-50, -50); + await wait.timeout(500); + await expectScreenshot(); + }); + + test('should unset header on the first row', async ({ + page, + wait, + mount, + editor, + expectScreenshot, + }) => { + const initial = dd` + #| + |:{header-rows="1"} + || one | two || + || three | four || + |# + `; + + await mount(); + + const tableLocator = ( + await editor.yfmTable.getTable(editor.locators.contenteditable) + ).first(); + const rowsLocator = await editor.yfmTable.getRows(tableLocator); + const cellsLocator = await editor.yfmTable.getCells(tableLocator); + const firstCell = cellsLocator.first(); + const rowButton = (await editor.yfmTable.getRowButtons(tableLocator)).first(); + + await expect(rowsLocator.nth(0)).toHaveAttribute('data-header', 'true'); + + await editor.yfmTable.focusFirstCell(tableLocator); + await firstCell.hover(); + await rowButton.click(); + await editor.yfmTable.doCellAction('row', 'header-toggle'); + + await expect(rowsLocator.nth(0)).not.toHaveAttribute('data-header', 'true'); + await expect(rowsLocator.nth(1)).not.toHaveAttribute('data-header', 'true'); + + await editor.focus(); + await page.keyboard.press('Escape'); + await editor.blur(); + + await page.mouse.move(-50, -50); + await wait.timeout(500); + await expectScreenshot(); + }); + + test('should make 2nd row a header when rowspan from row 0 covers row 1', async ({ + wait, + mount, + editor, + }) => { + const initial = dd` + #| + |:{header-rows="1"} + || one | two || + || three | ^ || + || five | six || + |# + `; + + await mount(); + + const tableLocator = ( + await editor.yfmTable.getTable(editor.locators.contenteditable) + ).first(); + const rowsLocator = await editor.yfmTable.getRows(tableLocator); + const cellsLocator = await editor.yfmTable.getCells(tableLocator); + const rowButton = (await editor.yfmTable.getRowButtons(tableLocator)).first(); + + await expect(rowsLocator.nth(0)).toHaveAttribute('data-header', 'true'); + await expect(rowsLocator.nth(1)).not.toHaveAttribute('data-header', 'true'); + + await editor.yfmTable.focusFirstCell(tableLocator); + + // hover the first cell of row 1 (column 0) + await cellsLocator.nth(2).hover(); + await wait.timeout(200); + await rowButton.click(); + await editor.yfmTable.doCellAction('row', 'header-toggle'); + + await expect(rowsLocator.nth(0)).toHaveAttribute('data-header', 'true'); + await expect(rowsLocator.nth(1)).toHaveAttribute('data-header', 'true'); + await expect(rowsLocator.nth(2)).not.toHaveAttribute('data-header', 'true'); + }); + + test('should make all rows header when first cell rowspan covers entire table', async ({ + page, + wait, + mount, + editor, + expectScreenshot, + }) => { + const initial = dd` + #| + || one | two || + || ^ | four || + || ^ | six || + |# + `; + + await mount(); + + const tableLocator = ( + await editor.yfmTable.getTable(editor.locators.contenteditable) + ).first(); + const rowsLocator = await editor.yfmTable.getRows(tableLocator); + const cellsLocator = await editor.yfmTable.getCells(tableLocator); + const firstCell = cellsLocator.first(); + const rowButton = (await editor.yfmTable.getRowButtons(tableLocator)).first(); + + await editor.yfmTable.focusFirstCell(tableLocator); + await firstCell.hover(); + await rowButton.click(); + await editor.yfmTable.doCellAction('row', 'header-toggle'); + + await expect(rowsLocator.nth(0)).toHaveAttribute('data-header', 'true'); + await expect(rowsLocator.nth(1)).toHaveAttribute('data-header', 'true'); + await expect(rowsLocator.nth(2)).toHaveAttribute('data-header', 'true'); + + await editor.focus(); + await page.keyboard.press('Escape'); + await editor.blur(); + + await page.mouse.move(-50, -50); + await wait.timeout(500); + await expectScreenshot(); + }); + + test('should unset header for current and all subsequent header rows', async ({ + wait, + mount, + editor, + }) => { + const initial = dd` + #| + |:{header-rows="3"} + || one | two || + || three | four || + || five | six || + || seven | eight || + |# + `; + + await mount(); + + const tableLocator = ( + await editor.yfmTable.getTable(editor.locators.contenteditable) + ).first(); + const rowsLocator = await editor.yfmTable.getRows(tableLocator); + const cellsLocator = await editor.yfmTable.getCells(tableLocator); + const rowButton = (await editor.yfmTable.getRowButtons(tableLocator)).first(); + const rowMenu = editor.yfmTable.getMenuLocator('row'); + const headerToggle = editor.yfmTable.getCellActionLocator('row', 'header-toggle'); + + await expect(rowsLocator.nth(0)).toHaveAttribute('data-header', 'true'); + await expect(rowsLocator.nth(1)).toHaveAttribute('data-header', 'true'); + await expect(rowsLocator.nth(2)).toHaveAttribute('data-header', 'true'); + await expect(rowsLocator.nth(3)).not.toHaveAttribute('data-header', 'true'); + + await editor.yfmTable.focusFirstCell(tableLocator); + + // open row menu of row 1 (first cell of row 1 is the 3rd cell overall) + await cellsLocator.nth(2).hover(); + await wait.timeout(200); + await rowButton.click(); + await editor.yfmTable.doCellAction('row', 'header-toggle'); + + await expect(rowsLocator.nth(0)).toHaveAttribute('data-header', 'true'); + await expect(rowsLocator.nth(1)).not.toHaveAttribute('data-header', 'true'); + await expect(rowsLocator.nth(2)).not.toHaveAttribute('data-header', 'true'); + await expect(rowsLocator.nth(3)).not.toHaveAttribute('data-header', 'true'); + + // re-open row menu of row 1 — header toggle should be hidden now + await cellsLocator.nth(2).hover(); + await wait.timeout(200); + await rowButton.click(); + await rowMenu.waitFor({state: 'visible'}); + await expect(headerToggle).toBeHidden(); + }); + + test('should shrink header-rows block when inserting a row inside it', async ({ + wait, + mount, + editor, + }) => { + const initial = dd` + #| + |:{header-rows="3"} + || one | two || + || three | four || + || five | six || + || seven | eight || + |# + `; + + await mount(); + + const tableLocator = ( + await editor.yfmTable.getTable(editor.locators.contenteditable) + ).first(); + const rowsLocator = await editor.yfmTable.getRows(tableLocator); + const cellsLocator = await editor.yfmTable.getCells(tableLocator); + const rowButton = (await editor.yfmTable.getRowButtons(tableLocator)).first(); + + await expect(rowsLocator.nth(0)).toHaveAttribute('data-header', 'true'); + await expect(rowsLocator.nth(1)).toHaveAttribute('data-header', 'true'); + await expect(rowsLocator.nth(2)).toHaveAttribute('data-header', 'true'); + await expect(rowsLocator.nth(3)).not.toHaveAttribute('data-header', 'true'); + + await editor.yfmTable.focusFirstCell(tableLocator); + + // open row menu of row 1 (first cell of row 1 is the 3rd cell overall) + await cellsLocator.nth(2).hover(); + await wait.timeout(200); + await rowButton.click(); + await editor.yfmTable.doCellAction('row', 'add-row-before'); + + await expect(rowsLocator).toHaveCount(5); + await expect(rowsLocator.nth(0)).toHaveAttribute('data-header', 'true'); + await expect(rowsLocator.nth(1)).not.toHaveAttribute('data-header', 'true'); + await expect(rowsLocator.nth(2)).not.toHaveAttribute('data-header', 'true'); + await expect(rowsLocator.nth(3)).not.toHaveAttribute('data-header', 'true'); + await expect(rowsLocator.nth(4)).not.toHaveAttribute('data-header', 'true'); + }); + + test('should decrease header-rows count by the number of removed header rows', async ({ + wait, + mount, + editor, + }) => { + const initial = dd` + #| + |:{header-rows="3"} + || one | two || + || three | four || + || ^ | six || + || seven | eight || + |# + `; + + await mount(); + + const tableLocator = ( + await editor.yfmTable.getTable(editor.locators.contenteditable) + ).first(); + const rowsLocator = await editor.yfmTable.getRows(tableLocator); + const cellsLocator = await editor.yfmTable.getCells(tableLocator); + const rowButton = (await editor.yfmTable.getRowButtons(tableLocator)).first(); + + await expect(rowsLocator.nth(0)).toHaveAttribute('data-header', 'true'); + await expect(rowsLocator.nth(1)).toHaveAttribute('data-header', 'true'); + await expect(rowsLocator.nth(2)).toHaveAttribute('data-header', 'true'); + await expect(rowsLocator.nth(3)).not.toHaveAttribute('data-header', 'true'); + + await editor.yfmTable.focusFirstCell(tableLocator); + + // Open row menu of row 2 (first cell of row 2 is the 3rd cell overall) + await cellsLocator.nth(2).hover(); + await wait.timeout(200); + await rowButton.click(); + await editor.yfmTable.doCellAction('row', 'remove-row'); + + await expect(rowsLocator).toHaveCount(2); + await expect(rowsLocator.nth(0)).toHaveAttribute('data-header', 'true'); + await expect(rowsLocator.nth(1)).not.toHaveAttribute('data-header', 'true'); + }); + }); }); diff --git a/package.json b/package.json index 23bdd9c01..ab239fce9 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "typecheck": "nx run-many -t typecheck", "test": "nx run-many -t test,test:esbuild,test:circular-deps", "test:e2e": "nx playwright:docker @markdown-editor/demo", + "test:e2e:ui": "nx playwright:docker:ui @markdown-editor/demo", "test:e2e:report": "nx playwright:docker:report @markdown-editor/demo", "lint": "run-p -cs lint:*", "lint:js": "eslint './**/*.{js,jsx,mjs,ts,tsx}'", diff --git a/packages/editor/src/extensions/additional/FoldingHeading/plugins/Folding.ts b/packages/editor/src/extensions/additional/FoldingHeading/plugins/Folding.ts index a46709941..877196030 100644 --- a/packages/editor/src/extensions/additional/FoldingHeading/plugins/Folding.ts +++ b/packages/editor/src/extensions/additional/FoldingHeading/plugins/Folding.ts @@ -1,17 +1,11 @@ import type {Node} from 'prosemirror-model'; -import {Plugin, type Transaction} from 'prosemirror-state'; -import { - AddMarkStep, - AddNodeMarkStep, - DocAttrStep, - RemoveMarkStep, - RemoveNodeMarkStep, - ReplaceStep, -} from 'prosemirror-transform'; +import {Plugin} from 'prosemirror-state'; // @ts-ignore // TODO: fix cjs build import {findChildren} from 'prosemirror-utils'; import {Decoration, type DecorationAttrs, DecorationSet} from 'prosemirror-view'; +import {isNonStructuralTr} from 'src/utils/transaction'; + import {YfmHeadingAttr} from '../const'; import { isFoldedHeading, @@ -37,7 +31,7 @@ export const foldingPlugin = () => { if ( !tr.docChanged || // Optimization: ignoring trs, that don't change position of blocks in doc - canSafelyIgnoreTr(tr) + isNonStructuralTr(tr) ) { return prev.map(tr.mapping, tr.doc); } @@ -75,31 +69,6 @@ function isLeftPaddingClick(event: MouseEvent): boolean { return event.offsetX < leftPadding; } -const safeSteps = [AddMarkStep, AddNodeMarkStep, DocAttrStep, RemoveMarkStep, RemoveNodeMarkStep]; -function canSafelyIgnoreTr(tr: Transaction): boolean { - if (isInputTr(tr) || isTextBackspaceTr(tr)) return true; - if (tr.steps.every((step) => safeSteps.some((SafeStep) => step instanceof SafeStep))) - return true; - return false; -} - -function isInputTr(tr: Transaction): boolean { - if (tr.steps.length !== 1) return false; - const [step] = tr.steps; - return ( - step instanceof ReplaceStep && - step.from === step.to && - step.slice.content.childCount === 1 && - step.slice.content.child(0).type.name === 'text' - ); -} - -function isTextBackspaceTr(tr: Transaction): boolean { - if (tr.steps.length !== 1) return false; - const [step] = tr.steps; - return step instanceof ReplaceStep && step.to - step.from === 1 && step.slice.size === 0; -} - // eslint-disable-next-line complexity function buildDecosSet(doc: Node): DecorationSet { const contentDecos: Record = {}; diff --git a/packages/editor/src/extensions/yfm/YfmTable/YfmTable.test.ts b/packages/editor/src/extensions/yfm/YfmTable/YfmTable.test.ts index 82afa32da..5274c0c6f 100644 --- a/packages/editor/src/extensions/yfm/YfmTable/YfmTable.test.ts +++ b/packages/editor/src/extensions/yfm/YfmTable/YfmTable.test.ts @@ -1,6 +1,7 @@ import {EditorState} from 'prosemirror-state'; import {builders} from 'prosemirror-test-builder'; import {EditorView} from 'prosemirror-view'; +import dd from 'ts-dedent'; import {dispatchPasteEvent} from '../../../../tests/dispatch-event'; import {parseDOM} from '../../../../tests/parse-dom'; @@ -519,6 +520,98 @@ nested table ); }); + it('should parse table with header-rows attribute', () => { + const markup = dd` + #| + |:{header-rows="1"} + || + + Header 1 + + | + + Header 2 + + || + || + + Cell 1 + + | + + Cell 2 + + || + |# + + + `; + + same( + markup, + doc( + table( + {[YfmTableAttr.HeaderRows]: 1}, + tbody( + tr(td(p('Header 1')), td(p('Header 2'))), + tr(td(p('Cell 1')), td(p('Cell 2'))), + ), + ), + ), + ); + }); + + it('should parse table with header-rows="2"', () => { + const markup = dd` + #| + |:{header-rows="2"} + || + + H1 + + | + + H2 + + || + || + + H3 + + | + + H4 + + || + || + + Cell 1 + + | + + Cell 2 + + || + |# + + + `; + + same( + markup, + doc( + table( + {[YfmTableAttr.HeaderRows]: 2}, + tbody( + tr(td(p('H1')), td(p('H2'))), + tr(td(p('H3')), td(p('H4'))), + tr(td(p('Cell 1')), td(p('Cell 2'))), + ), + ), + ), + ); + }); + it('should preserve cell-align', () => { const markup = ` #| diff --git a/packages/editor/src/extensions/yfm/YfmTable/YfmTableSpecs/const.ts b/packages/editor/src/extensions/yfm/YfmTable/YfmTableSpecs/const.ts index 9c47a488c..d6f0819a0 100644 --- a/packages/editor/src/extensions/yfm/YfmTable/YfmTableSpecs/const.ts +++ b/packages/editor/src/extensions/yfm/YfmTable/YfmTableSpecs/const.ts @@ -9,4 +9,5 @@ export enum YfmTableAttr { Colspan = 'colspan', Rowspan = 'rowspan', CellAlign = 'data-cell-align', + HeaderRows = 'data-header-rows', } diff --git a/packages/editor/src/extensions/yfm/YfmTable/YfmTableSpecs/parser.ts b/packages/editor/src/extensions/yfm/YfmTable/YfmTableSpecs/parser.ts index 5c9177bb3..e1fa64412 100644 --- a/packages/editor/src/extensions/yfm/YfmTable/YfmTableSpecs/parser.ts +++ b/packages/editor/src/extensions/yfm/YfmTable/YfmTableSpecs/parser.ts @@ -3,7 +3,15 @@ import type {ParserToken} from '../../../../core'; import {YfmTableAttr, YfmTableNode} from './const'; export const parserTokens: Record = { - [YfmTableNode.Table]: {name: YfmTableNode.Table, type: 'block'}, + [YfmTableNode.Table]: { + name: YfmTableNode.Table, + type: 'block', + getAttrs: (token) => { + return { + [YfmTableAttr.HeaderRows]: token.meta?.headerRows || 0, + }; + }, + }, [YfmTableNode.Body]: {name: YfmTableNode.Body, type: 'block'}, diff --git a/packages/editor/src/extensions/yfm/YfmTable/YfmTableSpecs/schema.ts b/packages/editor/src/extensions/yfm/YfmTable/YfmTableSpecs/schema.ts index 7866428d6..13269b8c2 100644 --- a/packages/editor/src/extensions/yfm/YfmTable/YfmTableSpecs/schema.ts +++ b/packages/editor/src/extensions/yfm/YfmTable/YfmTableSpecs/schema.ts @@ -23,9 +23,25 @@ export const getSchemaSpecs = ( content: `${YfmTableNode.Body}`, isolating: true, definingAsContext: true, - parseDOM: [{tag: 'table', priority: 200}], - toDOM() { - return ['table', 0]; + attrs: { + [YfmTableAttr.HeaderRows]: {default: 0}, + }, + parseDOM: [ + { + tag: 'table', + priority: 200, + getAttrs(dom) { + const attr = dom.getAttribute(YfmTableAttr.HeaderRows); + const fromDataAttr = attr ? Math.max(0, parseInt(attr, 10) || 0) : 0; + const theadRows = dom.querySelectorAll('thead > tr').length; + return {[YfmTableAttr.HeaderRows]: fromDataAttr || theadRows}; + }, + }, + ], + toDOM(node) { + const headerRows = node.attrs[YfmTableAttr.HeaderRows]; + const attrs = headerRows ? node.attrs : {}; + return ['table', attrs, 0]; }, selectable: true, allowSelection: true, diff --git a/packages/editor/src/extensions/yfm/YfmTable/YfmTableSpecs/serializer.ts b/packages/editor/src/extensions/yfm/YfmTable/YfmTableSpecs/serializer.ts index 9d2bb0e7b..b95f4a884 100644 --- a/packages/editor/src/extensions/yfm/YfmTable/YfmTableSpecs/serializer.ts +++ b/packages/editor/src/extensions/yfm/YfmTable/YfmTableSpecs/serializer.ts @@ -9,6 +9,13 @@ export const serializerTokens: Record = { state.ensureNewLine(); state.write('#|'); state.ensureNewLine(); + + const headerRows = Number(node.attrs[YfmTableAttr.HeaderRows]) || 0; + if (headerRows > 0) { + state.write(`|:{header-rows="${headerRows}"}`); + state.ensureNewLine(); + } + state.renderContent(node); state.write('|#'); state.ensureNewLine(); diff --git a/packages/editor/src/extensions/yfm/YfmTable/index.ts b/packages/editor/src/extensions/yfm/YfmTable/index.ts index 7d3051935..6a35b1ea2 100644 --- a/packages/editor/src/extensions/yfm/YfmTable/index.ts +++ b/packages/editor/src/extensions/yfm/YfmTable/index.ts @@ -31,6 +31,14 @@ export type YfmTableOptions = YfmTableSpecsOptions & { * @default true */ dnd?: boolean; + /** + * Enables header rows functionality in table (toggle row as header, visual decoration). + * The `controls` property must also be `true`. + * + * Available with @diplodoc/transform v4.75.0 or higher. + * @default false + */ + headerRows?: boolean; }; export const YfmTable: ExtensionWithOptions = (builder, options) => { @@ -47,7 +55,12 @@ export const YfmTable: ExtensionWithOptions = (builder, options builder.addPlugin(yfmTableTransformPastedPlugin); if (options.controls !== false) { - builder.addPlugin(yfmTableControlsPlugins({dndEnabled: options.dnd !== false})); + builder.addPlugin( + yfmTableControlsPlugins({ + dndEnabled: options.dnd !== false, + headerRowsEnabled: options.headerRows === true, + }), + ); } }; diff --git a/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/commands/insert-empty-row.ts b/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/commands/insert-empty-row.ts index beba3bac6..851abbd8d 100644 --- a/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/commands/insert-empty-row.ts +++ b/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/commands/insert-empty-row.ts @@ -4,6 +4,7 @@ import {isEqual, range, uniqWith} from 'src/lodash'; import {type RealCellPos, type TableCellRealDesc, TableDesc} from 'src/table-utils/table-desc'; import {yfmTableCellType, yfmTableRowType} from '../../../YfmTableSpecs'; +import {YfmTableAttr} from '../../../YfmTableSpecs/const'; export type InsertEmptyRowParams = { tablePos: number; @@ -48,6 +49,13 @@ export const insertEmptyRow = (params: InsertEmptyRowParams): Command => { } tr.insert(posToInsert, createSimpleRow(state.schema, newCellsCount)); tr.setSelection(TextSelection.near(tr.doc.resolve(posToInsert), 1)); + + // If the new row is inserted inside the header-rows block, shrink the block + // so the new row and everything below it stop being header rows. + if (tableDesc.base.isHeaderRow(rowIdx)) { + tr.setNodeAttribute(params.tablePos, YfmTableAttr.HeaderRows, rowIdx); + } + dispatch(tr.scrollIntoView()); } diff --git a/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/commands/remove-row-range.ts b/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/commands/remove-row-range.ts index 7e3b32be3..dbbc998f5 100644 --- a/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/commands/remove-row-range.ts +++ b/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/commands/remove-row-range.ts @@ -8,6 +8,7 @@ import { type VirtualCellPos, } from 'src/table-utils/table-desc'; +import {YfmTableAttr} from '../../../YfmTableSpecs/const'; import {yfmTableCellType} from '../../../YfmTableSpecs/utils'; export type RemoveRowRangeParams = { @@ -76,6 +77,22 @@ export const removeRowRange = (params: RemoveRowRangeParams): Command => { updateRowspan(tr, tableDesc, diffRowspan); + // Decrease header-rows count by the number of header rows removed by this range. + const prevHeaderRows = tableDesc.base.headerRows; + if (prevHeaderRows > 0) { + const removedHeaderRows = Math.max( + 0, + Math.min(range.endIdx, prevHeaderRows - 1) - range.startIdx + 1, + ); + if (removedHeaderRows > 0) { + tr.setNodeAttribute( + params.tablePos, + YfmTableAttr.HeaderRows, + prevHeaderRows - removedHeaderRows, + ); + } + } + dispatch(tr); } diff --git a/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/commands/toggle-header-rows.test.ts b/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/commands/toggle-header-rows.test.ts new file mode 100644 index 000000000..fa608dcd5 --- /dev/null +++ b/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/commands/toggle-header-rows.test.ts @@ -0,0 +1,120 @@ +import {EditorState} from 'prosemirror-state'; +import {builders} from 'prosemirror-test-builder'; + +import {ExtensionsManager} from 'src/core'; +import {BaseNode, BaseSchemaSpecs} from 'src/extensions/base/specs'; +import {TableDesc} from 'src/table-utils/table-desc'; + +import {YfmTableNode, YfmTableSpecs} from '../../../YfmTableSpecs'; +import {YfmTableAttr} from '../../../const'; + +import {canMakeRowHeader, toggleHeaderRows} from './toggle-header-rows'; + +const {schema} = new ExtensionsManager({ + extensions: (builder) => builder.use(BaseSchemaSpecs, {}).use(YfmTableSpecs, {}), +}).build(); + +const {doc, p, table, tbody, tr, td} = builders<'doc' | 'p' | 'table' | 'tbody' | 'tr' | 'td'>( + schema, + { + doc: {nodeType: BaseNode.Doc}, + p: {nodeType: BaseNode.Paragraph}, + table: {nodeType: YfmTableNode.Table}, + tbody: {nodeType: YfmTableNode.Body}, + tr: {nodeType: YfmTableNode.Row}, + td: {nodeType: YfmTableNode.Cell}, + }, +); + +describe('toggleHeaderRows command', () => { + it('should set header-rows to 1', () => { + const editorState = EditorState.create({ + doc: doc(table(tbody(tr(td(p('A')), td(p('B'))), tr(td(p('C')), td(p('D')))))), + schema, + }); + + let dispatched: any = null; + const ok = toggleHeaderRows({tablePos: 0, value: 1})(editorState, (tr) => { + dispatched = tr; + }); + expect(ok).toBe(true); + expect(dispatched.doc.nodeAt(0)?.attrs[YfmTableAttr.HeaderRows]).toBe(1); + }); + + it('should clamp value to table row count', () => { + const editorState = EditorState.create({ + doc: doc(table(tbody(tr(td(p('A'))), tr(td(p('B')))))), + schema, + }); + + let dispatched: any = null; + toggleHeaderRows({tablePos: 0, value: 999})(editorState, (tr) => { + dispatched = tr; + }); + expect(dispatched.doc.nodeAt(0)?.attrs[YfmTableAttr.HeaderRows]).toBe(2); + }); + + it('should set header-rows to 0 (unset)', () => { + const editorState = EditorState.create({ + doc: doc(table({[YfmTableAttr.HeaderRows]: 1}, tbody(tr(td(p('A'))), tr(td(p('B')))))), + schema, + }); + + let dispatched: any = null; + toggleHeaderRows({tablePos: 0, value: 0})(editorState, (tr) => { + dispatched = tr; + }); + expect(dispatched.doc.nodeAt(0)?.attrs[YfmTableAttr.HeaderRows]).toBe(0); + }); + + it('should return false when value does not change', () => { + const editorState = EditorState.create({ + doc: doc(table({[YfmTableAttr.HeaderRows]: 1}, tbody(tr(td(p('A'))), tr(td(p('B')))))), + schema, + }); + + const result = toggleHeaderRows({tablePos: 0, value: 1})(editorState, () => {}); + expect(result).toBe(false); + }); +}); + +describe('canMakeRowHeader helper', () => { + it('should allow making row 0 header when headerRows=0', () => { + const tableNode = table(tbody(tr(td(p('A'))), tr(td(p('B'))))); + const desc = TableDesc.create(tableNode)!; + expect(canMakeRowHeader(desc, 0)).toBe(true); + }); + + it('should NOT allow making row 0 header when already headerRows=1', () => { + const tableNode = table( + {[YfmTableAttr.HeaderRows]: 1}, + tbody(tr(td(p('A'))), tr(td(p('B')))), + ); + const desc = TableDesc.create(tableNode)!; + expect(canMakeRowHeader(desc, 0)).toBe(false); + }); + + it('should NOT allow making row 1 header when headerRows=0 and no rowspan from row 0', () => { + const tableNode = table(tbody(tr(td(p('A'))), tr(td(p('B'))))); + const desc = TableDesc.create(tableNode)!; + expect(canMakeRowHeader(desc, 1)).toBe(false); + }); + + it('should allow making row 1 header when row 0 has rowspan covering row 1 (headerRows=1)', () => { + const tableNode = table( + {[YfmTableAttr.HeaderRows]: 1}, + tbody(tr(td({rowspan: '2'}, p('A')), td(p('B'))), tr(td(p('C')))), + ); + const desc = TableDesc.create(tableNode)!; + expect(canMakeRowHeader(desc, 1)).toBe(true); + }); + + it('should allow making row 2 header when row 0 has rowspan=3 covering rows 1 and 2 (headerRows=2)', () => { + const tableNode = table( + {[YfmTableAttr.HeaderRows]: 2}, + tbody(tr(td({rowspan: '3'}, p('A')), td(p('B'))), tr(td(p('C'))), tr(td(p('D')))), + ); + const desc = TableDesc.create(tableNode)!; + expect(canMakeRowHeader(desc, 2)).toBe(true); + }); +}); diff --git a/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/commands/toggle-header-rows.ts b/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/commands/toggle-header-rows.ts new file mode 100644 index 000000000..9cca349a1 --- /dev/null +++ b/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/commands/toggle-header-rows.ts @@ -0,0 +1,41 @@ +import type {Command} from '#pm/state'; +import {TableDesc} from 'src/table-utils/table-desc'; + +import {YfmTableAttr} from '../../../YfmTableSpecs/const'; + +export type ToggleHeaderRowsParams = { + tablePos: number; + value: number; +}; + +export const toggleHeaderRows = + (params: ToggleHeaderRowsParams): Command => + (state, dispatch) => { + const table = state.doc.nodeAt(params.tablePos); + const tableDesc = table && TableDesc.create(table); + if (!tableDesc) return false; + const next = Math.max(0, Math.min(params.value, tableDesc.rows)); + if (next === tableDesc.headerRows) return false; + if (dispatch) { + dispatch(state.tr.setNodeAttribute(params.tablePos, YfmTableAttr.HeaderRows, next)); + } + return true; + }; + +/** + * Returns true if the row at `rowIdx` can be made a header row. + * Row 0 is always eligible (to set headerRows from 0 to 1). + * A subsequent row is eligible only if it's "visually glued" to the current header + * block — i.e. it contains a virtual cell produced by a real cell that starts within + * the existing header rows (rowspan crosses the header/body boundary). + */ +export function canMakeRowHeader(tableDesc: TableDesc, rowIdx: number): boolean { + const {headerRows} = tableDesc; + if (rowIdx === 0) return headerRows < 1; + if (rowIdx !== headerRows) return false; + const row = tableDesc.rowsDesc[rowIdx]; + if (!row) return false; + return row.cells.some( + (c) => c.type === 'virtual' && c.rowspan !== undefined && c.rowspan[0] < headerRows, + ); +} diff --git a/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/components/FloatingMenuControl/FloatingMenuControl.tsx b/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/components/FloatingMenuControl/FloatingMenuControl.tsx index b549f4c94..983a65ae6 100644 --- a/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/components/FloatingMenuControl/FloatingMenuControl.tsx +++ b/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/components/FloatingMenuControl/FloatingMenuControl.tsx @@ -7,10 +7,11 @@ import { ArrowRight, ArrowUp, BroomMotion as ClearCells, + LayoutHeader as HeaderRow, TrashBin, Xmark, } from '@gravity-ui/icons'; -import {Icon} from '@gravity-ui/uikit'; +import {type DropdownMenuItem, Flex, Icon, Switch} from '@gravity-ui/uikit'; import {i18n} from 'src/i18n/yfm-table'; @@ -31,6 +32,8 @@ export type FloatingMenuControlProps = { onInsertAfterClick: () => void; onRemoveRangeClick: () => void; onRemoveTableClick: () => void; + isRowHeader?: boolean; + onRowHeaderChange?: (value: boolean) => void; }; export const FloatingMenuControl: React.FC = @@ -46,58 +49,82 @@ export const FloatingMenuControl: React.FC = onInsertAfterClick, onRemoveRangeClick, onRemoveTableClick, + isRowHeader = false, + onRowHeaderChange, }) { - const dropdownItems = useMemo( - () => + const dropdownItems = useMemo(() => { + const headerItems: DropdownMenuItem[] = []; + if (onRowHeaderChange) { + headerItems.push({ + text: ( + + {i18n(`row.header${multiple ? '.multiple' : ''}`)} + , which would trigger action twice + style={{pointerEvents: 'none'}} + /> + + ), + iconStart: , + qa: 'g-md-yfm-table-row-header-toggle', + action: () => onRowHeaderChange(!isRowHeader), + }); + } + + return [ + ...headerItems, [ - [ - { - text: i18n(`${type}.add.before`), - qa: `g-md-yfm-table-action-add-${type}-before`, - action: onInsertBeforeClick, - iconStart: , - }, - { - text: i18n(`${type}.add.after`), - qa: `g-md-yfm-table-action-add-${type}-after`, - action: onInsertAfterClick, - iconStart: , - }, - ], - [ - { - text: i18n('cells.clear'), - qa: `g-md-yfm-table-${type}-clear-cells`, - action: onClearCellsClick, - iconStart: , - }, - ], - [ - { - text: i18n(`${type}.remove${multiple ? '.multiple' : ''}`), - qa: `g-md-yfm-table-action-remove-${type}`, - action: onRemoveRangeClick, - iconStart: , - }, - { - theme: 'danger', - text: i18n('table.remove'), - qa: 'g-md-yfm-table-action-remove-table', - action: onRemoveTableClick, - iconStart: , - }, - ], - ] satisfies FloatingMenuProps['dropdownItems'], - [ - type, - multiple, - onClearCellsClick, - onInsertAfterClick, - onInsertBeforeClick, - onRemoveRangeClick, - onRemoveTableClick, - ], - ); + { + text: i18n(`${type}.add.before`), + qa: `g-md-yfm-table-action-add-${type}-before`, + action: onInsertBeforeClick, + iconStart: , + }, + { + text: i18n(`${type}.add.after`), + qa: `g-md-yfm-table-action-add-${type}-after`, + action: onInsertAfterClick, + iconStart: , + }, + ], + [ + { + text: i18n('cells.clear'), + qa: `g-md-yfm-table-${type}-clear-cells`, + action: onClearCellsClick, + iconStart: , + }, + ], + [ + { + text: i18n(`${type}.remove${multiple ? '.multiple' : ''}`), + qa: `g-md-yfm-table-action-remove-${type}`, + action: onRemoveRangeClick, + iconStart: , + }, + { + theme: 'danger', + text: i18n('table.remove'), + qa: 'g-md-yfm-table-action-remove-table', + action: onRemoveTableClick, + iconStart: , + }, + ], + ] satisfies FloatingMenuProps['dropdownItems']; + }, [ + type, + multiple, + isRowHeader, + onRowHeaderChange, + onClearCellsClick, + onInsertAfterClick, + onInsertBeforeClick, + onRemoveRangeClick, + onRemoveTableClick, + ]); const anchor = useMemo( () => getVirtualAnchor(type, tableElement, cellElement), diff --git a/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/dnd/dnd.scss b/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/dnd/dnd.scss index 86a546da9..3f90f56ef 100644 --- a/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/dnd/dnd.scss +++ b/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/dnd/dnd.scss @@ -26,8 +26,11 @@ border-color: var(--g-color-line-brand); box-shadow: 0 8px 20px 1px var(--g-color-sfx-shadow); - & > tbody > tr > td { - border-color: var(--g-color-line-brand); + & > tbody > tr { + & > th, + & > td { + border-color: var(--g-color-line-brand); + } } } } @@ -42,6 +45,7 @@ } .yfm.ProseMirror table { + th, td { $selectedCell: 'g-md-yfm-table-selected-cell'; diff --git a/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/dnd/dnd.ts b/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/dnd/dnd.ts index 81106408e..b41805309 100644 --- a/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/dnd/dnd.ts +++ b/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/dnd/dnd.ts @@ -15,6 +15,7 @@ import { } from 'src/table-utils/table-desc'; import {YfmTableNode} from '../../../YfmTableSpecs'; +import {YfmTableAttr} from '../../../YfmTableSpecs/const'; import {clearAllSelections, selectDraggedColumn, selectDraggedRow} from '../plugins/dnd-plugin'; import {hideHoverDecos} from '../plugins/focus-plugin-key'; import {getSelectedCellsForColumns, getSelectedCellsForRows} from '../utils'; @@ -201,6 +202,7 @@ class YfmTableRowDnDHandler extends YfmTableDnDAbstractHandler { canDrag(): boolean { const res = this._getTableDescAndCellInfo(); if (!res) return false; + if (res.tableDesc.base.isHeaderRow(res.cellInfo.row)) return false; const rowRange = res.tableDesc.base.getRowRangeByRowIdx(res.cellInfo.row); return rowRange.safeTopBoundary && rowRange.safeBottomBoundary; } @@ -210,6 +212,8 @@ class YfmTableRowDnDHandler extends YfmTableDnDAbstractHandler { if (!info) return; const {tableDesc, cellInfo} = info; + if (tableDesc.base.isHeaderRow(cellInfo.row)) return; + const rowRanges = tableDesc.base.getRowRanges(); const currRowRange = tableDesc.base.getRowRangeByRowIdx(cellInfo.row); if (!currRowRange.safeTopBoundary || !currRowRange.safeBottomBoundary) return; @@ -379,7 +383,33 @@ class YfmTableRowDnDHandler extends YfmTableDnDAbstractHandler { return; } + const insertIdx = ((): number => { + const total = info.tableDesc.base.rows; + for (let i = 0; i < total; i++) { + const {from, to} = info.tableDesc.getPosForRow(i); + if (point === from) return i; + if (point === to) return i + 1; + } + return -1; + })(); + + const tablePos = info.tableDesc.pos; + const prevHeaderRows = info.tableDesc.base.headerRows; + const nextHeaderRows = ((): number => { + if (prevHeaderRows <= 0 || insertIdx < 0) return prevHeaderRows; + const from = draggedRange.startIdx; + const to = draggedRange.endIdx + 1; + // dragged downwards + if (insertIdx > to && from < prevHeaderRows) return from; + // dragged upwards into (or above) the header block + if (insertIdx < from && insertIdx < prevHeaderRows) return insertIdx; + return prevHeaderRows; + })(); + const {tr} = this._editorView.state; + if (nextHeaderRows !== prevHeaderRows) { + tr.setNodeAttribute(tablePos, YfmTableAttr.HeaderRows, nextHeaderRows); + } const fragment = tr.doc.slice(rangeFrom, rangeTo, false).content; if (point > rangeFrom) { tr.insert(point, fragment); diff --git a/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/index.ts b/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/index.ts index b9738f3f8..e43a688b8 100644 --- a/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/index.ts +++ b/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/index.ts @@ -2,8 +2,16 @@ import type {ExtensionDeps} from '#core'; import {yfmTableDndPlugin} from './plugins/dnd-plugin'; import {yfmTableFocusPlugin} from './plugins/focus-plugin'; +import {yfmTableHeaderRowsPlugin} from './plugins/header-rows-plugin'; -export const yfmTableControlsPlugins = (opts: {dndEnabled: boolean}) => (_deps: ExtensionDeps) => [ - yfmTableFocusPlugin(opts), - yfmTableDndPlugin(), -]; +export type YfmTableControlsPluginsOpts = { + dndEnabled: boolean; + headerRowsEnabled: boolean; +}; + +export const yfmTableControlsPlugins = + (opts: YfmTableControlsPluginsOpts) => (_deps: ExtensionDeps) => [ + yfmTableFocusPlugin(opts), + yfmTableDndPlugin(), + ...(opts.headerRowsEnabled ? [yfmTableHeaderRowsPlugin()] : []), + ]; diff --git a/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/nodeviews/yfm-table-cell-view.scss b/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/nodeviews/yfm-table-cell-view.scss new file mode 100644 index 000000000..72f7fd9ce --- /dev/null +++ b/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/nodeviews/yfm-table-cell-view.scss @@ -0,0 +1,5 @@ +.yfm.ProseMirror { + table td[role='columnheader'] { + vertical-align: inherit; + } +} diff --git a/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/nodeviews/yfm-table-cell-view.tsx b/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/nodeviews/yfm-table-cell-view.tsx index c8f97fa25..229da4779 100644 --- a/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/nodeviews/yfm-table-cell-view.tsx +++ b/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/nodeviews/yfm-table-cell-view.tsx @@ -21,6 +21,7 @@ import {insertEmptyColumn} from '../commands/insert-empty-column'; import {insertEmptyRow} from '../commands/insert-empty-row'; import {removeColumnRange} from '../commands/remove-column-range'; import {removeRowRange} from '../commands/remove-row-range'; +import {canMakeRowHeader, toggleHeaderRows} from '../commands/toggle-header-rows'; import {FloatingMenuControl} from '../components/FloatingMenuControl'; import { YfmTableDecorationType as DecoType, @@ -36,6 +37,8 @@ import { } from '../plugins/dnd-plugin'; import {getSelectedCellsForColumns, getSelectedCellsForRows} from '../utils'; +import './yfm-table-cell-view.scss'; + const dropCursorParams: DropCursorParams = { color: 'var(--g-color-line-brand)', width: 2, @@ -44,6 +47,7 @@ const dropCursorParams: DropCursorParams = { type GetPos = () => number | undefined; type YfmTableCellViewOptions = { dndEnabled: boolean; + headerRowsEnabled: boolean; }; export const yfmTableCellView = @@ -62,7 +66,9 @@ class YfmTableCellView implements NodeView { private readonly _renderer; private readonly _logger: Logger2.ILogger; private readonly _dndEnabled: boolean; + private readonly _headerRowsEnabled: boolean; + private _isHeader: boolean; private _decoRowUniqKey: number | null = null; private _decoColumnUniqKey: number | null = null; private _cellInfo: null | { @@ -73,6 +79,8 @@ class YfmTableCellView implements NodeView { columnRange: Readonly; showRowControl: boolean; showColumnControl: boolean; + isRowHeader: boolean; + canToggleRowHeader: boolean; }; private _dndHandler: YfmTableDnDHandler | null; @@ -91,7 +99,9 @@ class YfmTableCellView implements NodeView { node: 'yfm-table', }); this._dndEnabled = opts.dndEnabled; + this._headerRowsEnabled = opts.headerRowsEnabled; + this._isHeader = this._computeIsHeader(undefined); this.dom = document.createElement('td'); this._updateDom(); @@ -124,6 +134,12 @@ class YfmTableCellView implements NodeView { onInsertAfterClick={this._onRowInsertAfterClick} onRemoveRangeClick={this._onRowRemoveRangeClick} onRemoveTableClick={this._onRemoveTableClick} + isRowHeader={this._cellInfo.isRowHeader} + onRowHeaderChange={ + this._cellInfo.canToggleRowHeader + ? this._onRowHeaderChange + : undefined + } /> )} {showColumnControl && ( @@ -150,25 +166,28 @@ class YfmTableCellView implements NodeView { } update(node: Node, decorations: readonly Decoration[]): boolean { - { - const prev = this._node; - this._node = node; - this._updateDom(prev); - } + const prev = this._node; + this._node = node; const cellInfo = this._getCellInfo(); + this._isHeader = this._computeIsHeader(cellInfo); + this._updateDom(prev); if (cellInfo && (cellInfo.cell.row === 0 || cellInfo.cell.column === 0)) { - const info = (this._cellInfo = { + const desc = cellInfo.tableDesc.base; + const isRowHeader = desc.isHeaderRow(cellInfo.cell.row); + const info: YfmTableCellView['_cellInfo'] = (this._cellInfo = { tablePos: cellInfo.table.pos, rowIndex: cellInfo.cell.row, columnIndex: cellInfo.cell.column, showRowControl: false as boolean, showColumnControl: false as boolean, - rowRange: cellInfo.tableDesc.base.getRowRangeByRowIdx(cellInfo.cell.row), - columnRange: cellInfo.tableDesc.base.getColumnRangeByColumnIdx( - cellInfo.cell.column, - ), + rowRange: desc.getRowRangeByRowIdx(cellInfo.cell.row), + columnRange: desc.getColumnRangeByColumnIdx(cellInfo.cell.column), + isRowHeader, + canToggleRowHeader: + this._headerRowsEnabled && + (isRowHeader || canMakeRowHeader(desc, cellInfo.cell.row)), }); for (const deco of decorations) { @@ -210,6 +229,13 @@ class YfmTableCellView implements NodeView { } private _updateDom(prev?: Node) { + if (this._isHeader) { + if (this.dom.getAttribute('role') !== 'columnheader') + this.dom.setAttribute('role', 'columnheader'); + } else if (this.dom.hasAttribute('role')) { + this.dom.removeAttribute('role'); + } + if (prev?.attrs[YfmTableAttr.CellAlign]) { this.dom.classList.remove(prev.attrs[YfmTableAttr.CellAlign]); } @@ -404,6 +430,33 @@ class YfmTableCellView implements NodeView { this._view.focus(); }; + private _toggleHeaderRows( + event: 'row-set-header' | 'row-unset-header', + source: 'row-menu' | 'column-menu', + getValue: (rowRange: Readonly) => number, + ) { + this._logger.event({event, source}); + + const info = this._getCellInfo(); + if (info) { + const rowRange = info.tableDesc.base.getRowRangeByRowIdx(info.cell.row); + toggleHeaderRows({ + tablePos: info.table.pos, + value: getValue(rowRange), + })(this._view.state, this._view.dispatch); + } + + this._view.focus(); + } + + private _onRowHeaderChange = (value: boolean) => { + this._toggleHeaderRows( + value ? 'row-set-header' : 'row-unset-header', + 'row-menu', + (range) => (value ? range.endIdx + 1 : range.startIdx), + ); + }; + private _onRemoveTableClick = () => { this._logger.event({event: 'table-remove'}); @@ -418,10 +471,16 @@ class YfmTableCellView implements NodeView { this._view.focus(); }; - private _getCellInfo() { + private _computeIsHeader(cellInfo: ReturnType): boolean { + if (!this._headerRowsEnabled) return false; + if (!cellInfo) return false; + return cellInfo.tableDesc.base.isHeaderRow(cellInfo.cell.row); + } + + private _getCellInfo(node: Node = this._node) { const table = this._getParentTable(); const tableDesc = table ? TableDesc.create(table.node)?.bind(table.pos) : undefined; - const cellInfo = tableDesc?.base.getCellInfo(this._node); + const cellInfo = tableDesc?.base.getCellInfo(node); return cellInfo ? {pos: this._getPos()!, table: table!, tableDesc: tableDesc!, cell: cellInfo} : undefined; diff --git a/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/plugins/focus-plugin.ts b/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/plugins/focus-plugin.ts index c921f6774..faec0d5bb 100644 --- a/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/plugins/focus-plugin.ts +++ b/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/plugins/focus-plugin.ts @@ -31,7 +31,7 @@ function shouldUpdateState(prev: HoverState, curr: HoverState): boolean { return true; } -export const yfmTableFocusPlugin = (opts: {dndEnabled: boolean}) => { +export const yfmTableFocusPlugin = (opts: {dndEnabled: boolean; headerRowsEnabled: boolean}) => { return new Plugin({ key: pluginKey, state: { @@ -109,12 +109,12 @@ export const yfmTableFocusPlugin = (opts: {dndEnabled: boolean}) => { if ( e.target instanceof Element && - e.target.closest('td') && + e.target.closest('td,th') && e.target.closest(`.${FOCUSED_CLASSNAME}`) ) { const $pos = (() => { - const td = e.target.closest('td'); + const td = e.target.closest('td,th'); const domPos = td && view.posAtDOM(td, 0); return domPos === null ? null : view.state.doc.resolve(domPos); })() ?? diff --git a/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/plugins/header-rows-plugin.ts b/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/plugins/header-rows-plugin.ts new file mode 100644 index 000000000..46e1a4c0b --- /dev/null +++ b/packages/editor/src/extensions/yfm/YfmTable/plugins/YfmTableControls/plugins/header-rows-plugin.ts @@ -0,0 +1,45 @@ +import type {Node} from '#pm/model'; +import {Plugin} from '#pm/state'; +import {Decoration, DecorationSet} from '#pm/view'; +import {isTableNode} from 'src/table-utils'; +import {TableDesc} from 'src/table-utils/table-desc'; +import {isNonStructuralTr} from 'src/utils/transaction'; + +function buildHeaderDecorations(doc: Node): Decoration[] { + const out: Decoration[] = []; + doc.descendants((node, pos) => { + if (node.isTextblock) return false; + + if (isTableNode(node)) { + const tableDesc = TableDesc.create(node); + if (tableDesc?.headerRows) { + const bound = tableDesc.bind(pos); + for (let i = 0; i < tableDesc.headerRows; i++) { + const {from, to} = bound.getPosForRow(i); + out.push(Decoration.node(from, to, {'data-header': 'true'})); + } + } + } + + return true; + }); + return out; +} + +export const yfmTableHeaderRowsPlugin = () => + new Plugin({ + state: { + init(_config, state) { + return DecorationSet.create(state.doc, buildHeaderDecorations(state.doc)); + }, + apply(tr, set) { + if (!tr.docChanged || isNonStructuralTr(tr)) return set.map(tr.mapping, tr.doc); + return DecorationSet.create(tr.doc, buildHeaderDecorations(tr.doc)); + }, + }, + props: { + decorations(state) { + return this.getState(state); + }, + }, + }); diff --git a/packages/editor/src/extensions/yfm/YfmTabs/YfmTabs.test.ts b/packages/editor/src/extensions/yfm/YfmTabs/YfmTabs.test.ts index d7a3e5d15..661c5da01 100644 --- a/packages/editor/src/extensions/yfm/YfmTabs/YfmTabs.test.ts +++ b/packages/editor/src/extensions/yfm/YfmTabs/YfmTabs.test.ts @@ -90,7 +90,7 @@ describe('YfmTabs extension', () => { id: 'unknown', class: 'yfm-tab yfm-tab-group active', role: 'tab', - 'aria-controls': generatedId, + 'aria-controls': `regular-${generatedId}`, 'aria-selected': 'true', tabindex: '0', 'data-diplodoc-is-active': 'true', @@ -104,7 +104,7 @@ describe('YfmTabs extension', () => { id: 'unknown', class: 'yfm-tab yfm-tab-group', role: 'tab', - 'aria-controls': generatedId, + 'aria-controls': `regular-${generatedId}`, 'aria-selected': 'false', tabindex: '-1', 'data-diplodoc-is-active': 'false', @@ -116,7 +116,7 @@ describe('YfmTabs extension', () => { ), tabPanel( { - id: generatedId, + id: `regular-${generatedId}`, class: 'yfm-tab-panel active', role: 'tabpanel', 'data-title': 'panel title 1', @@ -126,7 +126,7 @@ describe('YfmTabs extension', () => { ), tabPanel( { - id: generatedId, + id: `regular-${generatedId}`, class: 'yfm-tab-panel', role: 'tabpanel', 'data-title': 'panel title 2', @@ -168,7 +168,7 @@ describe('YfmTabs extension', () => { id: 'unknown', class: 'yfm-tab yfm-tab-group active', role: 'tab', - 'aria-controls': generatedId, + 'aria-controls': `regular-${generatedId}`, 'aria-selected': 'true', tabindex: '0', 'data-diplodoc-is-active': 'true', @@ -180,7 +180,7 @@ describe('YfmTabs extension', () => { ), tabPanel( { - id: generatedId, + id: `regular-${generatedId}`, class: 'yfm-tab-panel active', role: 'tabpanel', 'data-title': 'Tab', @@ -236,7 +236,7 @@ ${' '} id: null, class: 'yfm-tab yfm-tab-group yfm-vertical-tab', role: 'tab', - 'aria-controls': generatedId, + 'aria-controls': `radio-${generatedId}`, 'aria-selected': 'false', tabindex: '0', 'data-diplodoc-is-active': 'false', @@ -252,7 +252,7 @@ ${' '} ), tabPanel( { - id: generatedId, + id: `radio-${generatedId}`, class: 'yfm-tab-panel', role: 'tabpanel', 'data-title': 'Radio button 1', @@ -270,7 +270,7 @@ ${' '} id: null, class: 'yfm-tab yfm-tab-group yfm-vertical-tab', role: 'tab', - 'aria-controls': generatedId, + 'aria-controls': `radio-${generatedId}`, 'aria-selected': 'false', tabindex: '0', 'data-diplodoc-is-active': 'false', @@ -286,7 +286,7 @@ ${' '} ), tabPanel( { - id: generatedId, + id: `radio-${generatedId}`, class: 'yfm-tab-panel', role: 'tabpanel', 'data-title': 'Nested radio button 1', @@ -299,7 +299,7 @@ ${' '} id: null, class: 'yfm-tab yfm-tab-group yfm-vertical-tab', role: 'tab', - 'aria-controls': generatedId, + 'aria-controls': `radio-${generatedId}`, 'aria-selected': 'false', tabindex: '-1', 'data-diplodoc-is-active': 'false', @@ -315,7 +315,7 @@ ${' '} ), tabPanel( { - id: generatedId, + id: `radio-${generatedId}`, class: 'yfm-tab-panel', role: 'tabpanel', 'data-title': 'Nested radio button 2', @@ -330,7 +330,7 @@ ${' '} id: null, class: 'yfm-tab yfm-tab-group yfm-vertical-tab', role: 'tab', - 'aria-controls': generatedId, + 'aria-controls': `radio-${generatedId}`, 'aria-selected': 'false', tabindex: '-1', 'data-diplodoc-is-active': 'false', @@ -346,7 +346,7 @@ ${' '} ), tabPanel( { - id: generatedId, + id: `radio-${generatedId}`, class: 'yfm-tab-panel', role: 'tabpanel', 'data-title': 'Radio button 2', diff --git a/packages/editor/src/i18n/yfm-table/en.json b/packages/editor/src/i18n/yfm-table/en.json index 74457d6d4..1276cd2e5 100644 --- a/packages/editor/src/i18n/yfm-table/en.json +++ b/packages/editor/src/i18n/yfm-table/en.json @@ -3,6 +3,8 @@ "column.add.after": "Add column after", "column.remove": "Remove column", "column.remove.multiple": "Remove columns", + "row.header": "Header row", + "row.header.multiple": "Header rows", "row.add.before": "Add row before", "row.add.after": "Add row after", "row.remove": "Remove row", diff --git a/packages/editor/src/i18n/yfm-table/ru.json b/packages/editor/src/i18n/yfm-table/ru.json index 48c60b680..43e4542b1 100644 --- a/packages/editor/src/i18n/yfm-table/ru.json +++ b/packages/editor/src/i18n/yfm-table/ru.json @@ -3,6 +3,8 @@ "column.add.after": "Добавить столбец после", "column.remove": "Удалить столбец", "column.remove.multiple": "Удалить столбцы", + "row.header": "Заглавная строка", + "row.header.multiple": "Заглавные строки", "row.add.before": "Добавить строку до", "row.add.after": "Добавить строку после", "row.remove": "Удалить строку", diff --git a/packages/editor/src/table-utils/table-desc.ts b/packages/editor/src/table-utils/table-desc.ts index 9c178edfe..56f3c2f35 100644 --- a/packages/editor/src/table-utils/table-desc.ts +++ b/packages/editor/src/table-utils/table-desc.ts @@ -132,7 +132,10 @@ export class TableDesc { } // <--- validation - const desc = new this(rows.length, rows[0].cells.length, rows, baseOffset); + const rawHeaderRows = Number(table.attrs['data-header-rows']) || 0; + const headerRows = Math.max(0, Math.min(rawHeaderRows, rows.length)); + + const desc = new this(rows.length, rows[0].cells.length, rows, baseOffset, headerRows); this.__cache.set(table, desc); return desc; } @@ -146,6 +149,7 @@ export class TableDesc { readonly cols: number, readonly rowsDesc: readonly TableRowDesc[], readonly baseOffset: number, + readonly headerRows: number, /* eslint-enable @typescript-eslint/parameter-properties */ ) {} @@ -153,6 +157,10 @@ export class TableDesc { return new TableDescBinded(pos, this); } + isHeaderRow(rowIdx: number): boolean { + return this.headerRows > 0 && rowIdx >= 0 && rowIdx < this.headerRows; + } + rowHasVirtualCells(rowIndex: number): boolean { return this.rowsDesc[rowIndex]?.cells.some((cell) => cell.type === 'virtual'); } diff --git a/packages/editor/src/utils/transaction.ts b/packages/editor/src/utils/transaction.ts index 200655f27..05f4620a3 100644 --- a/packages/editor/src/utils/transaction.ts +++ b/packages/editor/src/utils/transaction.ts @@ -1,6 +1,56 @@ import type {Node} from '#pm/model'; import type {Transaction} from '#pm/state'; -import {AttrStep} from '#pm/transform'; +import { + AddMarkStep, + AddNodeMarkStep, + AttrStep, + DocAttrStep, + RemoveMarkStep, + RemoveNodeMarkStep, + ReplaceStep, +} from '#pm/transform'; + +/** + * Returns true if the transaction is a single text-insertion step + * (e.g. typing one character). + */ +export function isTextInsertTr(tr: Transaction): boolean { + if (tr.steps.length !== 1) return false; + const [step] = tr.steps; + return ( + step instanceof ReplaceStep && + step.from === step.to && + step.slice.content.childCount === 1 && + step.slice.content.child(0).type.name === 'text' + ); +} + +/** + * Returns true if the transaction is a single-character deletion + * (deletion of exactly one position, no replacement content). + */ +export function isSingleCharDeleteTr(tr: Transaction): boolean { + if (tr.steps.length !== 1) return false; + const [step] = tr.steps; + return step instanceof ReplaceStep && step.to - step.from === 1 && step.slice.size === 0; +} + +const safeSteps = [AddMarkStep, AddNodeMarkStep, DocAttrStep, RemoveMarkStep, RemoveNodeMarkStep]; + +/** + * Returns true if the transaction does not change the structural shape + * of the document — i.e. the relative order/nesting of blocks and node + * attrs (outside of marks/doc-attrs) stay the same. Absolute positions + * may still shift due to text being inserted or removed within textblocks. + * Such transactions only insert/delete text within a textblock or toggle + * marks/doc-attrs. + */ +export function isNonStructuralTr(tr: Transaction): boolean { + if (isTextInsertTr(tr) || isSingleCharDeleteTr(tr)) return true; + if (tr.steps.every((step) => safeSteps.some((SafeStep) => step instanceof SafeStep))) + return true; + return false; +} /** @internal */ export function getChangedRanges(tr: Transaction): {from: number; to: number}[] { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3be70448f..b1ab2cb88 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,16 +52,16 @@ catalogs: version: 2.0.0 '@diplodoc/page-constructor-extension': specifier: ^0.13.3 - version: 0.13.2 + version: 0.13.3 '@diplodoc/quote-link-extension': specifier: 0.1.3 version: 0.1.3 '@diplodoc/tabs-extension': specifier: ^3.7.5 - version: 3.8.0 + version: 3.10.0 '@diplodoc/transform': - specifier: 4.69.0 - version: 4.69.0 + specifier: 4.75.1 + version: 4.75.1 peer-gravity: '@gravity-ui/components': specifier: 4.10.0 @@ -154,7 +154,7 @@ importers: version: 0.1.2 '@diplodoc/html-extension': specifier: catalog:peer-diplodoc - version: 2.9.6(@diplodoc/transform@4.69.0(@types/markdown-it@13.0.9)(highlight.js@11.8.0)(react@18.2.0))(@types/markdown-it@13.0.9)(markdown-it@13.0.2)(react@18.2.0) + version: 2.9.6(@diplodoc/transform@4.75.1(@types/markdown-it@13.0.9)(highlight.js@11.8.0)(react@18.2.0))(@types/markdown-it@13.0.9)(markdown-it@13.0.2)(react@18.2.0) '@diplodoc/latex-extension': specifier: catalog:peer-diplodoc version: 1.4.1(katex@0.16.27)(markdown-it@13.0.2)(react@18.2.0) @@ -163,16 +163,16 @@ importers: version: 2.0.0(markdown-it@13.0.2)(react@18.2.0) '@diplodoc/page-constructor-extension': specifier: catalog:peer-diplodoc - version: 0.13.2(@diplodoc/transform@4.69.0(@types/markdown-it@13.0.9)(highlight.js@11.8.0)(react@18.2.0))(@gravity-ui/uikit@7.13.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/markdown-it@13.0.9)(@types/react@18.0.28)(markdown-it@13.0.2)(react-dom@18.2.0(react@18.2.0))(react-is@18.3.1)(react@18.2.0) + version: 0.13.3(@diplodoc/transform@4.75.1(@types/markdown-it@13.0.9)(highlight.js@11.8.0)(react@18.2.0))(@gravity-ui/uikit@7.13.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/markdown-it@13.0.9)(@types/react@18.0.28)(markdown-it@13.0.2)(react-dom@18.2.0(react@18.2.0))(react-is@18.3.1)(react@18.2.0) '@diplodoc/quote-link-extension': specifier: catalog:peer-diplodoc version: 0.1.3(react@18.2.0) '@diplodoc/tabs-extension': specifier: catalog:peer-diplodoc - version: 3.8.0(react@18.2.0) + version: 3.10.0(react@18.2.0) '@diplodoc/transform': specifier: catalog:peer-diplodoc - version: 4.69.0(@types/markdown-it@13.0.9)(highlight.js@11.8.0)(react@18.2.0) + version: 4.75.1(@types/markdown-it@13.0.9)(highlight.js@11.8.0)(react@18.2.0) '@gravity-ui/components': specifier: catalog:peer-gravity version: 4.10.0(@gravity-ui/uikit@7.13.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -187,7 +187,7 @@ importers: version: link:../packages/page-constructor-extension '@gravity-ui/page-constructor': specifier: catalog:peer-gravity - version: 7.25.0(@diplodoc/transform@4.69.0(highlight.js@11.8.0)(react@18.2.0))(@gravity-ui/uikit@7.13.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.0.28)(js-yaml@4.1.1)(react-dom@18.2.0(react@18.2.0))(react-is@18.3.1)(react@18.2.0) + version: 7.25.0(@diplodoc/transform@4.75.1(highlight.js@11.8.0)(react@18.2.0))(@gravity-ui/uikit@7.13.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.0.28)(js-yaml@4.1.1)(react-dom@18.2.0(react@18.2.0))(react-is@18.3.1)(react@18.2.0) '@gravity-ui/uikit': specifier: catalog:peer-gravity version: 7.13.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -412,7 +412,7 @@ importers: version: 0.1.1 '@diplodoc/utils': specifier: ^2.1.0 - version: 2.1.0(react@18.2.0) + version: 2.2.2(react@18.2.0) '@floating-ui/react': specifier: ^0.27.16 version: 0.27.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -536,7 +536,7 @@ importers: version: 0.1.2 '@diplodoc/html-extension': specifier: catalog:peer-diplodoc - version: 2.9.6(@diplodoc/transform@4.69.0(@types/markdown-it@13.0.9)(highlight.js@11.8.0)(react@18.2.0))(@types/markdown-it@13.0.9)(markdown-it@13.0.2)(react@18.2.0) + version: 2.9.6(@diplodoc/transform@4.75.1(@types/markdown-it@13.0.9)(highlight.js@11.8.0)(react@18.2.0))(@types/markdown-it@13.0.9)(markdown-it@13.0.2)(react@18.2.0) '@diplodoc/latex-extension': specifier: catalog:peer-diplodoc version: 1.4.1(katex@0.16.27)(markdown-it@13.0.2)(react@18.2.0) @@ -548,13 +548,13 @@ importers: version: 0.1.3(react@18.2.0) '@diplodoc/tabs-extension': specifier: catalog:peer-diplodoc - version: 3.8.0(react@18.2.0) + version: 3.10.0(react@18.2.0) '@diplodoc/themes': specifier: ^1.0.0 version: 1.2.0 '@diplodoc/transform': specifier: catalog:peer-diplodoc - version: 4.69.0(@types/markdown-it@13.0.9)(highlight.js@11.8.0)(react@18.2.0) + version: 4.75.1(@types/markdown-it@13.0.9)(highlight.js@11.8.0)(react@18.2.0) '@gravity-ui/components': specifier: catalog:peer-gravity version: 4.10.0(@gravity-ui/uikit@7.13.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -724,13 +724,13 @@ importers: devDependencies: '@diplodoc/page-constructor-extension': specifier: catalog:peer-diplodoc - version: 0.13.2(@diplodoc/transform@4.69.0(@types/markdown-it@13.0.9)(highlight.js@11.8.0)(react@18.2.0))(@gravity-ui/uikit@7.13.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/markdown-it@13.0.9)(@types/react@18.0.28)(markdown-it@13.0.2)(react-dom@18.2.0(react@18.2.0))(react-is@18.3.1)(react@18.2.0) + version: 0.13.3(@diplodoc/transform@4.75.1(@types/markdown-it@13.0.9)(highlight.js@11.8.0)(react@18.2.0))(@gravity-ui/uikit@7.13.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/markdown-it@13.0.9)(@types/react@18.0.28)(markdown-it@13.0.2)(react-dom@18.2.0(react@18.2.0))(react-is@18.3.1)(react@18.2.0) '@gravity-ui/markdown-editor': specifier: workspace:* version: link:../editor '@gravity-ui/page-constructor': specifier: catalog:peer-gravity - version: 7.25.0(@diplodoc/transform@4.69.0(highlight.js@11.8.0)(react@18.2.0))(@gravity-ui/uikit@7.13.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.0.28)(js-yaml@4.1.1)(react-dom@18.2.0(react@18.2.0))(react-is@18.3.1)(react@18.2.0) + version: 7.25.0(@diplodoc/transform@4.75.1(highlight.js@11.8.0)(react@18.2.0))(@gravity-ui/uikit@7.13.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.0.28)(js-yaml@4.1.1)(react-dom@18.2.0(react@18.2.0))(react-is@18.3.1)(react@18.2.0) '@gravity-ui/uikit': specifier: catalog:peer-gravity version: 7.13.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -1683,8 +1683,8 @@ packages: react: optional: true - '@diplodoc/page-constructor-extension@0.13.2': - resolution: {integrity: sha512-D18FjFwY+pxEAiNBUkW0B2p1OK2IRPh2ZiBm4qiWT7hswmPbenFDyKfG+Lwj2Gb7ecBcdZAXwjoNeolKb2FJIQ==} + '@diplodoc/page-constructor-extension@0.13.3': + resolution: {integrity: sha512-ND0B4soYIkK0TboewloqphtVukYjCDQ/DvjVJ6uJsWmw2slji0s9JgNmK3r2TJGZaT1id5bt7r4ZcdRx09xfsg==} engines: {node: '>=18', npm: '>=11.5.1'} peerDependencies: '@diplodoc/transform': ^4.62.0 @@ -1695,8 +1695,8 @@ packages: '@diplodoc/quote-link-extension@0.1.3': resolution: {integrity: sha512-1Q/pNkg7tOFuxhDny2iRxL6dSyXOarKFBzx0RyTjMipKhVFOeonFMT3XoeO34Wns7loi88CTO+jJCsqc3K4BUw==} - '@diplodoc/tabs-extension@3.8.0': - resolution: {integrity: sha512-Ar6YGC+VWFX1dRCZ5wYVlKcOf1lqzknsAVnyW5Bian4s6Vp0llLmGtY7n4bbN7IQdIT5X5OU0mBnbDwA9X+9nA==} + '@diplodoc/tabs-extension@3.10.0': + resolution: {integrity: sha512-3Ky6FiGpoQgc5A/6R82y6kuTY0dMMImrFNW0ktPxAfs9CSWqpaPMWR2ZD2dy6Q0ks8mThLNJ4GjQn6vkwMuF9w==} engines: {node: '>=22', npm: '>=11.5.1'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -1707,8 +1707,8 @@ packages: '@diplodoc/themes@1.2.0': resolution: {integrity: sha512-fb1iPz9TN0PjLXaKT4fudJxoBPdyR6AxYG9yp40a+A833vGtIKXBLrrKhNWk/eGdAheTgROtGk5N6o2vYnnltg==} - '@diplodoc/transform@4.69.0': - resolution: {integrity: sha512-CVGh0Yirp/pY04+4JDjwr2TT75JSzi9JcncqdbZdrDI+IgxEI/sf93geawtUdu6ahxf/frKovJ/i5kwG7enKgw==} + '@diplodoc/transform@4.75.1': + resolution: {integrity: sha512-/GTGBtgd3UGnCHVmVFZaV8B+W3aBJp9ELQOO9kb6QlBz/fgw1IH/0/8crEW65lKj7vK1SXYyMZ0YN+cxU3zbpw==} engines: {npm: '>=11.5.1'} peerDependencies: highlight.js: ^10.0.3 || ^11 @@ -1716,8 +1716,9 @@ packages: highlight.js: optional: true - '@diplodoc/utils@2.1.0': - resolution: {integrity: sha512-1XfZSb0gPLqSRGwxlLHcXo4c59bcFomcEaDM5v2S/aFDhgNRfZgDGxWEbHwkIijfBB2rvFWuVgKzON0VDp2uqQ==} + '@diplodoc/utils@2.2.2': + resolution: {integrity: sha512-j6AgDFuxo7CVbs3EteJSqxGDwjCGuMfOGFSFbx+0rc90tj1qvgYMkmGQQdcUkL/aCFU/0JokCrD+4nSjIcAKKw==} + engines: {npm: '>=11.5.1'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: @@ -3596,9 +3597,6 @@ packages: ajv@6.14.0: resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} - ajv@8.17.1: - resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} - ajv@8.18.0: resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} @@ -6316,9 +6314,6 @@ packages: lodash.upperfirst@4.3.1: resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - lodash@4.17.23: resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} @@ -7132,9 +7127,6 @@ packages: raf-schd@4.0.3: resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} - randombytes@2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -7480,9 +7472,6 @@ packages: safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - safe-push-apply@1.0.0: resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} engines: {node: '>= 0.4'} @@ -7563,9 +7552,6 @@ packages: engines: {node: '>=10'} hasBin: true - serialize-javascript@6.0.2: - resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} - set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -7937,22 +7923,6 @@ packages: resolution: {integrity: sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg==} engines: {node: '>=6.0.0'} - terser-webpack-plugin@5.3.16: - resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==} - engines: {node: '>= 10.13.0'} - peerDependencies: - '@swc/core': '*' - esbuild: '*' - uglify-js: '*' - webpack: ^5.1.0 - peerDependenciesMeta: - '@swc/core': - optional: true - esbuild: - optional: true - uglify-js: - optional: true - terser-webpack-plugin@5.4.0: resolution: {integrity: sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==} engines: {node: '>= 10.13.0'} @@ -8272,6 +8242,7 @@ packages: uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true v8-to-istanbul@9.3.0: @@ -9689,7 +9660,7 @@ snapshots: '@diplodoc/cut-extension@1.1.1(@types/markdown-it@13.0.9)(markdown-it@13.0.2)(react@18.2.0)': dependencies: '@diplodoc/directive': 0.3.3(@types/markdown-it@13.0.9)(markdown-it@13.0.2) - '@diplodoc/utils': 2.1.0(react@18.2.0) + '@diplodoc/utils': 2.2.2(react@18.2.0) transitivePeerDependencies: - '@types/markdown-it' - markdown-it @@ -9711,13 +9682,13 @@ snapshots: '@diplodoc/folding-headings-extension@0.1.2': {} - '@diplodoc/html-extension@2.9.6(@diplodoc/transform@4.69.0(@types/markdown-it@13.0.9)(highlight.js@11.8.0)(react@18.2.0))(@types/markdown-it@13.0.9)(markdown-it@13.0.2)(react@18.2.0)': + '@diplodoc/html-extension@2.9.6(@diplodoc/transform@4.75.1(@types/markdown-it@13.0.9)(highlight.js@11.8.0)(react@18.2.0))(@types/markdown-it@13.0.9)(markdown-it@13.0.2)(react@18.2.0)': dependencies: '@diplodoc/directive': 0.3.3(@types/markdown-it@13.0.9)(markdown-it@13.0.2) markdown-it: 13.0.2 parse5: 8.0.1 optionalDependencies: - '@diplodoc/transform': 4.69.0(@types/markdown-it@13.0.9)(highlight.js@11.8.0)(react@18.2.0) + '@diplodoc/transform': 4.75.1(@types/markdown-it@13.0.9)(highlight.js@11.8.0)(react@18.2.0) react: 18.2.0 transitivePeerDependencies: - '@types/markdown-it' @@ -9739,12 +9710,12 @@ snapshots: optionalDependencies: react: 18.2.0 - '@diplodoc/page-constructor-extension@0.13.2(@diplodoc/transform@4.69.0(@types/markdown-it@13.0.9)(highlight.js@11.8.0)(react@18.2.0))(@gravity-ui/uikit@7.13.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/markdown-it@13.0.9)(@types/react@18.0.28)(markdown-it@13.0.2)(react-dom@18.2.0(react@18.2.0))(react-is@18.3.1)(react@18.2.0)': + '@diplodoc/page-constructor-extension@0.13.3(@diplodoc/transform@4.75.1(@types/markdown-it@13.0.9)(highlight.js@11.8.0)(react@18.2.0))(@gravity-ui/uikit@7.13.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/markdown-it@13.0.9)(@types/react@18.0.28)(markdown-it@13.0.2)(react-dom@18.2.0(react@18.2.0))(react-is@18.3.1)(react@18.2.0)': dependencies: '@diplodoc/directive': 0.3.3(@types/markdown-it@13.0.9)(markdown-it@13.0.2) - '@diplodoc/transform': 4.69.0(@types/markdown-it@13.0.9)(highlight.js@11.8.0)(react@18.2.0) - '@diplodoc/utils': 2.1.0(react@18.2.0) - '@gravity-ui/page-constructor': 7.25.0(@diplodoc/transform@4.69.0(highlight.js@11.8.0)(react@18.2.0))(@gravity-ui/uikit@7.13.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.0.28)(js-yaml@4.1.1)(react-dom@18.2.0(react@18.2.0))(react-is@18.3.1)(react@18.2.0) + '@diplodoc/transform': 4.75.1(@types/markdown-it@13.0.9)(highlight.js@11.8.0)(react@18.2.0) + '@diplodoc/utils': 2.2.2(react@18.2.0) + '@gravity-ui/page-constructor': 7.25.0(@diplodoc/transform@4.75.1(highlight.js@11.8.0)(react@18.2.0))(@gravity-ui/uikit@7.13.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.0.28)(js-yaml@4.1.1)(react-dom@18.2.0(react@18.2.0))(react-is@18.3.1)(react@18.2.0) cheerio: 1.0.0 js-yaml: 4.1.1 lodash: 4.17.23 @@ -9760,22 +9731,22 @@ snapshots: '@diplodoc/quote-link-extension@0.1.3(react@18.2.0)': dependencies: - '@diplodoc/utils': 2.1.0(react@18.2.0) + '@diplodoc/utils': 2.2.2(react@18.2.0) transitivePeerDependencies: - react - '@diplodoc/tabs-extension@3.8.0(react@18.2.0)': + '@diplodoc/tabs-extension@3.10.0(react@18.2.0)': optionalDependencies: react: 18.2.0 '@diplodoc/themes@1.2.0': {} - '@diplodoc/transform@4.69.0(@types/markdown-it@13.0.9)(highlight.js@11.8.0)(react@18.2.0)': + '@diplodoc/transform@4.75.1(@types/markdown-it@13.0.9)(highlight.js@11.8.0)(react@18.2.0)': dependencies: '@diplodoc/cut-extension': 1.1.1(@types/markdown-it@13.0.9)(markdown-it@13.0.2)(react@18.2.0) '@diplodoc/file-extension': 0.2.2(@types/markdown-it@13.0.9)(markdown-it@13.0.2) - '@diplodoc/tabs-extension': 3.8.0(react@18.2.0) - '@diplodoc/utils': 2.1.0(react@18.2.0) + '@diplodoc/tabs-extension': 3.10.0(react@18.2.0) + '@diplodoc/utils': 2.2.2(react@18.2.0) chalk: 4.1.2 cheerio: 1.0.0 css: 3.0.0 @@ -9783,7 +9754,7 @@ snapshots: get-root-node-polyfill: 1.0.0 github-slugger: 1.5.0 js-yaml: 4.1.1 - lodash: 4.17.21 + lodash: 4.17.23 markdown-it: 13.0.2 markdown-it-attrs: 4.2.0(markdown-it@13.0.2) markdown-it-deflist: 2.1.0 @@ -9801,7 +9772,7 @@ snapshots: - '@types/markdown-it' - react - '@diplodoc/utils@2.1.0(react@18.2.0)': + '@diplodoc/utils@2.2.2(react@18.2.0)': optionalDependencies: react: 18.2.0 @@ -10215,10 +10186,10 @@ snapshots: optionalDependencies: react: 18.2.0 - '@gravity-ui/page-constructor@7.25.0(@diplodoc/transform@4.69.0(highlight.js@11.8.0)(react@18.2.0))(@gravity-ui/uikit@7.13.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.0.28)(js-yaml@4.1.1)(react-dom@18.2.0(react@18.2.0))(react-is@18.3.1)(react@18.2.0)': + '@gravity-ui/page-constructor@7.25.0(@diplodoc/transform@4.75.1(highlight.js@11.8.0)(react@18.2.0))(@gravity-ui/uikit@7.13.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.0.28)(js-yaml@4.1.1)(react-dom@18.2.0(react@18.2.0))(react-is@18.3.1)(react@18.2.0)': dependencies: '@bem-react/classname': 1.6.0 - '@diplodoc/transform': 4.69.0(@types/markdown-it@13.0.9)(highlight.js@11.8.0)(react@18.2.0) + '@diplodoc/transform': 4.75.1(@types/markdown-it@13.0.9)(highlight.js@11.8.0)(react@18.2.0) '@gravity-ui/components': 4.10.0(@gravity-ui/uikit@7.13.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@gravity-ui/dynamic-forms': 5.13.2(@gravity-ui/uikit@7.13.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.0.28)(final-form@4.20.10)(react-dom@18.2.0(react@18.2.0))(react-final-form@6.5.9(final-form@4.20.10)(react@18.2.0))(react-is@18.3.1)(react@18.2.0) '@gravity-ui/i18n': 1.8.0 @@ -10907,7 +10878,7 @@ snapshots: magic-string: 0.30.21 storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) style-loader: 4.0.0(webpack@5.106.2(esbuild@0.27.3)) - terser-webpack-plugin: 5.3.16(esbuild@0.27.3)(webpack@5.106.2(esbuild@0.27.3)) + terser-webpack-plugin: 5.4.0(esbuild@0.27.3)(webpack@5.106.2(esbuild@0.27.3)) ts-dedent: 2.2.0 webpack: 5.106.2(esbuild@0.27.3) webpack-dev-middleware: 6.1.3(webpack@5.106.2(esbuild@0.27.3)) @@ -11784,19 +11755,14 @@ snapshots: transitivePeerDependencies: - supports-color - ajv-formats@2.1.1(ajv@8.17.1): + ajv-formats@2.1.1(ajv@8.18.0): optionalDependencies: - ajv: 8.17.1 + ajv: 8.18.0 ajv-keywords@3.5.2(ajv@6.14.0): dependencies: ajv: 6.14.0 - ajv-keywords@5.1.0(ajv@8.17.1): - dependencies: - ajv: 8.17.1 - fast-deep-equal: 3.1.3 - ajv-keywords@5.1.0(ajv@8.18.0): dependencies: ajv: 8.18.0 @@ -11809,13 +11775,6 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ajv@8.17.1: - dependencies: - fast-deep-equal: 3.1.3 - fast-uri: 3.1.0 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 @@ -15144,8 +15103,6 @@ snapshots: lodash.upperfirst@4.3.1: {} - lodash@4.17.21: {} - lodash@4.17.23: {} log-symbols@4.1.0: @@ -16037,10 +15994,6 @@ snapshots: raf-schd@4.0.3: {} - randombytes@2.1.0: - dependencies: - safe-buffer: 5.2.1 - range-parser@1.2.1: {} rc-slider@11.1.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0): @@ -16470,8 +16423,6 @@ snapshots: safe-buffer@5.1.2: {} - safe-buffer@5.2.1: {} - safe-push-apply@1.0.0: dependencies: es-errors: 1.3.0 @@ -16539,9 +16490,9 @@ snapshots: schema-utils@4.3.3: dependencies: '@types/json-schema': 7.0.15 - ajv: 8.17.1 - ajv-formats: 2.1.1(ajv@8.17.1) - ajv-keywords: 5.1.0(ajv@8.17.1) + ajv: 8.18.0 + ajv-formats: 2.1.1(ajv@8.18.0) + ajv-keywords: 5.1.0(ajv@8.18.0) screenfull@5.2.0: {} @@ -16555,10 +16506,6 @@ snapshots: semver@7.7.4: {} - serialize-javascript@6.0.2: - dependencies: - randombytes: 2.1.0 - set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -17048,17 +16995,6 @@ snapshots: dependencies: rimraf: 2.6.3 - terser-webpack-plugin@5.3.16(esbuild@0.27.3)(webpack@5.106.2(esbuild@0.27.3)): - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - jest-worker: 27.5.1 - schema-utils: 4.3.3 - serialize-javascript: 6.0.2 - terser: 5.46.0 - webpack: 5.106.2(esbuild@0.27.3) - optionalDependencies: - esbuild: 0.27.3 - terser-webpack-plugin@5.4.0(esbuild@0.27.3)(webpack@5.106.2(esbuild@0.27.3)): dependencies: '@jridgewell/trace-mapping': 0.3.31 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d8da4ff23..16d295a50 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -26,7 +26,7 @@ catalogs: '@diplodoc/page-constructor-extension': ^0.13.3 '@diplodoc/quote-link-extension': 0.1.3 '@diplodoc/tabs-extension': ^3.7.5 - '@diplodoc/transform': 4.69.0 + '@diplodoc/transform': 4.75.1 peer-gravity: '@gravity-ui/components': 4.10.0