Add QWord table structure commands
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import type { CSSProperties } from "react";
|
||||
import type { CSSProperties, MouseEvent } from "react";
|
||||
import {
|
||||
AlignCenter,
|
||||
AlignJustify,
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
AlignRight,
|
||||
Bold,
|
||||
ClipboardPaste,
|
||||
Columns2,
|
||||
Copy,
|
||||
Download,
|
||||
FileText,
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
Superscript,
|
||||
Table2,
|
||||
Type,
|
||||
Rows3,
|
||||
Underline
|
||||
} from "lucide-react";
|
||||
import type { WordDocument } from "../../types";
|
||||
@@ -45,6 +47,12 @@ interface QWordProps {
|
||||
onStatusChange?: (status: QWordStatus) => void;
|
||||
}
|
||||
|
||||
interface TableCellLocation {
|
||||
tableIndex: number;
|
||||
rowIndex: number;
|
||||
cellIndex: number;
|
||||
}
|
||||
|
||||
export interface QWordStatus {
|
||||
currentPage: number;
|
||||
pageCount: number;
|
||||
@@ -190,6 +198,55 @@ function qwordTableHtml(rows: number, columns: number) {
|
||||
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 }) {
|
||||
const pageCount = Math.max(1, input.pageCount);
|
||||
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) {
|
||||
const stageRef = useRef<HTMLElement>(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 imageInputRef = useRef<HTMLInputElement>(null);
|
||||
const [activeRibbonTab, setActiveRibbonTab] = useState<WordRibbonTabId>("home");
|
||||
@@ -260,6 +320,25 @@ export function QWord({ document, zoom = 100, onChange, onStatusChange }: QWordP
|
||||
return () => observer.disconnect();
|
||||
}, [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(() => {
|
||||
onStatusChange?.({ currentPage: pageView.current, pageCount: pageView.count, 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>');
|
||||
}
|
||||
|
||||
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() {
|
||||
const editor = editorRef.current;
|
||||
if (!editor) {
|
||||
@@ -364,9 +508,13 @@ export function QWord({ document, zoom = 100, onChange, onStatusChange }: QWordP
|
||||
if (range && (range.commonAncestorContainer === editor || editor.contains(range.commonAncestorContainer))) {
|
||||
range.deleteContents();
|
||||
const fragment = globalThis.document.createRange().createContextualFragment(html);
|
||||
const firstCell = fragment.querySelector("td,th") as HTMLTableCellElement | null;
|
||||
const lastNode = fragment.lastChild;
|
||||
range.insertNode(fragment);
|
||||
if (lastNode) {
|
||||
if (firstCell) {
|
||||
queueActiveTableCellRestore(firstCell);
|
||||
placeCaretInTableCell(firstCell);
|
||||
} else if (lastNode) {
|
||||
range.setStartAfter(lastNode);
|
||||
range.collapse(true);
|
||||
selection?.removeAllRanges();
|
||||
@@ -374,11 +522,77 @@ export function QWord({ document, zoom = 100, onChange, onStatusChange }: QWordP
|
||||
}
|
||||
} else {
|
||||
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();
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!file) {
|
||||
return;
|
||||
@@ -406,6 +620,7 @@ export function QWord({ document, zoom = 100, onChange, onStatusChange }: QWordP
|
||||
}
|
||||
|
||||
function updateHtml() {
|
||||
rememberActiveTableCell();
|
||||
if (editorRef.current) {
|
||||
onChange({ ...document, html: editorRef.current.innerHTML });
|
||||
refreshPageView();
|
||||
@@ -717,10 +932,18 @@ export function QWord({ document, zoom = 100, onChange, onStatusChange }: QWordP
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<button className="compact-command" type="button" onClick={insertTable}>
|
||||
<button className="compact-command" type="button" onMouseDown={keepEditorSelection} onClick={insertTable}>
|
||||
<Table2 size={16} />
|
||||
Таблица
|
||||
</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>
|
||||
<span className="word-ribbon-title">Таблица</span>
|
||||
</section>
|
||||
@@ -782,6 +1005,10 @@ export function QWord({ document, zoom = 100, onChange, onStatusChange }: QWordP
|
||||
contentEditable
|
||||
suppressContentEditableWarning
|
||||
onInput={updateHtml}
|
||||
onFocus={rememberActiveTableCell}
|
||||
onKeyUp={rememberActiveTableCell}
|
||||
onMouseDown={rememberActiveTableCellFromEvent}
|
||||
onMouseUp={rememberActiveTableCellFromEvent}
|
||||
dangerouslySetInnerHTML={{ __html: document.html }}
|
||||
aria-label="Содержимое документа"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user