Add QWord table structure commands
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
|
import { useState } from "react";
|
||||||
import { afterEach, describe, expect, it } from "vitest";
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
import type { WordDocument } from "../../types";
|
import type { WordDocument } from "../../types";
|
||||||
@@ -141,6 +142,131 @@ describe("QWord", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("сохраняет активную ячейку после вставки таблицы и перерендера", () => {
|
||||||
|
const originalResizeObserver = globalThis.ResizeObserver;
|
||||||
|
const originalRequestAnimationFrame = window.requestAnimationFrame;
|
||||||
|
const onChange = vi.fn();
|
||||||
|
|
||||||
|
globalThis.ResizeObserver = class {
|
||||||
|
observe() {}
|
||||||
|
disconnect() {}
|
||||||
|
} as unknown as typeof ResizeObserver;
|
||||||
|
window.requestAnimationFrame = ((callback: FrameRequestCallback) => {
|
||||||
|
callback(0);
|
||||||
|
return 1;
|
||||||
|
}) as typeof window.requestAnimationFrame;
|
||||||
|
|
||||||
|
const document: WordDocument = {
|
||||||
|
id: "word-insert-table-restore-test",
|
||||||
|
title: "Активная таблица.docx",
|
||||||
|
updatedAt: "2026-06-02T00:00:00.000Z",
|
||||||
|
html: "<p>Перед таблицей</p>"
|
||||||
|
};
|
||||||
|
|
||||||
|
function StatefulQWord() {
|
||||||
|
const [currentDocument, setCurrentDocument] = useState(document);
|
||||||
|
return (
|
||||||
|
<QWord
|
||||||
|
document={currentDocument}
|
||||||
|
onChange={(nextDocument) => {
|
||||||
|
onChange(nextDocument);
|
||||||
|
setCurrentDocument(nextDocument);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
render(<StatefulQWord />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("tab", { name: "Вставка" }));
|
||||||
|
const tableButton = screen.getByRole("button", { name: "Таблица" });
|
||||||
|
expect(fireEvent.mouseDown(tableButton)).toBe(false);
|
||||||
|
fireEvent.click(tableButton);
|
||||||
|
|
||||||
|
const rowButton = screen.getByRole("button", { name: "Строка ниже" });
|
||||||
|
expect(fireEvent.mouseDown(rowButton)).toBe(false);
|
||||||
|
fireEvent.click(rowButton);
|
||||||
|
|
||||||
|
const updatedDocument = onChange.mock.calls[onChange.mock.calls.length - 1]?.[0] as WordDocument | undefined;
|
||||||
|
expect(updatedDocument?.html.match(/<tr/g)).toHaveLength(4);
|
||||||
|
expect(updatedDocument?.html.match(/<td/g)).toHaveLength(12);
|
||||||
|
} finally {
|
||||||
|
globalThis.ResizeObserver = originalResizeObserver;
|
||||||
|
window.requestAnimationFrame = originalRequestAnimationFrame;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("добавляет строку и столбец к выбранной таблице", () => {
|
||||||
|
const originalResizeObserver = globalThis.ResizeObserver;
|
||||||
|
const originalRequestAnimationFrame = window.requestAnimationFrame;
|
||||||
|
const onChange = vi.fn();
|
||||||
|
|
||||||
|
globalThis.ResizeObserver = class {
|
||||||
|
observe() {}
|
||||||
|
disconnect() {}
|
||||||
|
} as unknown as typeof ResizeObserver;
|
||||||
|
window.requestAnimationFrame = ((callback: FrameRequestCallback) => {
|
||||||
|
callback(0);
|
||||||
|
return 1;
|
||||||
|
}) as typeof window.requestAnimationFrame;
|
||||||
|
|
||||||
|
const document: WordDocument = {
|
||||||
|
id: "word-table-structure-test",
|
||||||
|
title: "Структура таблицы.docx",
|
||||||
|
updatedAt: "2026-06-02T00:00:00.000Z",
|
||||||
|
html: "<table><tbody><tr><td>A</td><td>B</td></tr></tbody></table>"
|
||||||
|
};
|
||||||
|
|
||||||
|
function selectCell(text: string) {
|
||||||
|
const cell = screen.getByText(text).closest("td");
|
||||||
|
expect(cell).toBeInTheDocument();
|
||||||
|
const range = window.document.createRange();
|
||||||
|
range.selectNodeContents(cell as HTMLTableCellElement);
|
||||||
|
window.getSelection()?.removeAllRanges();
|
||||||
|
window.getSelection()?.addRange(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatefulQWord() {
|
||||||
|
const [currentDocument, setCurrentDocument] = useState(document);
|
||||||
|
return (
|
||||||
|
<QWord
|
||||||
|
document={currentDocument}
|
||||||
|
onChange={(nextDocument) => {
|
||||||
|
onChange(nextDocument);
|
||||||
|
setCurrentDocument(nextDocument);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
render(<StatefulQWord />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("tab", { name: "Вставка" }));
|
||||||
|
selectCell("A");
|
||||||
|
const rowButton = screen.getByRole("button", { name: "Строка ниже" });
|
||||||
|
expect(fireEvent.mouseDown(rowButton)).toBe(false);
|
||||||
|
fireEvent.click(rowButton);
|
||||||
|
|
||||||
|
let updatedDocument = onChange.mock.calls[onChange.mock.calls.length - 1]?.[0] as WordDocument | undefined;
|
||||||
|
expect(updatedDocument?.html.match(/<tr/g)).toHaveLength(2);
|
||||||
|
expect(updatedDocument?.html.match(/<td/g)).toHaveLength(4);
|
||||||
|
|
||||||
|
selectCell("A");
|
||||||
|
const columnButton = screen.getByRole("button", { name: "Столбец справа" });
|
||||||
|
expect(fireEvent.mouseDown(columnButton)).toBe(false);
|
||||||
|
fireEvent.click(columnButton);
|
||||||
|
|
||||||
|
updatedDocument = onChange.mock.calls[onChange.mock.calls.length - 1]?.[0] as WordDocument | undefined;
|
||||||
|
expect(updatedDocument?.html.match(/<tr/g)).toHaveLength(2);
|
||||||
|
expect(updatedDocument?.html.match(/<td/g)).toHaveLength(6);
|
||||||
|
} finally {
|
||||||
|
globalThis.ResizeObserver = originalResizeObserver;
|
||||||
|
window.requestAnimationFrame = originalRequestAnimationFrame;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("обновляет текущую страницу в статусе при прокрутке документа", async () => {
|
it("обновляет текущую страницу в статусе при прокрутке документа", async () => {
|
||||||
const originalResizeObserver = globalThis.ResizeObserver;
|
const originalResizeObserver = globalThis.ResizeObserver;
|
||||||
const originalRequestAnimationFrame = window.requestAnimationFrame;
|
const originalRequestAnimationFrame = window.requestAnimationFrame;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useLayoutEffect, useMemo, useRef, useState } from "react";
|
import { useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||||
import type { CSSProperties } from "react";
|
import type { CSSProperties, MouseEvent } from "react";
|
||||||
import {
|
import {
|
||||||
AlignCenter,
|
AlignCenter,
|
||||||
AlignJustify,
|
AlignJustify,
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
AlignRight,
|
AlignRight,
|
||||||
Bold,
|
Bold,
|
||||||
ClipboardPaste,
|
ClipboardPaste,
|
||||||
|
Columns2,
|
||||||
Copy,
|
Copy,
|
||||||
Download,
|
Download,
|
||||||
FileText,
|
FileText,
|
||||||
@@ -28,6 +29,7 @@ import {
|
|||||||
Superscript,
|
Superscript,
|
||||||
Table2,
|
Table2,
|
||||||
Type,
|
Type,
|
||||||
|
Rows3,
|
||||||
Underline
|
Underline
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { WordDocument } from "../../types";
|
import type { WordDocument } from "../../types";
|
||||||
@@ -45,6 +47,12 @@ interface QWordProps {
|
|||||||
onStatusChange?: (status: QWordStatus) => void;
|
onStatusChange?: (status: QWordStatus) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TableCellLocation {
|
||||||
|
tableIndex: number;
|
||||||
|
rowIndex: number;
|
||||||
|
cellIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface QWordStatus {
|
export interface QWordStatus {
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
pageCount: number;
|
pageCount: number;
|
||||||
@@ -190,6 +198,55 @@ function qwordTableHtml(rows: number, columns: number) {
|
|||||||
return `<table><tbody>${tableRows}</tbody></table>`;
|
return `<table><tbody>${tableRows}</tbody></table>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createBlankTableCell(tagName: "td" | "th" = "td") {
|
||||||
|
const cell = globalThis.document.createElement(tagName);
|
||||||
|
cell.append(globalThis.document.createElement("br"));
|
||||||
|
return cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tableCellFromSelection(editor: HTMLElement): HTMLTableCellElement | undefined {
|
||||||
|
const node = window.getSelection()?.anchorNode;
|
||||||
|
const element = node instanceof Element ? node : node?.parentElement;
|
||||||
|
const cell = element?.closest("td,th");
|
||||||
|
return cell instanceof HTMLTableCellElement && editor.contains(cell) ? cell : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tableCellLocation(editor: HTMLElement, cell: HTMLTableCellElement): TableCellLocation | undefined {
|
||||||
|
const table = cell.closest("table");
|
||||||
|
const row = cell.parentElement;
|
||||||
|
if (!(table instanceof HTMLTableElement) || !(row instanceof HTMLTableRowElement) || !editor.contains(table)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableIndex = Array.from(editor.querySelectorAll("table")).indexOf(table);
|
||||||
|
const rowIndex = Array.from(table.rows).indexOf(row);
|
||||||
|
const cellIndex = cell.cellIndex;
|
||||||
|
return tableIndex >= 0 && rowIndex >= 0 && cellIndex >= 0 ? { cellIndex, rowIndex, tableIndex } : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tableCellFromLocation(editor: HTMLElement, location: TableCellLocation): HTMLTableCellElement | undefined {
|
||||||
|
const table = Array.from(editor.querySelectorAll("table"))[location.tableIndex];
|
||||||
|
const cell = table?.rows[location.rowIndex]?.cells[location.cellIndex];
|
||||||
|
return cell instanceof HTMLTableCellElement ? cell : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function placeCaretInTableCell(cell: HTMLTableCellElement) {
|
||||||
|
if (cell.childNodes.length === 0) {
|
||||||
|
cell.append(globalThis.document.createElement("br"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const range = globalThis.document.createRange();
|
||||||
|
range.selectNodeContents(cell);
|
||||||
|
range.collapse(true);
|
||||||
|
const selection = window.getSelection();
|
||||||
|
selection?.removeAllRanges();
|
||||||
|
selection?.addRange(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
function keepEditorSelection(event: MouseEvent<HTMLButtonElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
export function qwordCurrentPageFromViewport(input: { stageTop: number; paperTop: number; pageHeight: number; pageCount: number; zoom: number }) {
|
export function qwordCurrentPageFromViewport(input: { stageTop: number; paperTop: number; pageHeight: number; pageCount: number; zoom: number }) {
|
||||||
const pageCount = Math.max(1, input.pageCount);
|
const pageCount = Math.max(1, input.pageCount);
|
||||||
const zoom = Number.isFinite(input.zoom) && input.zoom > 0 ? input.zoom : 1;
|
const zoom = Number.isFinite(input.zoom) && input.zoom > 0 ? input.zoom : 1;
|
||||||
@@ -201,6 +258,9 @@ export function qwordCurrentPageFromViewport(input: { stageTop: number; paperTop
|
|||||||
export function QWord({ document, zoom = 100, onChange, onStatusChange }: QWordProps) {
|
export function QWord({ document, zoom = 100, onChange, onStatusChange }: QWordProps) {
|
||||||
const stageRef = useRef<HTMLElement>(null);
|
const stageRef = useRef<HTMLElement>(null);
|
||||||
const editorRef = useRef<HTMLDivElement>(null);
|
const editorRef = useRef<HTMLDivElement>(null);
|
||||||
|
const activeTableCellRef = useRef<HTMLTableCellElement | null>(null);
|
||||||
|
const activeTableCellLocationRef = useRef<TableCellLocation | null>(null);
|
||||||
|
const restoreActiveTableCellAfterRenderRef = useRef(false);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const imageInputRef = useRef<HTMLInputElement>(null);
|
const imageInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [activeRibbonTab, setActiveRibbonTab] = useState<WordRibbonTabId>("home");
|
const [activeRibbonTab, setActiveRibbonTab] = useState<WordRibbonTabId>("home");
|
||||||
@@ -260,6 +320,25 @@ export function QWord({ document, zoom = 100, onChange, onStatusChange }: QWordP
|
|||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, [document.html, zoom]);
|
}, [document.html, zoom]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!restoreActiveTableCellAfterRenderRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreActiveTableCellAfterRenderRef.current = false;
|
||||||
|
const editor = editorRef.current;
|
||||||
|
const location = activeTableCellLocationRef.current;
|
||||||
|
if (!editor || !location) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const restoredCell = tableCellFromLocation(editor, location);
|
||||||
|
if (restoredCell) {
|
||||||
|
activeTableCellRef.current = restoredCell;
|
||||||
|
placeCaretInTableCell(restoredCell);
|
||||||
|
}
|
||||||
|
}, [document.html]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
onStatusChange?.({ currentPage: pageView.current, pageCount: pageView.count, wordCount });
|
onStatusChange?.({ currentPage: pageView.current, pageCount: pageView.count, wordCount });
|
||||||
}, [onStatusChange, pageView.count, pageView.current, wordCount]);
|
}, [onStatusChange, pageView.count, pageView.current, wordCount]);
|
||||||
@@ -351,6 +430,71 @@ export function QWord({ document, zoom = 100, onChange, onStatusChange }: QWordP
|
|||||||
runCommand("insertHTML", '<span data-qoffice-page-break="true" contenteditable="false"></span><p><br></p>');
|
runCommand("insertHTML", '<span data-qoffice-page-break="true" contenteditable="false"></span><p><br></p>');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setActiveTableCell(cell: HTMLTableCellElement) {
|
||||||
|
const editor = editorRef.current;
|
||||||
|
activeTableCellRef.current = cell;
|
||||||
|
if (editor) {
|
||||||
|
activeTableCellLocationRef.current = tableCellLocation(editor, cell) ?? activeTableCellLocationRef.current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function queueActiveTableCellRestore(cell: HTMLTableCellElement) {
|
||||||
|
setActiveTableCell(cell);
|
||||||
|
restoreActiveTableCellAfterRenderRef.current = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rememberActiveTableCell(): HTMLTableCellElement | undefined {
|
||||||
|
const editor = editorRef.current;
|
||||||
|
if (!editor) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const cell = tableCellFromSelection(editor);
|
||||||
|
if (cell) {
|
||||||
|
setActiveTableCell(cell);
|
||||||
|
}
|
||||||
|
return cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rememberActiveTableCellFromEvent(event: MouseEvent<HTMLElement>) {
|
||||||
|
const editor = editorRef.current;
|
||||||
|
const target = event.target;
|
||||||
|
const cell = target instanceof Element ? target.closest("td,th") : undefined;
|
||||||
|
if (editor && cell instanceof HTMLTableCellElement && editor.contains(cell)) {
|
||||||
|
setActiveTableCell(cell);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rememberActiveTableCell();
|
||||||
|
}
|
||||||
|
|
||||||
|
function activeTableCell(): HTMLTableCellElement | undefined {
|
||||||
|
const editor = editorRef.current;
|
||||||
|
if (!editor) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedCell = rememberActiveTableCell();
|
||||||
|
if (selectedCell) {
|
||||||
|
return selectedCell;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rememberedCell = activeTableCellRef.current;
|
||||||
|
if (rememberedCell && editor.contains(rememberedCell)) {
|
||||||
|
return rememberedCell;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rememberedLocation = activeTableCellLocationRef.current;
|
||||||
|
if (!rememberedLocation) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const restoredCell = tableCellFromLocation(editor, rememberedLocation);
|
||||||
|
if (restoredCell) {
|
||||||
|
activeTableCellRef.current = restoredCell;
|
||||||
|
}
|
||||||
|
return restoredCell;
|
||||||
|
}
|
||||||
|
|
||||||
function insertTable() {
|
function insertTable() {
|
||||||
const editor = editorRef.current;
|
const editor = editorRef.current;
|
||||||
if (!editor) {
|
if (!editor) {
|
||||||
@@ -364,9 +508,13 @@ export function QWord({ document, zoom = 100, onChange, onStatusChange }: QWordP
|
|||||||
if (range && (range.commonAncestorContainer === editor || editor.contains(range.commonAncestorContainer))) {
|
if (range && (range.commonAncestorContainer === editor || editor.contains(range.commonAncestorContainer))) {
|
||||||
range.deleteContents();
|
range.deleteContents();
|
||||||
const fragment = globalThis.document.createRange().createContextualFragment(html);
|
const fragment = globalThis.document.createRange().createContextualFragment(html);
|
||||||
|
const firstCell = fragment.querySelector("td,th") as HTMLTableCellElement | null;
|
||||||
const lastNode = fragment.lastChild;
|
const lastNode = fragment.lastChild;
|
||||||
range.insertNode(fragment);
|
range.insertNode(fragment);
|
||||||
if (lastNode) {
|
if (firstCell) {
|
||||||
|
queueActiveTableCellRestore(firstCell);
|
||||||
|
placeCaretInTableCell(firstCell);
|
||||||
|
} else if (lastNode) {
|
||||||
range.setStartAfter(lastNode);
|
range.setStartAfter(lastNode);
|
||||||
range.collapse(true);
|
range.collapse(true);
|
||||||
selection?.removeAllRanges();
|
selection?.removeAllRanges();
|
||||||
@@ -374,11 +522,77 @@ export function QWord({ document, zoom = 100, onChange, onStatusChange }: QWordP
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
editor.insertAdjacentHTML("beforeend", html);
|
editor.insertAdjacentHTML("beforeend", html);
|
||||||
|
const tables = editor.querySelectorAll("table");
|
||||||
|
const firstCell = tables[tables.length - 1]?.querySelector("td,th") as HTMLTableCellElement | null;
|
||||||
|
if (firstCell) {
|
||||||
|
queueActiveTableCellRestore(firstCell);
|
||||||
|
placeCaretInTableCell(firstCell);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateHtml();
|
updateHtml();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function insertTableRowBelow() {
|
||||||
|
const editor = editorRef.current;
|
||||||
|
if (!editor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cell = activeTableCell();
|
||||||
|
const row = cell?.parentElement;
|
||||||
|
if (!cell || !(row instanceof HTMLTableRowElement)) {
|
||||||
|
window.alert("Поместите курсор в ячейку таблицы.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRow = globalThis.document.createElement("tr");
|
||||||
|
Array.from({ length: Math.max(1, row.cells.length) }, () => createBlankTableCell()).forEach((newCell) => newRow.append(newCell));
|
||||||
|
row.after(newRow);
|
||||||
|
const nextCell = newRow.cells[Math.min(cell.cellIndex, newRow.cells.length - 1)] ?? newRow.cells[0];
|
||||||
|
queueActiveTableCellRestore(nextCell);
|
||||||
|
placeCaretInTableCell(nextCell);
|
||||||
|
updateHtml();
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertTableColumnRight() {
|
||||||
|
const editor = editorRef.current;
|
||||||
|
if (!editor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cell = activeTableCell();
|
||||||
|
const row = cell?.parentElement;
|
||||||
|
const table = cell?.closest("table");
|
||||||
|
if (!cell || !(row instanceof HTMLTableRowElement) || !(table instanceof HTMLTableElement)) {
|
||||||
|
window.alert("Поместите курсор в ячейку таблицы.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnIndex = cell.cellIndex;
|
||||||
|
let selectedNewCell: HTMLTableCellElement | undefined;
|
||||||
|
Array.from(table.rows).forEach((tableRow) => {
|
||||||
|
const referenceCell = tableRow.cells[Math.min(columnIndex, Math.max(0, tableRow.cells.length - 1))];
|
||||||
|
const tagName = referenceCell?.tagName.toLowerCase() === "th" ? "th" : "td";
|
||||||
|
const newCell = createBlankTableCell(tagName);
|
||||||
|
if (referenceCell?.nextSibling) {
|
||||||
|
tableRow.insertBefore(newCell, referenceCell.nextSibling);
|
||||||
|
} else {
|
||||||
|
tableRow.append(newCell);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tableRow === row) {
|
||||||
|
selectedNewCell = newCell;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (selectedNewCell) {
|
||||||
|
queueActiveTableCellRestore(selectedNewCell);
|
||||||
|
placeCaretInTableCell(selectedNewCell);
|
||||||
|
}
|
||||||
|
updateHtml();
|
||||||
|
}
|
||||||
|
|
||||||
async function insertImage(file: File | undefined) {
|
async function insertImage(file: File | undefined) {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return;
|
return;
|
||||||
@@ -406,6 +620,7 @@ export function QWord({ document, zoom = 100, onChange, onStatusChange }: QWordP
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateHtml() {
|
function updateHtml() {
|
||||||
|
rememberActiveTableCell();
|
||||||
if (editorRef.current) {
|
if (editorRef.current) {
|
||||||
onChange({ ...document, html: editorRef.current.innerHTML });
|
onChange({ ...document, html: editorRef.current.innerHTML });
|
||||||
refreshPageView();
|
refreshPageView();
|
||||||
@@ -717,10 +932,18 @@ export function QWord({ document, zoom = 100, onChange, onStatusChange }: QWordP
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<button className="compact-command" type="button" onClick={insertTable}>
|
<button className="compact-command" type="button" onMouseDown={keepEditorSelection} onClick={insertTable}>
|
||||||
<Table2 size={16} />
|
<Table2 size={16} />
|
||||||
Таблица
|
Таблица
|
||||||
</button>
|
</button>
|
||||||
|
<button className="compact-command word-table-structure-command" type="button" onMouseDown={keepEditorSelection} onClick={insertTableRowBelow}>
|
||||||
|
<Rows3 size={16} />
|
||||||
|
Строка ниже
|
||||||
|
</button>
|
||||||
|
<button className="compact-command word-table-structure-command" type="button" onMouseDown={keepEditorSelection} onClick={insertTableColumnRight}>
|
||||||
|
<Columns2 size={16} />
|
||||||
|
Столбец справа
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<span className="word-ribbon-title">Таблица</span>
|
<span className="word-ribbon-title">Таблица</span>
|
||||||
</section>
|
</section>
|
||||||
@@ -782,6 +1005,10 @@ export function QWord({ document, zoom = 100, onChange, onStatusChange }: QWordP
|
|||||||
contentEditable
|
contentEditable
|
||||||
suppressContentEditableWarning
|
suppressContentEditableWarning
|
||||||
onInput={updateHtml}
|
onInput={updateHtml}
|
||||||
|
onFocus={rememberActiveTableCell}
|
||||||
|
onKeyUp={rememberActiveTableCell}
|
||||||
|
onMouseDown={rememberActiveTableCellFromEvent}
|
||||||
|
onMouseUp={rememberActiveTableCellFromEvent}
|
||||||
dangerouslySetInnerHTML={{ __html: document.html }}
|
dangerouslySetInnerHTML={{ __html: document.html }}
|
||||||
aria-label="Содержимое документа"
|
aria-label="Содержимое документа"
|
||||||
/>
|
/>
|
||||||
|
|||||||
+7
-2
@@ -675,7 +675,7 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.word-table-group {
|
.word-table-group {
|
||||||
flex: 0 0 156px;
|
flex: 0 0 236px;
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -701,12 +701,17 @@ button {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.word-table-controls .compact-command {
|
.word-table-controls .compact-command:not(.word-table-structure-command) {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.word-table-structure-command {
|
||||||
|
justify-content: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.word-edit-group {
|
.word-edit-group {
|
||||||
flex: 0 0 118px;
|
flex: 0 0 118px;
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
|||||||
Reference in New Issue
Block a user