Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/editor/src/core/constants/meta.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export enum CORE_EDITOR_META {
SKIP_FILE_DELETION = "skipFileDeletion",
INTENTIONAL_DELETION = "intentionalDeletion",
ADD_TO_HISTORY = "addToHistory",
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import type { Editor } from "@tiptap/core";
import { Fragment, type Node, type Node as ProseMirrorNode } from "@tiptap/pm/model";
import type { Transaction } from "@tiptap/pm/state";
import { type CellSelection, TableMap } from "@tiptap/pm/tables";
// extensions
import { TableNodeLocation } from "@/extensions/table/table/utilities/helpers";

type TableRow = (ProseMirrorNode | null)[];
type TableRows = TableRow[];

/**
* Move the selected columns to the specified index.
* @param {Editor} editor - The editor instance.
* @param {TableNodeLocation} table - The table node location.
* @param {CellSelection} selection - The cell selection.
* @param {number} to - The index to move the columns to.
* @param {Transaction} tr - The transaction.
* @returns {Transaction} The updated transaction.
*/
export const moveSelectedColumns = (
editor: Editor,
table: TableNodeLocation,
selection: CellSelection,
to: number,
tr: Transaction
): Transaction => {
const tableMap = TableMap.get(table.node);

let columnStart = -1;
let columnEnd = -1;

selection.forEachCell((_node, pos) => {
const cell = tableMap.findCell(pos - table.pos - 1);
for (let i = cell.left; i < cell.right; i++) {
columnStart = columnStart >= 0 ? Math.min(cell.left, columnStart) : cell.left;
columnEnd = columnEnd >= 0 ? Math.max(cell.right, columnEnd) : cell.right;
}
});

if (columnStart === -1 || columnEnd === -1) {
console.warn("Invalid column selection");
return tr;
}

if (to < 0 || to > tableMap.width || (to >= columnStart && to < columnEnd)) return tr;

const rows = tableToCells(table);
for (const row of rows) {
const range = row.splice(columnStart, columnEnd - columnStart);
const offset = to > columnStart ? to - (columnEnd - columnStart - 1) : to;
row.splice(offset, 0, ...range);
}

tableFromCells(editor, table, rows, tr);
return tr;
};

/**
* Move the selected rows to the specified index.
* @param {Editor} editor - The editor instance.
* @param {TableNodeLocation} table - The table node location.
* @param {CellSelection} selection - The cell selection.
* @param {number} to - The index to move the rows to.
* @param {Transaction} tr - The transaction.
* @returns {Transaction} The updated transaction.
*/
export const moveSelectedRows = (
editor: Editor,
table: TableNodeLocation,
selection: CellSelection,
to: number,
tr: Transaction
): Transaction => {
const tableMap = TableMap.get(table.node);

let rowStart = -1;
let rowEnd = -1;

selection.forEachCell((_node, pos) => {
const cell = tableMap.findCell(pos - table.pos - 1);
for (let i = cell.top; i < cell.bottom; i++) {
rowStart = rowStart >= 0 ? Math.min(cell.top, rowStart) : cell.top;
rowEnd = rowEnd >= 0 ? Math.max(cell.bottom, rowEnd) : cell.bottom;
}
});

if (rowStart === -1 || rowEnd === -1) {
console.warn("Invalid row selection");
return tr;
}

if (to < 0 || to > tableMap.height || (to >= rowStart && to < rowEnd)) return tr;

const rows = tableToCells(table);
const range = rows.splice(rowStart, rowEnd - rowStart);
const offset = to > rowStart ? to - (rowEnd - rowStart - 1) : to;
rows.splice(offset, 0, ...range);

tableFromCells(editor, table, rows, tr);
return tr;
};

/**
* @description Duplicate the selected rows.
* @param {TableNodeLocation} table - The table node location.
* @param {number[]} rowIndices - The indices of the rows to duplicate.
* @param {Transaction} tr - The transaction.
* @returns {Transaction} The updated transaction.
*/
export const duplicateRows = (table: TableNodeLocation, rowIndices: number[], tr: Transaction): Transaction => {
const rows = tableToCells(table);

const { map, width } = TableMap.get(table.node);

// Validate row indices
const maxRow = rows.length - 1;
if (rowIndices.some((idx) => idx < 0 || idx > maxRow)) {
console.warn("Invalid row indices for duplication");
return tr;
}

const mapStart = tr.mapping.maps.length;

const lastRowPos = map[rowIndices[rowIndices.length - 1] * width + width - 1];
const nextRowStart = lastRowPos + (table.node.nodeAt(lastRowPos)?.nodeSize ?? 0) + 1;
const insertPos = tr.mapping.slice(mapStart).map(table.start + nextRowStart);

for (let i = rowIndices.length - 1; i >= 0; i--) {
tr.insert(
insertPos,
rows[rowIndices[i]].filter((r) => r !== null)
);
}

return tr;
};

/**
* @description Duplicate the selected columns.
* @param {TableNodeLocation} table - The table node location.
* @param {number[]} columnIndices - The indices of the columns to duplicate.
* @param {Transaction} tr - The transaction.
* @returns {Transaction} The updated transaction.
*/
export const duplicateColumns = (table: TableNodeLocation, columnIndices: number[], tr: Transaction): Transaction => {
const rows = tableToCells(table);

const { map, width, height } = TableMap.get(table.node);

// Validate column indices
if (columnIndices.some((idx) => idx < 0 || idx >= width)) {
console.warn("Invalid column indices for duplication");
return tr;
}

const mapStart = tr.mapping.maps.length;

for (let row = 0; row < height; row++) {
const lastColumnPos = map[row * width + columnIndices[columnIndices.length - 1]];
const nextColumnStart = lastColumnPos + (table.node.nodeAt(lastColumnPos)?.nodeSize ?? 0);
const insertPos = tr.mapping.slice(mapStart).map(table.start + nextColumnStart);

for (let i = columnIndices.length - 1; i >= 0; i--) {
const copiedNode = rows[row][columnIndices[i]];
if (copiedNode !== null) {
tr.insert(insertPos, copiedNode);
}
}
}

return tr;
};

/**
* @description Convert the table to cells.
* @param {TableNodeLocation} table - The table node location.
* @returns {TableRows} The table rows.
*/
const tableToCells = (table: TableNodeLocation): TableRows => {
const { map, width, height } = TableMap.get(table.node);

const visitedCells = new Set<number>();
const rows: TableRows = [];
for (let row = 0; row < height; row++) {
const cells: (ProseMirrorNode | null)[] = [];
for (let col = 0; col < width; col++) {
const pos = map[row * width + col];
cells.push(!visitedCells.has(pos) ? table.node.nodeAt(pos) : null);
visitedCells.add(pos);
}
rows.push(cells);
}

return rows;
};

/**
* @description Convert the cells to a table.
* @param {Editor} editor - The editor instance.
* @param {TableNodeLocation} table - The table node location.
* @param {TableRows} rows - The table rows.
* @param {Transaction} tr - The transaction.
*/
const tableFromCells = (editor: Editor, table: TableNodeLocation, rows: TableRows, tr: Transaction): void => {
const schema = editor.schema.nodes;
const newRowNodes = rows.map((row) =>
schema.tableRow.create(null, row.filter((cell) => cell !== null) as readonly Node[])
);
const newTableNode = table.node.copy(Fragment.from(newRowNodes));
tr.replaceWith(table.pos, table.pos + table.node.nodeSize, newTableNode);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { Disclosure } from "@headlessui/react";
import type { Editor } from "@tiptap/core";
import { Ban, ChevronRight, Palette } from "lucide-react";
// plane imports
import { cn } from "@plane/utils";
// constants
import { COLORS_LIST } from "@/constants/common";
import { CORE_EXTENSIONS } from "@/constants/extension";

// TODO: implement text color selector

type Props = {
editor: Editor;
onSelect: (color: string | null) => void;
};

const handleBackgroundColorChange = (editor: Editor, color: string | null) => {
editor
.chain()
.focus()
.updateAttributes(CORE_EXTENSIONS.TABLE_CELL, {
background: color,
})
.run();
};

// const handleTextColorChange = (editor: Editor, color: string | null) => {
// editor
// .chain()
// .focus()
// .updateAttributes(CORE_EXTENSIONS.TABLE_CELL, {
// textColor: color,
// })
// .run();
// };

export const TableDragHandleDropdownColorSelector: React.FC<Props> = (props) => {
const { editor, onSelect } = props;

return (
<Disclosure defaultOpen>
<Disclosure.Button
as="button"
type="button"
className="flex items-center justify-between gap-2 w-full rounded px-1 py-1.5 text-xs text-left truncate text-custom-text-200 hover:bg-custom-background-80"
>
{({ open }) => (
<>
<span className="flex items-center gap-2">
<Palette className="shrink-0 size-3" />
Color
</span>
<ChevronRight
className={cn("shrink-0 size-3 transition-transform duration-200", {
"rotate-90": open,
})}
/>
</>
)}
</Disclosure.Button>
<Disclosure.Panel className="p-1 space-y-2 mb-1.5">
{/* <div className="space-y-1.5">
<p className="text-xs text-custom-text-300 font-semibold">Text colors</p>
<div className="flex items-center flex-wrap gap-2">
{COLORS_LIST.map((color) => (
<button
key={color.key}
type="button"
className="flex-shrink-0 size-6 rounded border-[0.5px] border-custom-border-400 hover:opacity-60 transition-opacity"
style={{
backgroundColor: color.textColor,
}}
onClick={() => handleTextColorChange(editor, color.textColor)}
/>
))}
<button
type="button"
className="flex-shrink-0 size-6 grid place-items-center rounded text-custom-text-300 border-[0.5px] border-custom-border-400 hover:bg-custom-background-80 transition-colors"
onClick={() => handleTextColorChange(editor, null)}
>
<Ban className="size-4" />
</button>
</div>
</div> */}
<div className="space-y-1">
<p className="text-xs text-custom-text-300 font-semibold">Background colors</p>
<div className="flex items-center flex-wrap gap-2">
{COLORS_LIST.map((color) => (
<button
key={color.key}
type="button"
className="flex-shrink-0 size-6 rounded border-[0.5px] border-custom-border-400 hover:opacity-60 transition-opacity"
style={{
backgroundColor: color.backgroundColor,
}}
onClick={() => {
handleBackgroundColorChange(editor, color.backgroundColor);
onSelect(color.backgroundColor);
}}
/>
))}
<button
type="button"
className="flex-shrink-0 size-6 grid place-items-center rounded text-custom-text-300 border-[0.5px] border-custom-border-400 hover:bg-custom-background-80 transition-colors"
onClick={() => {
handleBackgroundColorChange(editor, null);
onSelect(null);
}}
>
<Ban className="size-4" />
</button>
</div>
</div>
</Disclosure.Panel>
</Disclosure>
);
};
Loading
Loading