Table improvements (#6958)

* Header toggling, resizable columns

* Allow all blocks in table cells, disable column resizing in read-only

* Fixed dynamic scroll shadows

* Refactor, scroll styling

* fix scrolling, tweaks

* fix: Table layout lost on sort

* fix: Caching of grip decorators

* refactor

* stash

* fix first render shadows

* stash

* First add column grip, styles

* Just add column/row click handlers left

* fix: isTableSelected for single cell table

* Refactor mousedown handlers

* fix: 'Add row before' command missing on first row

* fix overflow on rhs

* fix: Error clicking column grip when menu is open

* Hide table controls when printing

* Restore table header background

* fix: Header behavior when adding columns and rows at the edges

* Tweak header styling

* fix: Serialize and parsing of column attributes when copy/pasting
fix: Column width is lost when changing column alignment
This commit is contained in:
Tom Moor
2024-05-31 17:52:39 -04:00
committed by GitHub
parent 1db46f4aac
commit da19054555
27 changed files with 1020 additions and 351 deletions

View File

@ -230,7 +230,7 @@ export default function SelectionToolbar(props: Props) {
if (isCodeSelection && selection.empty) {
items = getCodeMenuItems(state, readOnly, dictionary);
} else if (isTableSelection) {
items = getTableMenuItems(dictionary);
items = getTableMenuItems(state, dictionary);
} else if (colIndex !== undefined) {
items = getTableColMenuItems(state, colIndex, rtl, dictionary);
} else if (rowIndex !== undefined) {

View File

@ -1,10 +1,32 @@
import { TrashIcon } from "outline-icons";
import { AlignFullWidthIcon, TrashIcon } from "outline-icons";
import { EditorState } from "prosemirror-state";
import * as React from "react";
import { MenuItem } from "@shared/editor/types";
import isNodeActive from "@shared/editor/queries/isNodeActive";
import { MenuItem, TableLayout } from "@shared/editor/types";
import { Dictionary } from "~/hooks/useDictionary";
export default function tableMenuItems(dictionary: Dictionary): MenuItem[] {
export default function tableMenuItems(
state: EditorState,
dictionary: Dictionary
): MenuItem[] {
const { schema } = state;
const isFullWidth = isNodeActive(schema.nodes.table, {
layout: TableLayout.fullWidth,
})(state);
return [
{
name: "setTableAttr",
tooltip: isFullWidth
? dictionary.alignDefaultWidth
: dictionary.alignFullWidth,
icon: <AlignFullWidthIcon />,
attrs: isFullWidth ? { layout: null } : { layout: TableLayout.fullWidth },
active: () => isFullWidth,
},
{
name: "separator",
},
{
name: "deleteTable",
tooltip: dictionary.deleteTable,

View File

@ -7,6 +7,7 @@ import {
InsertRightIcon,
ArrowIcon,
MoreIcon,
TableHeaderColumnIcon,
} from "outline-icons";
import { EditorState } from "prosemirror-state";
import * as React from "react";
@ -78,15 +79,23 @@ export default function tableColMenuItems(
{
icon: <MoreIcon />,
children: [
{
name: "toggleHeaderColumn",
label: dictionary.toggleHeader,
icon: <TableHeaderColumnIcon />,
visible: index === 0,
},
{
name: rtl ? "addColumnAfter" : "addColumnBefore",
label: rtl ? dictionary.addColumnAfter : dictionary.addColumnBefore,
icon: <InsertLeftIcon />,
attrs: { index },
},
{
name: rtl ? "addColumnBefore" : "addColumnAfter",
label: rtl ? dictionary.addColumnBefore : dictionary.addColumnAfter,
icon: <InsertRightIcon />,
attrs: { index },
},
{
name: "deleteColumn",

View File

@ -3,6 +3,7 @@ import {
InsertAboveIcon,
InsertBelowIcon,
MoreIcon,
TableHeaderRowIcon,
} from "outline-icons";
import { EditorState } from "prosemirror-state";
import * as React from "react";
@ -19,11 +20,16 @@ export default function tableRowMenuItems(
icon: <MoreIcon />,
children: [
{
name: "addRowAfter",
name: "toggleHeaderRow",
label: dictionary.toggleHeader,
icon: <TableHeaderRowIcon />,
visible: index === 0,
},
{
name: "addRowBefore",
label: dictionary.addRowBefore,
icon: <InsertAboveIcon />,
attrs: { index: index - 1 },
visible: index !== 0,
attrs: { index },
},
{
name: "addRowAfter",

View File

@ -6,13 +6,14 @@ export default function useDictionary() {
return React.useMemo(
() => ({
addColumnAfter: t("Insert after"),
addColumnBefore: t("Insert before"),
addRowAfter: t("Insert after"),
addRowBefore: t("Insert before"),
addColumnAfter: t("Add column after"),
addColumnBefore: t("Add column before"),
addRowAfter: t("Add row after"),
addRowBefore: t("Add row before"),
alignCenter: t("Align center"),
alignLeft: t("Align left"),
alignRight: t("Align right"),
alignDefaultWidth: t("Default width"),
alignFullWidth: t("Full width"),
bulletList: t("Bulleted list"),
checkboxList: t("Todo list"),
@ -75,6 +76,7 @@ export default function useDictionary() {
sortAsc: t("Sort ascending"),
sortDesc: t("Sort descending"),
table: t("Table"),
toggleHeader: t("Toggle header"),
mathInline: t("Math inline (LaTeX)"),
mathBlock: t("Math block (LaTeX)"),
tip: t("Tip"),

View File

@ -9,7 +9,6 @@ declare module "styled-components" {
text: string;
cursor: string;
divider: string;
tableDivider: string;
tableSelected: string;
tableSelectedBackground: string;
quote: string;

View File

@ -7,6 +7,7 @@ import { JSDOM } from "jsdom";
import { Node } from "prosemirror-model";
import * as Y from "yjs";
import textBetween from "@shared/editor/lib/textBetween";
import { EditorStyleHelper } from "@shared/editor/styles/EditorStyleHelper";
import { ProsemirrorData } from "@shared/types";
import { parser, serializer, schema } from "@server/editor";
import { addTags } from "@server/logging/tracer";
@ -322,7 +323,7 @@ export class DocumentHelper {
// Special case for largetables, as this block can get very large we
// want to clip it to only the changed rows and surrounding context.
if (childNode.classList.contains("table-wrapper")) {
if (childNode.classList.contains(EditorStyleHelper.table)) {
const rows = childNode.querySelectorAll("tr");
if (rows.length < 3) {
continue;

View File

@ -1,18 +1,18 @@
import { Fragment, Node, NodeType } from "prosemirror-model";
import {
Command,
EditorState,
TextSelection,
Transaction,
} from "prosemirror-state";
import { Command, EditorState, TextSelection } from "prosemirror-state";
import {
CellSelection,
addRow,
isInTable,
selectedRect,
tableNodeTypes,
toggleHeader,
addColumn,
} from "prosemirror-tables";
import { getCellsInColumn } from "../queries/table";
import { chainTransactions } from "../lib/chainTransactions";
import { getCellsInColumn, isHeaderEnabled } from "../queries/table";
import { TableLayout } from "../types";
import collapseSelection from "./collapseSelection";
export function createTable({
rowsCount,
@ -34,7 +34,7 @@ export function createTable({
};
}
export function createTableInner(
function createTableInner(
state: EditorState,
rowsCount: number,
colsCount: number,
@ -93,6 +93,7 @@ export function sortTable({
if (!isInTable(state)) {
return false;
}
if (dispatch) {
const rect = selectedRect(state);
const table: Node[][] = [];
@ -159,7 +160,10 @@ export function sortTable({
}
// replace the original table with this sorted one
const nodes = state.schema.nodes.table.createChecked(null, rows);
const nodes = state.schema.nodes.table.createChecked(
rect.table.attrs,
rows
);
const { tr } = state;
tr.replaceRangeWith(
@ -168,21 +172,76 @@ export function sortTable({
nodes
);
dispatch(
tr
// .setSelection(
// CellSelection.create(
// tr.doc,
// rect.map.positionAt(0, index, rect.table)
// )
// )
.scrollIntoView()
);
dispatch(tr.scrollIntoView());
}
return true;
};
}
/**
* A command that safely adds a row taking into account any existing heading column at the top of
* the table, and preventing it moving "into" the table.
*
* @param index The index to add the row at, if undefined the current selection is used
* @returns The command
*/
export function addRowBefore({ index }: { index?: number }): Command {
return (state, dispatch) => {
if (!isInTable(state)) {
return false;
}
const rect = selectedRect(state);
const isHeaderRowEnabled = isHeaderEnabled(state, "row", rect);
const position = index !== undefined ? index : rect.left;
// Special case when adding row to the beginning of the table to ensure the header does not
// move inwards.
const headerSpecialCase = position === 0 && isHeaderRowEnabled;
chainTransactions(
headerSpecialCase ? toggleHeader("row") : undefined,
(s, d) => !!d?.(addRow(s.tr, rect, position)),
headerSpecialCase ? toggleHeader("row") : undefined,
collapseSelection()
)(state, dispatch);
return true;
};
}
/**
* A command that safely adds a column taking into account any existing heading column on the far
* left of the table, and preventing it moving "into" the table.
*
* @param index The index to add the column at, if undefined the current selection is used
* @returns The command
*/
export function addColumnBefore({ index }: { index?: number }): Command {
return (state, dispatch) => {
if (!isInTable(state)) {
return false;
}
const rect = selectedRect(state);
const isHeaderColumnEnabled = isHeaderEnabled(state, "column", rect);
const position = index !== undefined ? index : rect.left;
// Special case when adding column to the beginning of the table to ensure the header does not
// move inwards.
const headerSpecialCase = position === 0 && isHeaderColumnEnabled;
chainTransactions(
headerSpecialCase ? toggleHeader("column") : undefined,
(s, d) => !!d?.(addColumn(s.tr, rect, position)),
headerSpecialCase ? toggleHeader("column") : undefined,
collapseSelection()
)(state, dispatch);
return true;
};
}
export function addRowAndMoveSelection({
index,
}: {
@ -222,6 +281,12 @@ export function addRowAndMoveSelection({
};
}
/**
* Set column attributes. Passed attributes will be merged with existing.
*
* @param attrs The attributes to set
* @returns The command
*/
export function setColumnAttr({
index,
alignment,
@ -234,7 +299,9 @@ export function setColumnAttr({
const cells = getCellsInColumn(index)(state) || [];
let transaction = state.tr;
cells.forEach((pos) => {
const node = state.doc.nodeAt(pos);
transaction = transaction.setNodeMarkup(pos, undefined, {
...node?.attrs,
alignment,
});
});
@ -244,37 +311,78 @@ export function setColumnAttr({
};
}
export function selectRow(index: number, expand = false) {
return (state: EditorState): Transaction => {
const rect = selectedRect(state);
const pos = rect.map.positionAt(index, 0, rect.table);
const $pos = state.doc.resolve(rect.tableStart + pos);
const rowSelection =
expand && state.selection instanceof CellSelection
? CellSelection.rowSelection(state.selection.$anchorCell, $pos)
: CellSelection.rowSelection($pos);
return state.tr.setSelection(rowSelection);
/**
* Set table attributes. Passed attributes will be merged with existing.
*
* @param attrs The attributes to set
* @returns The command
*/
export function setTableAttr(attrs: { layout: TableLayout | null }): Command {
return (state, dispatch) => {
if (!isInTable(state)) {
return false;
}
if (dispatch) {
const { tr } = state;
const rect = selectedRect(state);
tr.setNodeMarkup(rect.tableStart - 1, undefined, {
...rect.table.attrs,
...attrs,
}).setSelection(TextSelection.near(tr.doc.resolve(rect.tableStart)));
dispatch(tr);
return true;
}
return false;
};
}
export function selectColumn(index: number, expand = false) {
return (state: EditorState): Transaction => {
const rect = selectedRect(state);
const pos = rect.map.positionAt(0, index, rect.table);
const $pos = state.doc.resolve(rect.tableStart + pos);
const colSelection =
expand && state.selection instanceof CellSelection
? CellSelection.colSelection(state.selection.$anchorCell, $pos)
: CellSelection.colSelection($pos);
return state.tr.setSelection(colSelection);
export function selectRow(index: number, expand = false): Command {
return (state: EditorState, dispatch): boolean => {
if (dispatch) {
const rect = selectedRect(state);
const pos = rect.map.positionAt(index, 0, rect.table);
const $pos = state.doc.resolve(rect.tableStart + pos);
const rowSelection =
expand && state.selection instanceof CellSelection
? CellSelection.rowSelection(state.selection.$anchorCell, $pos)
: CellSelection.rowSelection($pos);
dispatch(state.tr.setSelection(rowSelection));
return true;
}
return false;
};
}
export function selectTable(state: EditorState): Transaction {
const rect = selectedRect(state);
const map = rect.map.map;
const $anchor = state.doc.resolve(rect.tableStart + map[0]);
const $head = state.doc.resolve(rect.tableStart + map[map.length - 1]);
const tableSelection = new CellSelection($anchor, $head);
return state.tr.setSelection(tableSelection);
export function selectColumn(index: number, expand = false): Command {
return (state, dispatch): boolean => {
if (dispatch) {
const rect = selectedRect(state);
const pos = rect.map.positionAt(0, index, rect.table);
const $pos = state.doc.resolve(rect.tableStart + pos);
const colSelection =
expand && state.selection instanceof CellSelection
? CellSelection.colSelection(state.selection.$anchorCell, $pos)
: CellSelection.colSelection($pos);
dispatch(state.tr.setSelection(colSelection));
return true;
}
return false;
};
}
export function selectTable(): Command {
return (state, dispatch): boolean => {
if (dispatch) {
const rect = selectedRect(state);
const map = rect.map.map;
const $anchor = state.doc.resolve(rect.tableStart + map[0]);
const $head = state.doc.resolve(rect.tableStart + map[map.length - 1]);
const tableSelection = new CellSelection($anchor, $head);
dispatch(state.tr.setSelection(tableSelection));
return true;
}
return false;
};
}

View File

@ -1,7 +1,7 @@
import { NodeType } from "prosemirror-model";
import { wrapInList, liftListItem } from "prosemirror-schema-list";
import { Command } from "prosemirror-state";
import chainTransactions from "../lib/chainTransactions";
import { chainTransactions } from "../lib/chainTransactions";
import { findParentNode } from "../queries/findParentNode";
import isList from "../queries/isList";
import clearNodes from "./clearNodes";
@ -35,10 +35,7 @@ export default function toggleList(
) {
tr.setNodeMarkup(parentList.pos, listType);
if (dispatch) {
dispatch(tr);
}
dispatch?.(tr);
return false;
}
}

View File

@ -1,6 +1,7 @@
/* eslint-disable no-irregular-whitespace */
import { lighten, transparentize } from "polished";
import styled, { DefaultTheme, css, keyframes } from "styled-components";
import { EditorStyleHelper } from "../styles/EditorStyleHelper";
import { videoStyle } from "./Video";
export type Props = {
@ -13,6 +14,11 @@ export type Props = {
theme: DefaultTheme;
};
export const fadeIn = keyframes`
from { opacity: 0; }
to { opacity: 1; }
`;
export const pulse = keyframes`
0% { box-shadow: 0 0 0 1px rgba(255, 213, 0, 0.75) }
50% { box-shadow: 0 0 0 4px rgba(255, 213, 0, 0.75) }
@ -267,7 +273,7 @@ const emailStyle = (props: Props) => css`
}
`;
const style = (props: Props) => `
const style = (props: Props) => css`
flex-grow: ${props.grow ? 1 : 0};
justify-content: start;
color: ${props.theme.text};
@ -563,6 +569,40 @@ iframe.embed {
}
}
.${EditorStyleHelper.tableFullWidth} {
transform: translateX(calc(50% + ${
EditorStyleHelper.padding
}px + var(--container-width) * -0.5));
.${EditorStyleHelper.tableScrollable},
table {
width: calc(var(--container-width) - ${EditorStyleHelper.padding * 2}px);
}
&.${EditorStyleHelper.tableShadowRight}::after {
left: calc(var(--container-width) - ${EditorStyleHelper.padding * 3}px);
}
}
.column-resize-handle {
animation: ${fadeIn} 150ms ease-in-out;
${props.readOnly ? "display: none;" : ""}
position: absolute;
right: -1px;
top: 0;
bottom: -1px;
width: 2px;
z-index: 20;
background-color: ${props.theme.text};
pointer-events: none;
}
.resize-cursor {
${props.readOnly ? "pointer-events: none;" : ""}
cursor: ew-resize;
cursor: col-resize;
}
.ProseMirror-hideselection *::selection {
background: transparent;
}
@ -1284,26 +1324,25 @@ table {
tr {
position: relative;
border-bottom: 1px solid ${props.theme.tableDivider};
}
tr:first-of-type {
background: ${props.theme.secondaryBackground};
}
th {
background: transparent;
border-bottom: 1px solid ${props.theme.divider};
}
td,
th {
position: relative;
vertical-align: top;
border: 1px solid ${props.theme.tableDivider};
border: 1px solid ${props.theme.divider};
position: relative;
padding: 4px 8px;
text-align: ${props.rtl ? "right" : "left"};
min-width: 100px;
font-weight: normal;
}
th {
background: ${transparentize(0.75, props.theme.divider)};
color: ${props.theme.textSecondary};
font-weight: 500;
}
td .component-embed {
@ -1320,7 +1359,135 @@ table {
background-clip: padding-box;
}
.grip-column {
.${EditorStyleHelper.tableAddRow},
.${EditorStyleHelper.tableAddColumn},
.${EditorStyleHelper.tableGrip},
.${EditorStyleHelper.tableGripColumn},
.${EditorStyleHelper.tableGripRow} {
@media print {
display: none;
}
}
.${EditorStyleHelper.tableAddRow},
.${EditorStyleHelper.tableAddColumn} {
display: block;
position: absolute;
background: ${props.theme.accent};
cursor: var(--pointer);
&:hover::after {
width: 16px;
height: 16px;
z-index: 20;
background-color: ${props.theme.accent};
background-size: 16px 16px;
background-position: 50% 50%;
background-image: url("data:image/svg+xml;base64,${btoa(
'<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M12 5C11.4477 5 11 5.44772 11 6V11H6C5.44772 11 5 11.4477 5 12C5 12.5523 5.44772 13 6 13H11V18C11 18.5523 11.4477 19 12 19C12.5523 19 13 18.5523 13 18V13H18C18.5523 13 19 12.5523 19 12C19 11.4477 18.5523 11 18 11H13V6C13 5.44772 12.5523 5 12 5Z" fill="white"/></svg>'
)}")
}
// extra clickable area
&::before {
content: "";
display: block;
cursor: var(--pointer);
position: absolute;
width: 24px;
height: 24px;
}
}
.${EditorStyleHelper.tableAddRow} {
bottom: -1px;
left: -16px;
width: 0;
height: 2px;
&::after {
content: "";
position: absolute;
bottom: -1px;
left: -10px;
width: 4px;
height: 4px;
display: ${props.readOnly ? "none" : "block"};
border-radius: 100%;
background-color: ${props.theme.divider};
}
&:hover {
width: calc(var(--table-width) - ${EditorStyleHelper.padding * 1.5}px);
}
&:hover::after {
bottom: -7.5px;
left: -16px;
}
// extra clickable area
&::before {
bottom: -12px;
left: -18px;
}
&.first {
bottom: auto;
top: -1px;
&::before {
bottom: auto;
top: -12px;
}
}
}
.${EditorStyleHelper.tableAddColumn} {
top: -16px;
right: -1px;
width: 2px;
height: 0;
&::after {
content: "";
position: absolute;
top: -10px;
right: -1px;
width: 4px;
height: 4px;
display: ${props.readOnly ? "none" : "block"};
border-radius: 100%;
background-color: ${props.theme.divider};
}
&:hover {
height: calc(var(--table-height) - ${EditorStyleHelper.padding}px + 6px);
}
&:hover::after {
top: -16px;
right: -7px;
}
// extra clickable area
&::before {
top: -16px;
right: -12px;
}
&.first {
right: auto;
left: -1px;
&::before {
right: auto;
left: -12px;
}
}
}
.${EditorStyleHelper.tableGripColumn} {
/* usage of ::after for all of the table grips works around a bug in
* prosemirror-tables that causes Safari to hang when selecting a cell
* in an empty table:
@ -1333,8 +1500,7 @@ table {
${props.rtl ? "right" : "left"}: 0;
width: 100%;
height: 12px;
background: ${props.theme.tableDivider};
border-bottom: 3px solid ${props.theme.background};
background: ${props.theme.divider};
display: ${props.readOnly ? "none" : "block"};
}
@ -1343,16 +1509,18 @@ table {
}
&.first::after {
border-top-${props.rtl ? "right" : "left"}-radius: 3px;
border-bottom-${props.rtl ? "right" : "left"}-radius: 3px;
}
&.last::after {
border-top-${props.rtl ? "left" : "right"}-radius: 3px;
border-bottom-${props.rtl ? "left" : "right"}-radius: 3px;
}
&.selected::after {
background: ${props.theme.tableSelected};
}
}
.grip-row {
.${EditorStyleHelper.tableGripRow} {
&::after {
content: "";
cursor: var(--pointer);
@ -1361,8 +1529,7 @@ table {
top: 0;
height: 100%;
width: 12px;
background: ${props.theme.tableDivider};
border-${props.rtl ? "left" : "right"}: 3px solid;
background: ${props.theme.divider};
border-color: ${props.theme.background};
display: ${props.readOnly ? "none" : "block"};
}
@ -1371,21 +1538,23 @@ table {
background: ${props.theme.text};
}
&.first::after {
border-top-${props.rtl ? "right" : "left"}-radius: 3px;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
&.last::after {
border-bottom-${props.rtl ? "right" : "left"}-radius: 3px;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
}
&.selected::after {
background: ${props.theme.tableSelected};
}
}
.grip-table {
.${EditorStyleHelper.tableGrip} {
&::after {
content: "";
cursor: var(--pointer);
background: ${props.theme.tableDivider};
background: ${props.theme.divider};
width: 13px;
height: 13px;
border-radius: 13px;
@ -1394,6 +1563,7 @@ table {
top: -18px;
${props.rtl ? "right" : "left"}: -18px;
display: ${props.readOnly ? "none" : "block"};
z-index: 10;
}
&:hover::after {
@ -1405,11 +1575,22 @@ table {
}
}
.scrollable-wrapper {
.${EditorStyleHelper.table} {
position: relative;
margin: 0.5em 0px;
}
.${EditorStyleHelper.tableScrollable} {
position: relative;
margin: -1em ${-EditorStyleHelper.padding}px -0.5em;
scrollbar-width: thin;
scrollbar-color: transparent transparent;
overflow-y: hidden;
overflow-x: auto;
padding-top: 1em;
padding-bottom: .5em;
padding-left: ${EditorStyleHelper.padding}px;
padding-right: ${EditorStyleHelper.padding}px;
transition: border 250ms ease-in-out 0s;
&:hover {
scrollbar-color: ${props.theme.scrollbarThumb} ${
@ -1438,39 +1619,36 @@ table {
}
}
.scrollable {
overflow-y: hidden;
overflow-x: auto;
padding-${props.rtl ? "right" : "left"}: 1em;
margin-${props.rtl ? "right" : "left"}: -1em;
transition: border 250ms ease-in-out 0s;
}
.scrollable-shadow {
.${EditorStyleHelper.tableShadowLeft}::before,
.${EditorStyleHelper.tableShadowRight}::after {
content: "";
position: absolute;
top: 16px;
top: 1px;
bottom: 0;
${props.rtl ? "right" : "left"}: -1em;
width: 32px;
z-index: 1;
z-index: 20;
transition: box-shadow 250ms ease-in-out;
border: 0px solid transparent;
pointer-events: none;
}
&.left {
box-shadow: 16px 0 16px -16px inset rgba(0, 0, 0, ${
props.theme.isDark ? 1 : 0.25
});
border-left: 1em solid ${props.theme.background};
}
.${EditorStyleHelper.tableShadowLeft}::before {
left: -${EditorStyleHelper.padding}px;
right: auto;
box-shadow: 16px 0 16px -16px inset rgba(0, 0, 0, ${
props.theme.isDark ? 1 : 0.25
});
border-left: ${EditorStyleHelper.padding}px solid ${props.theme.background};
}
&.right {
right: 0;
left: auto;
box-shadow: -16px 0 16px -16px inset rgba(0, 0, 0, ${
props.theme.isDark ? 1 : 0.25
});
}
.${EditorStyleHelper.tableShadowRight}::after {
right: -${EditorStyleHelper.padding}px;
left: auto;
box-shadow: -16px 0 16px -16px inset rgba(0, 0, 0, ${
props.theme.isDark ? 1 : 0.25
});
border-right: ${EditorStyleHelper.padding}px solid ${props.theme.background};
}
.block-menu-trigger {

View File

@ -1,16 +1,21 @@
import { Command, Transaction } from "prosemirror-state";
export default function chainTransactions(...commands: Command[]): Command {
/**
* Chain multiple commands into a single command and collects state as it goes.
*
* @param commands The commands to chain
* @returns The chained command
*/
export function chainTransactions(
...commands: (Command | undefined)[]
): Command {
return (state, dispatch): boolean => {
const dispatcher = (tr: Transaction): void => {
state = state.apply(tr);
dispatch?.(tr);
};
const last = commands.pop();
const reduced = commands.reduce(
(result, command) => result || command(state, dispatcher),
false
);
return reduced && last !== undefined && last(state, dispatch);
commands.map((command) => command?.(state, dispatcher));
return last !== undefined && last(state, dispatch);
};
}

View File

@ -0,0 +1,67 @@
import { Attrs, Node } from "prosemirror-model";
import { MutableAttrs } from "prosemirror-tables";
import { TableLayout } from "../types";
export interface TableAttrs {
layout: TableLayout | null;
}
export interface CellAttrs {
colspan: number;
rowspan: number;
colwidth: number[] | null;
alignment: "center" | "left" | "right" | null;
}
/**
* Helper to get cell attributes from a DOM node, used when pasting table content.
*
* @param dom DOM node to get attributes from
* @returns Cell attributes
*/
export function getCellAttrs(dom: HTMLElement | string): Attrs {
if (typeof dom === "string") {
return {};
}
const widthAttr = dom.getAttribute("data-colwidth");
const widths =
widthAttr && /^\d+(,\d+)*$/.test(widthAttr)
? widthAttr.split(",").map((s) => Number(s))
: null;
const colspan = Number(dom.getAttribute("colspan") || 1);
return {
colspan,
rowspan: Number(dom.getAttribute("rowspan") || 1),
colwidth: widths && widths.length === colspan ? widths : null,
alignment:
dom.style.textAlign === "center"
? "center"
: dom.style.textAlign === "right"
? "right"
: null,
} satisfies CellAttrs;
}
/**
* Helper to serialize cell attributes on a node, used when copying table content.
*
* @param node Node to get attributes from
* @returns Attributes for the cell
*/
export function setCellAttrs(node: Node): Attrs {
const attrs: MutableAttrs = {};
if (node.attrs.colspan !== 1) {
attrs.colspan = node.attrs.colspan;
}
if (node.attrs.rowspan !== 1) {
attrs.rowspan = node.attrs.rowspan;
}
if (node.attrs.colwidth) {
attrs["data-colwidth"] = node.attrs.colwidth.join(",");
}
if (node.attrs.alignment) {
attrs.style = `text-align: ${node.attrs.alignment}`;
}
return attrs;
}

View File

@ -3,7 +3,7 @@ import { MarkSpec, MarkType, Schema, Mark as PMMark } from "prosemirror-model";
import { Command, Plugin } from "prosemirror-state";
import { v4 as uuidv4 } from "uuid";
import collapseSelection from "../commands/collapseSelection";
import chainTransactions from "../lib/chainTransactions";
import { chainTransactions } from "../lib/chainTransactions";
import isMarkActive from "../queries/isMarkActive";
import Mark from "./Mark";

View File

@ -1,5 +1,4 @@
import Token from "markdown-it/lib/token";
import { OpenIcon } from "outline-icons";
import { toggleMark } from "prosemirror-commands";
import { InputRule } from "prosemirror-inputrules";
import { MarkdownSerializerState } from "prosemirror-markdown";
@ -11,8 +10,6 @@ import {
} from "prosemirror-model";
import { Command, EditorState, Plugin, TextSelection } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import * as React from "react";
import ReactDOM from "react-dom";
import { toast } from "sonner";
import { sanitizeUrl } from "../../utils/urls";
import getMarkRange from "../queries/getMarkRange";
@ -21,14 +18,6 @@ import { EventType } from "../types";
import Mark from "./Mark";
const LINK_INPUT_REGEX = /\[([^[]+)]\((\S+)\)$/;
let icon: HTMLSpanElement;
if (typeof window !== "undefined") {
const component = <OpenIcon size={16} />;
icon = document.createElement("span");
icon.className = "external-link";
ReactDOM.render(component, icon);
}
function isPlainURL(
link: ProsemirrorMark,
@ -198,6 +187,10 @@ export default class Link extends Mark {
return false;
}
if (target.role === "button") {
return false;
}
// clicking a link while editing should show the link toolbar,
// clicking in read-only will navigate
if (!view.editable || (view.editable && !view.hasFocus())) {

View File

@ -1,28 +1,35 @@
import { chainCommands } from "prosemirror-commands";
import { NodeSpec, Node as ProsemirrorNode } from "prosemirror-model";
import { Plugin } from "prosemirror-state";
import {
addColumnAfter,
addColumnBefore,
addRowAfter,
columnResizing,
deleteColumn,
deleteRow,
deleteTable,
goToNextCell,
tableEditing,
toggleHeaderCell,
toggleHeaderColumn,
toggleHeaderRow,
toggleHeader,
} from "prosemirror-tables";
import { Decoration, DecorationSet } from "prosemirror-view";
import {
addRowBefore,
addColumnBefore,
addRowAndMoveSelection,
setColumnAttr,
createTable,
sortTable,
setTableAttr,
} from "../commands/table";
import { MarkdownSerializerState } from "../lib/markdown/serializer";
import tablesRule from "../rules/tables";
import { EditorStyleHelper } from "../styles/EditorStyleHelper";
import { TableLayout } from "../types";
import Node from "./Node";
import { TableView } from "./TableView";
export type TableAttrs = {
layout: TableLayout | null;
};
export default class Table extends Node {
get name() {
@ -36,15 +43,17 @@ export default class Table extends Node {
isolating: true,
group: "block",
parseDOM: [{ tag: "table" }],
attrs: {
layout: {
default: null,
},
},
toDOM() {
// Note: This is overridden by TableView
return [
"div",
{ class: "scrollable-wrapper table-wrapper" },
[
"div",
{ class: "scrollable" },
["table", { class: "rme-table" }, ["tbody", 0]],
],
{ class: EditorStyleHelper.table },
["table", {}, ["tbody", 0]],
];
},
};
@ -58,16 +67,17 @@ export default class Table extends Node {
return {
createTable,
setColumnAttr,
setTableAttr,
sortTable,
addColumnBefore: () => addColumnBefore,
addColumnBefore,
addColumnAfter: () => addColumnAfter,
deleteColumn: () => deleteColumn,
addRowAfter: addRowAndMoveSelection,
addRowBefore,
addRowAfter: () => addRowAfter,
deleteRow: () => deleteRow,
deleteTable: () => deleteTable,
toggleHeaderColumn: () => toggleHeaderColumn,
toggleHeaderRow: () => toggleHeaderRow,
toggleHeaderCell: () => toggleHeaderCell,
toggleHeaderColumn: () => toggleHeader("column"),
toggleHeaderRow: () => toggleHeader("row"),
};
}
@ -90,52 +100,12 @@ export default class Table extends Node {
get plugins() {
return [
tableEditing(),
new Plugin({
props: {
decorations: (state) => {
const { doc } = state;
const decorations: Decoration[] = [];
let index = 0;
doc.descendants((node, pos) => {
if (node.type.name !== this.name) {
return;
}
const elements = document.getElementsByClassName("rme-table");
const table = elements[index];
if (!table) {
return;
}
const element = table.parentElement;
const shadowRight = !!(
element && element.scrollWidth > element.clientWidth
);
if (shadowRight) {
decorations.push(
Decoration.widget(
pos + 1,
() => {
const shadow = document.createElement("div");
shadow.className = "scrollable-shadow right";
return shadow;
},
{
key: "table-shadow-right",
}
)
);
}
index++;
});
return DecorationSet.create(doc, decorations);
},
},
// Note: Important to register columnResizing before tableEditing
columnResizing({
View: TableView,
lastColumnResizable: false,
}),
tableEditing(),
];
}
}

View File

@ -2,12 +2,15 @@ import Token from "markdown-it/lib/token";
import { NodeSpec } from "prosemirror-model";
import { Plugin } from "prosemirror-state";
import { DecorationSet, Decoration } from "prosemirror-view";
import { selectRow, selectTable } from "../commands/table";
import { addRowBefore, selectRow, selectTable } from "../commands/table";
import { getCellAttrs, setCellAttrs } from "../lib/table";
import {
getCellsInColumn,
isRowSelected,
isTableSelected,
} from "../queries/table";
import { EditorStyleHelper } from "../styles/EditorStyleHelper";
import { cn } from "../styles/utils";
import Node from "./Node";
export default class TableCell extends Node {
@ -17,23 +20,18 @@ export default class TableCell extends Node {
get schema(): NodeSpec {
return {
content: "(paragraph | embed)+",
content: "block+",
tableRole: "cell",
isolating: true,
parseDOM: [{ tag: "td" }],
parseDOM: [{ tag: "td", getAttrs: getCellAttrs }],
toDOM(node) {
return [
"td",
node.attrs.alignment
? { style: `text-align: ${node.attrs.alignment}` }
: {},
0,
];
return ["td", setCellAttrs(node), 0];
},
attrs: {
colspan: { default: 1 },
rowspan: { default: 1 },
alignment: { default: null },
colwidth: { default: null },
},
};
}
@ -50,61 +48,129 @@ export default class TableCell extends Node {
}
get plugins() {
function buildAddRowDecoration(pos: number, index: number) {
const className = cn(EditorStyleHelper.tableAddRow, {
first: index === 0,
});
return Decoration.widget(
pos + 1,
() => {
const plus = document.createElement("a");
plus.role = "button";
plus.className = className;
plus.dataset.index = index.toString();
return plus;
},
{
key: cn(className, index),
}
);
}
return [
new Plugin({
props: {
handleDOMEvents: {
mousedown: (view, event) => {
if (!(event.target instanceof HTMLElement)) {
return false;
}
const targetAddRow = event.target.closest(
`.${EditorStyleHelper.tableAddRow}`
);
if (targetAddRow) {
event.preventDefault();
event.stopImmediatePropagation();
const index = Number(targetAddRow.getAttribute("data-index"));
addRowBefore({ index })(view.state, view.dispatch);
return true;
}
const targetGrip = event.target.closest(
`.${EditorStyleHelper.tableGrip}`
);
if (targetGrip) {
event.preventDefault();
event.stopImmediatePropagation();
selectTable()(view.state, view.dispatch);
return true;
}
const targetGripRow = event.target.closest(
`.${EditorStyleHelper.tableGripRow}`
);
if (targetGripRow) {
event.preventDefault();
event.stopImmediatePropagation();
selectRow(
Number(targetGripRow.getAttribute("data-index")),
event.metaKey || event.shiftKey
)(view.state, view.dispatch);
return true;
}
return false;
},
},
decorations: (state) => {
const { doc } = state;
const decorations: Decoration[] = [];
const cells = getCellsInColumn(0)(state);
const rows = getCellsInColumn(0)(state);
if (cells) {
cells.forEach((pos, index) => {
if (rows) {
rows.forEach((pos, index) => {
if (index === 0) {
const className = cn(EditorStyleHelper.tableGrip, {
selected: isTableSelected(state),
});
decorations.push(
Decoration.widget(pos + 1, () => {
let className = "grip-table";
const selected = isTableSelected(state);
if (selected) {
className += " selected";
Decoration.widget(
pos + 1,
() => {
const grip = document.createElement("a");
grip.role = "button";
grip.className = className;
return grip;
},
{
key: className,
}
const grip = document.createElement("a");
grip.className = className;
grip.addEventListener("mousedown", (event) => {
event.preventDefault();
event.stopImmediatePropagation();
this.editor.view.dispatch(selectTable(state));
});
return grip;
})
)
);
}
decorations.push(
Decoration.widget(pos + 1, () => {
const rowSelected = isRowSelected(index)(state);
let className = "grip-row";
if (rowSelected) {
className += " selected";
const className = cn(EditorStyleHelper.tableGripRow, {
selected: isRowSelected(index)(state),
first: index === 0,
last: index === rows.length - 1,
});
decorations.push(
Decoration.widget(
pos + 1,
() => {
const grip = document.createElement("a");
grip.role = "button";
grip.className = className;
grip.dataset.index = index.toString();
return grip;
},
{
key: cn(className, index),
}
if (index === 0) {
className += " first";
}
if (index === cells.length - 1) {
className += " last";
}
const grip = document.createElement("a");
grip.className = className;
grip.addEventListener("mousedown", (event) => {
event.preventDefault();
event.stopImmediatePropagation();
this.editor.view.dispatch(
selectRow(index, event.metaKey || event.shiftKey)(state)
);
});
return grip;
})
)
);
if (index === 0) {
decorations.push(buildAddRowDecoration(pos, index));
}
decorations.push(buildAddRowDecoration(pos, index + 1));
});
}

View File

@ -1,96 +0,0 @@
import Token from "markdown-it/lib/token";
import { NodeSpec } from "prosemirror-model";
import { Plugin } from "prosemirror-state";
import { DecorationSet, Decoration } from "prosemirror-view";
import { selectColumn } from "../commands/table";
import { getCellsInRow, isColumnSelected } from "../queries/table";
import Node from "./Node";
export default class TableHeadCell extends Node {
get name() {
return "th";
}
get schema(): NodeSpec {
return {
content: "(paragraph | embed)+",
tableRole: "header_cell",
isolating: true,
parseDOM: [{ tag: "th" }],
toDOM(node) {
return [
"th",
node.attrs.alignment
? { style: `text-align: ${node.attrs.alignment}` }
: {},
0,
];
},
attrs: {
colspan: { default: 1 },
rowspan: { default: 1 },
alignment: { default: null },
},
};
}
toMarkdown() {
// see: renderTable
}
parseMarkdown() {
return {
block: "th",
getAttrs: (tok: Token) => ({ alignment: tok.info }),
};
}
get plugins() {
return [
new Plugin({
props: {
decorations: (state) => {
const { doc } = state;
const decorations: Decoration[] = [];
const cells = getCellsInRow(0)(state);
if (cells) {
cells.forEach((pos, index) => {
decorations.push(
Decoration.widget(pos + 1, () => {
const colSelected = isColumnSelected(index)(state);
let className = "grip-column";
if (colSelected) {
className += " selected";
}
if (index === 0) {
className += " first";
} else if (index === cells.length - 1) {
className += " last";
}
const grip = document.createElement("a");
grip.className = className;
grip.addEventListener("mousedown", (event) => {
event.preventDefault();
event.stopImmediatePropagation();
this.editor.view.dispatch(
selectColumn(
index,
event.metaKey || event.shiftKey
)(state)
);
});
return grip;
})
);
});
}
return DecorationSet.create(doc, decorations);
},
},
}),
];
}
}

View File

@ -0,0 +1,149 @@
import Token from "markdown-it/lib/token";
import { NodeSpec } from "prosemirror-model";
import { Plugin } from "prosemirror-state";
import { DecorationSet, Decoration, EditorView } from "prosemirror-view";
import { addColumnBefore, selectColumn } from "../commands/table";
import { getCellAttrs, setCellAttrs } from "../lib/table";
import { getCellsInRow, isColumnSelected } from "../queries/table";
import { EditorStyleHelper } from "../styles/EditorStyleHelper";
import { cn } from "../styles/utils";
import Node from "./Node";
export default class TableHeader extends Node {
get name() {
return "th";
}
get schema(): NodeSpec {
return {
content: "block+",
tableRole: "header_cell",
isolating: true,
parseDOM: [{ tag: "th", getAttrs: getCellAttrs }],
toDOM(node) {
return ["th", setCellAttrs(node), 0];
},
attrs: {
colspan: { default: 1 },
rowspan: { default: 1 },
alignment: { default: null },
colwidth: { default: null },
},
};
}
toMarkdown() {
// see: renderTable
}
parseMarkdown() {
return {
block: "th",
getAttrs: (tok: Token) => ({ alignment: tok.info }),
};
}
get plugins() {
function buildAddColumnDecoration(pos: number, index: number) {
const className = cn(EditorStyleHelper.tableAddColumn, {
first: index === 0,
});
return Decoration.widget(
pos + 1,
() => {
const plus = document.createElement("a");
plus.role = "button";
plus.className = className;
plus.dataset.index = index.toString();
return plus;
},
{
key: cn(className, index),
}
);
}
return [
new Plugin({
props: {
handleDOMEvents: {
mousedown: (view: EditorView, event: MouseEvent) => {
if (!(event.target instanceof HTMLElement)) {
return false;
}
const targetAddColumn = event.target.closest(
`.${EditorStyleHelper.tableAddColumn}`
);
if (targetAddColumn) {
event.preventDefault();
event.stopImmediatePropagation();
const index = Number(
targetAddColumn.getAttribute("data-index")
);
addColumnBefore({ index })(view.state, view.dispatch);
return true;
}
const targetGripColumn = event.target.closest(
`.${EditorStyleHelper.tableGripColumn}`
);
if (targetGripColumn) {
event.preventDefault();
event.stopImmediatePropagation();
selectColumn(
Number(targetGripColumn.getAttribute("data-index")),
event.metaKey || event.shiftKey
)(view.state, view.dispatch);
return true;
}
return false;
},
},
decorations: (state) => {
const { doc } = state;
const decorations: Decoration[] = [];
const cols = getCellsInRow(0)(state);
if (cols) {
cols.forEach((pos, index) => {
const className = cn(EditorStyleHelper.tableGripColumn, {
selected: isColumnSelected(index)(state),
first: index === 0,
last: index === cols.length - 1,
});
decorations.push(
Decoration.widget(
pos + 1,
() => {
const grip = document.createElement("a");
grip.role = "button";
grip.className = className;
grip.dataset.index = index.toString();
return grip;
},
{
key: cn(className, index),
}
)
);
if (index === 0) {
decorations.push(buildAddColumnDecoration(pos, index));
}
decorations.push(buildAddColumnDecoration(pos, index + 1));
});
}
return DecorationSet.create(doc, decorations);
},
},
}),
];
}
}

View File

@ -0,0 +1,91 @@
import { Node } from "prosemirror-model";
import { TableView as ProsemirrorTableView } from "prosemirror-tables";
import { EditorStyleHelper } from "../styles/EditorStyleHelper";
import { TableLayout } from "../types";
export class TableView extends ProsemirrorTableView {
public constructor(public node: Node, public cellMinWidth: number) {
super(node, cellMinWidth);
this.dom.removeChild(this.table);
this.dom.classList.add(EditorStyleHelper.table);
// Add an extra wrapper to enable scrolling
this.scrollable = this.dom.appendChild(document.createElement("div"));
this.scrollable.appendChild(this.table);
this.scrollable.classList.add(EditorStyleHelper.tableScrollable);
this.scrollable.addEventListener(
"scroll",
() => {
this.updateClassList(this.node);
},
{
passive: true,
}
);
this.updateClassList(node);
// We need to wait for the next tick to ensure dom is rendered and scroll shadows are correct.
setTimeout(() => {
if (this.dom) {
this.updateClassList(node);
}
}, 0);
}
public override update(node: Node) {
this.updateClassList(node);
return super.update(node);
}
public override ignoreMutation(record: MutationRecord): boolean {
if (
record.type === "attributes" &&
record.target === this.dom &&
(record.attributeName === "class" || record.attributeName === "style")
) {
return true;
}
return (
record.type === "attributes" &&
(record.target === this.table || this.colgroup.contains(record.target))
);
}
private updateClassList(node: Node) {
this.dom.classList.toggle(
EditorStyleHelper.tableFullWidth,
node.attrs.layout === TableLayout.fullWidth
);
const shadowLeft = !!(this.scrollable && this.scrollable.scrollLeft > 0);
const shadowRight = !!(
this.scrollable &&
this.scrollable.scrollWidth > this.scrollable.clientWidth &&
this.scrollable.scrollLeft + this.scrollable.clientWidth <
this.scrollable.scrollWidth - 1
);
this.dom.classList.toggle(EditorStyleHelper.tableShadowLeft, shadowLeft);
this.dom.classList.toggle(EditorStyleHelper.tableShadowRight, shadowRight);
if (this.scrollable) {
this.dom.style.setProperty(
"--table-height",
`${this.scrollable?.clientHeight}px`
);
this.dom.style.setProperty(
"--table-width",
`${this.scrollable?.clientWidth}px`
);
} else {
this.dom.style.removeProperty("--table-height");
this.dom.style.removeProperty("--table-width");
}
}
private scrollable: HTMLDivElement | null = null;
}

View File

@ -39,7 +39,7 @@ import Paragraph from "./Paragraph";
import SimpleImage from "./SimpleImage";
import Table from "./Table";
import TableCell from "./TableCell";
import TableHeadCell from "./TableHeadCell";
import TableHeader from "./TableHeader";
import TableRow from "./TableRow";
import Text from "./Text";
import Video from "./Video";
@ -77,12 +77,7 @@ export const listExtensions: Nodes = [
ListItem,
];
export const tableExtensions: Nodes = [
Table,
TableCell,
TableHeadCell,
TableRow,
];
export const tableExtensions: Nodes = [Table, TableCell, TableHeader, TableRow];
/**
* The full set of nodes that are used in the editor. This is used for rich

View File

@ -1,5 +1,10 @@
import { EditorState } from "prosemirror-state";
import { CellSelection, isInTable, selectedRect } from "prosemirror-tables";
import {
CellSelection,
TableRect,
isInTable,
selectedRect,
} from "prosemirror-tables";
export function getColumnIndex(state: EditorState): number | undefined {
if (state.selection instanceof CellSelection) {
@ -70,6 +75,37 @@ export function isColumnSelected(index: number) {
};
}
/**
* Check if the header is enabled for the given type and table rect
*
* @param state The editor state
* @param type The type of header to check
* @param rect The table rect
* @returns Boolean indicating if the header is enabled
*/
export function isHeaderEnabled(
state: EditorState,
type: "row" | "column",
rect: TableRect
): boolean {
// Get cell positions for first row or first column
const cellPositions = rect.map.cellsInRect({
left: 0,
top: 0,
right: type === "row" ? rect.map.width : 1,
bottom: type === "column" ? rect.map.height : 1,
});
for (let i = 0; i < cellPositions.length; i++) {
const cell = rect.table.nodeAt(cellPositions[i]);
if (cell && cell.type !== state.schema.nodes.th) {
return false;
}
}
return true;
}
export function isRowSelected(index: number) {
return (state: EditorState): boolean => {
if (state.selection instanceof CellSelection) {
@ -90,6 +126,8 @@ export function isTableSelected(state: EditorState): boolean {
rect.top === 0 &&
rect.left === 0 &&
rect.bottom === rect.map.height &&
rect.right === rect.map.width
rect.right === rect.map.width &&
!state.selection.empty &&
state.selection instanceof CellSelection
);
}

View File

@ -0,0 +1,39 @@
/**
* Class names and values used by the editor.
*/
export class EditorStyleHelper {
// Tables
/** Table wrapper */
static readonly table = "table-wrapper";
/** Table grip (circle in top left) */
static readonly tableGrip = "table-grip";
/** Table row grip */
static readonly tableGripRow = "table-grip-row";
/** Table column grip */
static readonly tableGripColumn = "table-grip-column";
/** "Plus" to add column on tables */
static readonly tableAddColumn = "table-add-column";
/** "Plus" to add row on tables */
static readonly tableAddRow = "table-add-row";
/** Scrollable area of table */
static readonly tableScrollable = "table-scrollable";
/** Full-width table layout */
static readonly tableFullWidth = "table-full-width";
/** Shadow on the right side of the table */
static readonly tableShadowRight = "table-shadow-right";
/** Shadow on the left side of the table */
static readonly tableShadowLeft = "table-shadow-left";
/** Minimum padding around editor */
static readonly padding = 32;
}

View File

@ -0,0 +1,23 @@
/**
* Combines class names into a single string. If the value is an object, it will only include keys
* with a truthy value.
*
* @param classNames An array of class names
* @returns A single string of class names
*/
export function cn(
...classNames: (string | number | Record<string, boolean> | undefined)[]
) {
return classNames
.filter(Boolean)
.map((item) => {
if (typeof item === "object") {
return Object.entries(item)
.filter(([, value]) => value)
.map(([key]) => key)
.join(" ");
}
return item;
})
.join(" ");
}

View File

@ -13,6 +13,10 @@ export enum EventType {
LinkToolbarOpen = "linkMenuOpen",
}
export enum TableLayout {
fullWidth = "full-width",
}
export type MenuItem = {
icon?: React.ReactElement;
name?: string;

View File

@ -352,11 +352,14 @@
"Replace": "Replace",
"Replace all": "Replace all",
"Profile picture": "Profile picture",
"Insert after": "Insert after",
"Insert before": "Insert before",
"Add column after": "Add column after",
"Add column before": "Add column before",
"Add row after": "Add row after",
"Add row before": "Add row before",
"Align center": "Align center",
"Align left": "Align left",
"Align right": "Align right",
"Default width": "Default width",
"Full width": "Full width",
"Bulleted list": "Bulleted list",
"Todo list": "Task list",
@ -410,6 +413,7 @@
"Sort ascending": "Sort ascending",
"Sort descending": "Sort descending",
"Table": "Table",
"Toggle header": "Toggle header",
"Math inline (LaTeX)": "Math inline (LaTeX)",
"Math block (LaTeX)": "Math block (LaTeX)",
"Tip": "Tip",

View File

@ -23,6 +23,7 @@ export default createGlobalStyle<Props>`
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
--pointer: ${(props) => (props.useCursorPointer ? "pointer" : "default")};
overscroll-behavior-x: none;
}
body,

View File

@ -95,7 +95,7 @@ const buildBaseTheme = (input: Partial<Colors>) => {
noticeWarningText: colors.almostBlack,
noticeSuccessBackground: colors.brand.green,
noticeSuccessText: colors.almostBlack,
tableSelectedBackground: transparentize(0.8, colors.accent),
tableSelectedBackground: transparentize(0.9, colors.accent),
breakpoints,
...colors,
...spacing,
@ -145,7 +145,6 @@ export const buildLightTheme = (input: Partial<Colors>): DefaultTheme => {
inputBorderFocused: colors.slate,
listItemHoverBackground: colors.warmGrey,
mentionBackground: colors.warmGrey,
tableDivider: colors.smokeDark,
tableSelected: colors.accent,
buttonNeutralBackground: colors.white,
buttonNeutralText: colors.almostBlack,
@ -208,7 +207,6 @@ export const buildDarkTheme = (input: Partial<Colors>): DefaultTheme => {
inputBorderFocused: colors.slate,
listItemHoverBackground: colors.white10,
mentionBackground: colors.white10,
tableDivider: colors.lightBlack,
tableSelected: colors.accent,
buttonNeutralBackground: colors.almostBlack,
buttonNeutralText: colors.white,