mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
fc01deeefd
* chore(deps-dev): bump oxlint-tsgolint from 0.14.2 to 0.22.1 Bumps [oxlint-tsgolint](https://github.com/oxc-project/tsgolint) from 0.14.2 to 0.22.1. - [Release notes](https://github.com/oxc-project/tsgolint/releases) - [Commits](https://github.com/oxc-project/tsgolint/compare/v0.14.2...v0.22.1) --- updated-dependencies: - dependency-name: oxlint-tsgolint dependency-version: 0.22.1 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * chore: Switch tsconfig to bundler resolution for tsgolint 0.22.1 oxlint-tsgolint 0.22.1 removed support for moduleResolution=node10 (the alias for "node"). Switch to "bundler" with resolvePackageJsonExports disabled so packages whose exports field omits a types condition still resolve. Update markdown-it type imports to sub-paths since the package's .d.mts entry only re-exports a subset of named types. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: Resolve type-aware lint errors caught by tsgolint 0.22.1 oxlint-tsgolint 0.22.1 catches several await-thenable, no-floating-promises, and no-meaningless-void-operator cases the prior 0.14.2 missed: - Drop redundant inner `await` from Promise.all([await x, await y]) call sites so the array entries are real Promises rather than already-resolved values. - Replace Promise.all wrappers around synchronous presenters (presentEvent, presentTemplate, presentPublicTeam) with plain map / direct calls. - Wrap non-promise branches of ternaries inside Promise.all with Promise.resolve so the array remains thenable across both arms. - Add `void` to the unawaited provider.connect() in the auth-failed retry chain, and remove `void` from the disconnect() call which returns void. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Tom Moor <tom@getoutline.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
200 lines
6.0 KiB
TypeScript
200 lines
6.0 KiB
TypeScript
import type Token from "markdown-it/lib/token.mjs";
|
||
import {
|
||
type Node as ProsemirrorNode,
|
||
type NodeSpec,
|
||
Slice,
|
||
} from "prosemirror-model";
|
||
import type { EditorState } from "prosemirror-state";
|
||
import { Plugin, PluginKey } from "prosemirror-state";
|
||
import { Decoration, DecorationSet } from "prosemirror-view";
|
||
import { TableMap } from "prosemirror-tables";
|
||
import {
|
||
getCellAttrs,
|
||
isValidCellAlignment,
|
||
isValidCellMarks,
|
||
setCellAttrs,
|
||
} from "../lib/table";
|
||
import Node from "./Node";
|
||
import { presetColors, rgbaToHex } from "@shared/utils/color";
|
||
import { parseToRgb, transparentize } from "polished";
|
||
import type { RgbaColor } from "polished/lib/types/color";
|
||
|
||
export default class TableCell extends Node {
|
||
/** The default opacity of the table cell background */
|
||
static opacity = 0.7;
|
||
|
||
/** Preset colors with opacity applied, used for table cell backgrounds */
|
||
static presetColors = presetColors.map((preset) => ({
|
||
hex: rgbaToHex(
|
||
parseToRgb(transparentize(1 - TableCell.opacity, preset.hex)) as RgbaColor
|
||
),
|
||
name: preset.name,
|
||
}));
|
||
|
||
/**
|
||
* Checks if a color is one of the table cell preset colors.
|
||
*
|
||
* @param color - A hex color string to check.
|
||
* @returns true if the color matches a preset color's hex value.
|
||
*/
|
||
static isPresetColor(color: string): boolean {
|
||
return TableCell.presetColors.some((c) => c.hex === color);
|
||
}
|
||
|
||
get name() {
|
||
return "td";
|
||
}
|
||
|
||
get schema(): NodeSpec {
|
||
return {
|
||
content: "block+",
|
||
tableRole: "cell",
|
||
group: "cell",
|
||
isolating: true,
|
||
parseDOM: [{ tag: "td", getAttrs: getCellAttrs }],
|
||
toDOM(node) {
|
||
return ["td", setCellAttrs(node), 0];
|
||
},
|
||
attrs: {
|
||
colspan: { default: 1 },
|
||
rowspan: { default: 1 },
|
||
alignment: { default: null, validate: isValidCellAlignment },
|
||
colwidth: { default: null },
|
||
marks: {
|
||
default: undefined,
|
||
validate: (value: unknown) =>
|
||
isValidCellMarks(value, this.editor?.schema),
|
||
},
|
||
},
|
||
};
|
||
}
|
||
|
||
toMarkdown() {
|
||
// see: renderTable
|
||
}
|
||
|
||
parseMarkdown() {
|
||
return {
|
||
block: "td",
|
||
getAttrs: (tok: Token) => ({
|
||
alignment: isValidCellAlignment(tok.info) ? tok.info : null,
|
||
}),
|
||
};
|
||
}
|
||
|
||
get plugins() {
|
||
const createCellDecorations = (state: EditorState) => {
|
||
const { doc } = state;
|
||
const decorations: Decoration[] = [];
|
||
|
||
// Iterate through all tables in the document
|
||
doc.descendants((node: ProsemirrorNode, pos: number) => {
|
||
if (node.type.spec.tableRole === "table") {
|
||
const map = TableMap.get(node);
|
||
|
||
// Mark cells in the first column and last row of this table
|
||
node.descendants((cellNode: ProsemirrorNode, cellPos: number) => {
|
||
if (
|
||
cellNode.type.spec.tableRole === "cell" ||
|
||
cellNode.type.spec.tableRole === "header_cell"
|
||
) {
|
||
const cellOffset = cellPos;
|
||
const cellIndex = map.map.indexOf(cellOffset);
|
||
|
||
if (cellIndex !== -1) {
|
||
const col = cellIndex % map.width;
|
||
const row = Math.floor(cellIndex / map.width);
|
||
const rowspan = cellNode.attrs.rowspan || 1;
|
||
const colspan = cellNode.attrs.colspan || 1;
|
||
const attrs: Record<string, string> = {};
|
||
|
||
if (col === 0) {
|
||
attrs["data-first-column"] = "true";
|
||
}
|
||
|
||
// Mark cells that extend into the last column (accounting for colspan)
|
||
if (col + colspan >= map.width) {
|
||
attrs["data-last-column"] = "true";
|
||
}
|
||
|
||
// Mark cells that extend into the last row (accounting for rowspan)
|
||
if (row + rowspan >= map.height) {
|
||
attrs["data-last-row"] = "true";
|
||
}
|
||
|
||
if (Object.keys(attrs).length > 0) {
|
||
decorations.push(
|
||
Decoration.node(
|
||
pos + cellPos + 1,
|
||
pos + cellPos + 1 + cellNode.nodeSize,
|
||
attrs
|
||
)
|
||
);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
return DecorationSet.create(doc, decorations);
|
||
};
|
||
|
||
return [
|
||
new Plugin({
|
||
key: new PluginKey("table-cell-attributes"),
|
||
state: {
|
||
init: (_, state) => createCellDecorations(state),
|
||
apply: (tr, pluginState, oldState, newState) => {
|
||
// Only recompute if document changed
|
||
if (!tr.docChanged) {
|
||
return pluginState;
|
||
}
|
||
|
||
return createCellDecorations(newState);
|
||
},
|
||
},
|
||
props: {
|
||
decorations(state) {
|
||
return this.getState(state);
|
||
},
|
||
},
|
||
}),
|
||
new Plugin({
|
||
key: new PluginKey("table-cell-copy-transform"),
|
||
props: {
|
||
transformCopied: (slice) => {
|
||
// check if the copied selection is a single table, with a single row, with a single cell. If so,
|
||
// copy the cell content only – not a table with a single cell. This leads to more predictable pasting
|
||
// behavior, both in and outside the app.
|
||
if (slice.content.childCount === 1) {
|
||
const table = slice.content.firstChild;
|
||
if (
|
||
table?.type.spec.tableRole === "table" &&
|
||
table.childCount === 1
|
||
) {
|
||
const row = table.firstChild;
|
||
if (
|
||
row?.type.spec.tableRole === "row" &&
|
||
row.childCount === 1
|
||
) {
|
||
const cell = row.firstChild;
|
||
if (cell?.type.spec.tableRole === "cell") {
|
||
return new Slice(
|
||
cell.content,
|
||
slice.openStart,
|
||
slice.openEnd
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return slice;
|
||
},
|
||
},
|
||
}),
|
||
];
|
||
}
|
||
}
|