mirror of
https://github.com/outline/outline.git
synced 2025-04-10 03:03:45 +00:00
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:
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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"),
|
||||
|
1
app/typings/styled-components.d.ts
vendored
1
app/typings/styled-components.d.ts
vendored
@ -9,7 +9,6 @@ declare module "styled-components" {
|
||||
text: string;
|
||||
cursor: string;
|
||||
divider: string;
|
||||
tableDivider: string;
|
||||
tableSelected: string;
|
||||
tableSelectedBackground: string;
|
||||
quote: string;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
67
shared/editor/lib/table.ts
Normal file
67
shared/editor/lib/table.ts
Normal 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;
|
||||
}
|
@ -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";
|
||||
|
||||
|
@ -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())) {
|
||||
|
@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
149
shared/editor/nodes/TableHeader.ts
Normal file
149
shared/editor/nodes/TableHeader.ts
Normal 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);
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
91
shared/editor/nodes/TableView.ts
Normal file
91
shared/editor/nodes/TableView.ts
Normal 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;
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
39
shared/editor/styles/EditorStyleHelper.ts
Normal file
39
shared/editor/styles/EditorStyleHelper.ts
Normal 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;
|
||||
}
|
23
shared/editor/styles/utils.ts
Normal file
23
shared/editor/styles/utils.ts
Normal 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(" ");
|
||||
}
|
@ -13,6 +13,10 @@ export enum EventType {
|
||||
LinkToolbarOpen = "linkMenuOpen",
|
||||
}
|
||||
|
||||
export enum TableLayout {
|
||||
fullWidth = "full-width",
|
||||
}
|
||||
|
||||
export type MenuItem = {
|
||||
icon?: React.ReactElement;
|
||||
name?: string;
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
Reference in New Issue
Block a user