mirror of
https://github.com/docmost/docmost.git
synced 2025-11-11 09:22:06 +10:00
Compare commits
17 Commits
fix/billin
...
improve-ta
| Author | SHA1 | Date | |
|---|---|---|---|
| 54efe3bb91 | |||
| 1b4d88f98a | |||
| 07d5833695 | |||
| bcca52fa70 | |||
| ad1c692e14 | |||
| 82a2fc997d | |||
| 16ec218ba7 | |||
| 608783b5cf | |||
| 5f5f1484db | |||
| f4082171ec | |||
| 6792a191b1 | |||
| e51a93221c | |||
| e856c8eb69 | |||
| 276f6ea45e | |||
| 86aaf2f6af | |||
| 0d8b6ec4f0 | |||
| acc725db9c |
@ -194,11 +194,9 @@ export default function BillingPlans() {
|
||||
: `per month`}
|
||||
</Text>
|
||||
</Group>
|
||||
{isAnnual && (
|
||||
<Text size="sm" c="dimmed">
|
||||
Billed annually
|
||||
</Text>
|
||||
)}
|
||||
<Text size="sm" c="dimmed">
|
||||
{isAnnual ? "Billed annually" : "Billed monthly"}
|
||||
</Text>
|
||||
{plan.billingScheme === 'tiered' && plan.pricingTiers && (
|
||||
<Text size="md" fw={500}>
|
||||
For {plan.pricingTiers.find(tier => tier.upTo.toString() === selectedTierValue)?.upTo || plan.pricingTiers[0].upTo} users
|
||||
|
||||
@ -0,0 +1,145 @@
|
||||
import React, { FC } from "react";
|
||||
import { IconCheck, IconPalette } from "@tabler/icons-react";
|
||||
import {
|
||||
ActionIcon,
|
||||
ColorSwatch,
|
||||
Popover,
|
||||
Stack,
|
||||
Text,
|
||||
Tooltip,
|
||||
UnstyledButton,
|
||||
} from "@mantine/core";
|
||||
import { useEditor } from "@tiptap/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface TableColorItem {
|
||||
name: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface TableBackgroundColorProps {
|
||||
editor: ReturnType<typeof useEditor>;
|
||||
}
|
||||
|
||||
const TABLE_COLORS: TableColorItem[] = [
|
||||
{ name: "Default", color: "" },
|
||||
{ name: "Blue", color: "#b4d5ff" },
|
||||
{ name: "Green", color: "#acf5d2" },
|
||||
{ name: "Yellow", color: "#fef1b4" },
|
||||
{ name: "Red", color: "#ffbead" },
|
||||
{ name: "Pink", color: "#ffc7fe" },
|
||||
{ name: "Gray", color: "#eaecef" },
|
||||
{ name: "Purple", color: "#c1b7f2" },
|
||||
];
|
||||
|
||||
export const TableBackgroundColor: FC<TableBackgroundColorProps> = ({
|
||||
editor,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [opened, setOpened] = React.useState(false);
|
||||
|
||||
const setTableCellBackground = (color: string, colorName: string) => {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.updateAttributes("tableCell", {
|
||||
backgroundColor: color || null,
|
||||
backgroundColorName: color ? colorName : null
|
||||
})
|
||||
.updateAttributes("tableHeader", {
|
||||
backgroundColor: color || null,
|
||||
backgroundColorName: color ? colorName : null
|
||||
})
|
||||
.run();
|
||||
setOpened(false);
|
||||
};
|
||||
|
||||
// Get current cell's background color
|
||||
const getCurrentColor = () => {
|
||||
if (editor.isActive("tableCell")) {
|
||||
const attrs = editor.getAttributes("tableCell");
|
||||
return attrs.backgroundColor || "";
|
||||
}
|
||||
if (editor.isActive("tableHeader")) {
|
||||
const attrs = editor.getAttributes("tableHeader");
|
||||
return attrs.backgroundColor || "";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const currentColor = getCurrentColor();
|
||||
|
||||
return (
|
||||
<Popover
|
||||
width={200}
|
||||
position="bottom"
|
||||
opened={opened}
|
||||
onChange={setOpened}
|
||||
withArrow
|
||||
transitionProps={{ transition: "pop" }}
|
||||
>
|
||||
<Popover.Target>
|
||||
<Tooltip label={t("Background color")} withArrow>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="lg"
|
||||
aria-label={t("Background color")}
|
||||
onClick={() => setOpened(!opened)}
|
||||
>
|
||||
<IconPalette size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Popover.Target>
|
||||
|
||||
<Popover.Dropdown>
|
||||
<Stack gap="xs">
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("Background color")}
|
||||
</Text>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(4, 1fr)",
|
||||
gap: "8px",
|
||||
}}
|
||||
>
|
||||
{TABLE_COLORS.map((item, index) => (
|
||||
<UnstyledButton
|
||||
key={index}
|
||||
onClick={() => setTableCellBackground(item.color, item.name)}
|
||||
style={{
|
||||
position: "relative",
|
||||
width: "24px",
|
||||
height: "24px",
|
||||
}}
|
||||
title={t(item.name)}
|
||||
>
|
||||
<ColorSwatch
|
||||
color={item.color || "#ffffff"}
|
||||
size={24}
|
||||
style={{
|
||||
border: item.color === "" ? "1px solid #e5e7eb" : undefined,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
{currentColor === item.color && (
|
||||
<IconCheck
|
||||
size={18}
|
||||
style={{
|
||||
color:
|
||||
item.color === "" || item.color.startsWith("#F")
|
||||
? "#000000"
|
||||
: "#ffffff",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ColorSwatch>
|
||||
</UnstyledButton>
|
||||
))}
|
||||
</div>
|
||||
</Stack>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@ -12,8 +12,11 @@ import {
|
||||
IconColumnRemove,
|
||||
IconRowRemove,
|
||||
IconSquareToggle,
|
||||
IconTableRow,
|
||||
} from "@tabler/icons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TableBackgroundColor } from "./table-background-color";
|
||||
import { TableTextAlignment } from "./table-text-alignment";
|
||||
|
||||
export const TableCellMenu = React.memo(
|
||||
({ editor, appendTo }: EditorMenuProps): JSX.Element => {
|
||||
@ -45,6 +48,10 @@ export const TableCellMenu = React.memo(
|
||||
editor.chain().focus().deleteRow().run();
|
||||
}, [editor]);
|
||||
|
||||
const toggleHeaderCell = useCallback(() => {
|
||||
editor.chain().focus().toggleHeaderCell().run();
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<BaseBubbleMenu
|
||||
editor={editor}
|
||||
@ -60,6 +67,9 @@ export const TableCellMenu = React.memo(
|
||||
shouldShow={shouldShow}
|
||||
>
|
||||
<ActionIcon.Group>
|
||||
<TableBackgroundColor editor={editor} />
|
||||
<TableTextAlignment editor={editor} />
|
||||
|
||||
<Tooltip position="top" label={t("Merge cells")}>
|
||||
<ActionIcon
|
||||
onClick={mergeCells}
|
||||
@ -103,6 +113,17 @@ export const TableCellMenu = React.memo(
|
||||
<IconRowRemove size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip position="top" label={t("Toggle header cell")}>
|
||||
<ActionIcon
|
||||
onClick={toggleHeaderCell}
|
||||
variant="default"
|
||||
size="lg"
|
||||
aria-label={t("Toggle header cell")}
|
||||
>
|
||||
<IconTableRow size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</ActionIcon.Group>
|
||||
</BaseBubbleMenu>
|
||||
);
|
||||
|
||||
@ -0,0 +1,109 @@
|
||||
import React, { FC } from "react";
|
||||
import {
|
||||
IconAlignCenter,
|
||||
IconAlignLeft,
|
||||
IconAlignRight,
|
||||
IconCheck,
|
||||
} from "@tabler/icons-react";
|
||||
import {
|
||||
ActionIcon,
|
||||
Button,
|
||||
Popover,
|
||||
rem,
|
||||
ScrollArea,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { useEditor } from "@tiptap/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface TableTextAlignmentProps {
|
||||
editor: ReturnType<typeof useEditor>;
|
||||
}
|
||||
|
||||
interface AlignmentItem {
|
||||
name: string;
|
||||
icon: React.ElementType;
|
||||
command: () => void;
|
||||
isActive: () => boolean;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const TableTextAlignment: FC<TableTextAlignmentProps> = ({ editor }) => {
|
||||
const { t } = useTranslation();
|
||||
const [opened, setOpened] = React.useState(false);
|
||||
|
||||
const items: AlignmentItem[] = [
|
||||
{
|
||||
name: "Align left",
|
||||
value: "left",
|
||||
isActive: () => editor.isActive({ textAlign: "left" }),
|
||||
command: () => editor.chain().focus().setTextAlign("left").run(),
|
||||
icon: IconAlignLeft,
|
||||
},
|
||||
{
|
||||
name: "Align center",
|
||||
value: "center",
|
||||
isActive: () => editor.isActive({ textAlign: "center" }),
|
||||
command: () => editor.chain().focus().setTextAlign("center").run(),
|
||||
icon: IconAlignCenter,
|
||||
},
|
||||
{
|
||||
name: "Align right",
|
||||
value: "right",
|
||||
isActive: () => editor.isActive({ textAlign: "right" }),
|
||||
command: () => editor.chain().focus().setTextAlign("right").run(),
|
||||
icon: IconAlignRight,
|
||||
},
|
||||
];
|
||||
|
||||
const activeItem = items.find((item) => item.isActive()) || items[0];
|
||||
|
||||
return (
|
||||
<Popover
|
||||
opened={opened}
|
||||
onChange={setOpened}
|
||||
position="bottom"
|
||||
withArrow
|
||||
transitionProps={{ transition: 'pop' }}
|
||||
>
|
||||
<Popover.Target>
|
||||
<Tooltip label={t("Text alignment")} withArrow>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="lg"
|
||||
aria-label={t("Text alignment")}
|
||||
onClick={() => setOpened(!opened)}
|
||||
>
|
||||
<activeItem.icon size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Popover.Target>
|
||||
|
||||
<Popover.Dropdown>
|
||||
<ScrollArea.Autosize type="scroll" mah={300}>
|
||||
<Button.Group orientation="vertical">
|
||||
{items.map((item, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="default"
|
||||
leftSection={<item.icon size={16} />}
|
||||
rightSection={
|
||||
item.isActive() && <IconCheck size={16} />
|
||||
}
|
||||
justify="left"
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
item.command();
|
||||
setOpened(false);
|
||||
}}
|
||||
style={{ border: "none" }}
|
||||
>
|
||||
{t(item.name)}
|
||||
</Button>
|
||||
))}
|
||||
</Button.Group>
|
||||
</ScrollArea.Autosize>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@ -11,7 +11,6 @@ import { Typography } from "@tiptap/extension-typography";
|
||||
import { TextStyle } from "@tiptap/extension-text-style";
|
||||
import { Color } from "@tiptap/extension-color";
|
||||
import Table from "@tiptap/extension-table";
|
||||
import TableHeader from "@tiptap/extension-table-header";
|
||||
import SlashCommand from "@/features/editor/extensions/slash-command";
|
||||
import { Collaboration } from "@tiptap/extension-collaboration";
|
||||
import { CollaborationCursor } from "@tiptap/extension-collaboration-cursor";
|
||||
@ -25,6 +24,7 @@ import {
|
||||
MathInline,
|
||||
TableCell,
|
||||
TableRow,
|
||||
TableHeader,
|
||||
TrailingNode,
|
||||
TiptapImage,
|
||||
Callout,
|
||||
|
||||
@ -217,6 +217,10 @@ export default function PageEditor({
|
||||
scrollMargin: 80,
|
||||
handleDOMEvents: {
|
||||
keydown: (_view, event) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyS') {
|
||||
event.preventDefault();
|
||||
return true;
|
||||
}
|
||||
if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) {
|
||||
const slashCommand = document.querySelector("#slash-command");
|
||||
if (slashCommand) {
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
overflow-x: auto;
|
||||
& table {
|
||||
overflow-x: hidden;
|
||||
min-width: 700px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,8 +39,8 @@
|
||||
|
||||
th {
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-gray-1),
|
||||
var(--mantine-color-dark-5)
|
||||
var(--mantine-color-gray-1),
|
||||
var(--mantine-color-dark-5)
|
||||
);
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
@ -66,8 +67,54 @@
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/* Table cell background colors with dark mode support */
|
||||
.ProseMirror {
|
||||
table {
|
||||
@mixin dark {
|
||||
/* Blue */
|
||||
td[data-background-color="#b4d5ff"],
|
||||
th[data-background-color="#b4d5ff"] {
|
||||
background-color: #1a3a5c !important;
|
||||
}
|
||||
|
||||
/* Green */
|
||||
td[data-background-color="#acf5d2"],
|
||||
th[data-background-color="#acf5d2"] {
|
||||
background-color: #1a4d3a !important;
|
||||
}
|
||||
|
||||
/* Yellow */
|
||||
td[data-background-color="#fef1b4"],
|
||||
th[data-background-color="#fef1b4"] {
|
||||
background-color: #7c5014 !important;
|
||||
}
|
||||
|
||||
/* Red */
|
||||
td[data-background-color="#ffbead"],
|
||||
th[data-background-color="#ffbead"] {
|
||||
background-color: #5c2a23 !important;
|
||||
}
|
||||
|
||||
/* Pink */
|
||||
td[data-background-color="#ffc7fe"],
|
||||
th[data-background-color="#ffc7fe"] {
|
||||
background-color: #4d2a4d !important;
|
||||
}
|
||||
|
||||
/* Gray */
|
||||
td[data-background-color="#eaecef"],
|
||||
th[data-background-color="#eaecef"] {
|
||||
background-color: #2a2e33 !important;
|
||||
}
|
||||
|
||||
/* Purple */
|
||||
td[data-background-color="#c1b7f2"],
|
||||
th[data-background-color="#c1b7f2"] {
|
||||
background-color: #3a2f5c !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,6 +26,9 @@ const renderMultiSelectOption: MultiSelectProps["renderOption"] = ({
|
||||
{option["type"] === "group" && <IconGroupCircle />}
|
||||
<div>
|
||||
<Text size="sm" lineClamp={1}>{option.label}</Text>
|
||||
{option["type"] === "user" && option["email"] && (
|
||||
<Text size="xs" c="dimmed" lineClamp={1}>{option["email"]}</Text>
|
||||
)}
|
||||
</div>
|
||||
</Group>
|
||||
);
|
||||
@ -47,6 +50,7 @@ export function MultiMemberSelect({ onChange }: MultiMemberSelectProps) {
|
||||
const userItems = suggestion?.users.map((user: IUser) => ({
|
||||
value: `user-${user.id}`,
|
||||
label: user.name,
|
||||
email: user.email,
|
||||
avatarUrl: user.avatarUrl,
|
||||
type: "user",
|
||||
}));
|
||||
|
||||
@ -11,7 +11,6 @@ import { TextStyle } from '@tiptap/extension-text-style';
|
||||
import { Color } from '@tiptap/extension-color';
|
||||
import { Youtube } from '@tiptap/extension-youtube';
|
||||
import Table from '@tiptap/extension-table';
|
||||
import TableHeader from '@tiptap/extension-table-header';
|
||||
import {
|
||||
Callout,
|
||||
Comment,
|
||||
@ -22,6 +21,7 @@ import {
|
||||
LinkExtension,
|
||||
MathBlock,
|
||||
MathInline,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
TableRow,
|
||||
TiptapImage,
|
||||
@ -31,7 +31,7 @@ import {
|
||||
Drawio,
|
||||
Excalidraw,
|
||||
Embed,
|
||||
Mention
|
||||
Mention,
|
||||
} from '@docmost/editor-ext';
|
||||
import { generateText, getSchema, JSONContent } from '@tiptap/core';
|
||||
import { generateHTML } from '../common/helpers/prosemirror/html';
|
||||
@ -46,7 +46,7 @@ export const tiptapExtensions = [
|
||||
codeBlock: false,
|
||||
}),
|
||||
Comment,
|
||||
TextAlign.configure({ types: ["heading", "paragraph"] }),
|
||||
TextAlign.configure({ types: ['heading', 'paragraph'] }),
|
||||
TaskList,
|
||||
TaskItem,
|
||||
Underline,
|
||||
@ -64,9 +64,9 @@ export const tiptapExtensions = [
|
||||
DetailsContent,
|
||||
DetailsSummary,
|
||||
Table,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableRow,
|
||||
TableHeader,
|
||||
Youtube,
|
||||
TiptapImage,
|
||||
TiptapVideo,
|
||||
@ -76,7 +76,7 @@ export const tiptapExtensions = [
|
||||
Drawio,
|
||||
Excalidraw,
|
||||
Embed,
|
||||
Mention
|
||||
Mention,
|
||||
] as any;
|
||||
|
||||
export function jsonToHtml(tiptapJson: any) {
|
||||
|
||||
@ -46,6 +46,10 @@ export class AuthenticationExtension implements Extension {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
if (user.deactivatedAt || user.deletedAt) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
const page = await this.pageRepo.findById(pageId);
|
||||
if (!page) {
|
||||
this.logger.warn(`Page not found: ${pageId}`);
|
||||
|
||||
@ -108,7 +108,7 @@ export class AuthController {
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
return this.authService.getCollabToken(user.id, workspace.id);
|
||||
return this.authService.getCollabToken(user, workspace.id);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
|
||||
@ -22,7 +22,7 @@ import { ForgotPasswordDto } from '../dto/forgot-password.dto';
|
||||
import ForgotPasswordEmail from '@docmost/transactional/emails/forgot-password-email';
|
||||
import { UserTokenRepo } from '@docmost/db/repos/user-token/user-token.repo';
|
||||
import { PasswordResetDto } from '../dto/password-reset.dto';
|
||||
import { UserToken, Workspace } from '@docmost/db/types/entity.types';
|
||||
import { User, UserToken, Workspace } from '@docmost/db/types/entity.types';
|
||||
import { UserTokenType } from '../auth.constants';
|
||||
import { KyselyDB } from '@docmost/db/types/kysely.types';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
@ -222,9 +222,9 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
async getCollabToken(userId: string, workspaceId: string) {
|
||||
async getCollabToken(user: User, workspaceId: string) {
|
||||
const token = await this.tokenService.generateCollabToken(
|
||||
userId,
|
||||
user,
|
||||
workspaceId,
|
||||
);
|
||||
return { token };
|
||||
|
||||
@ -22,7 +22,7 @@ export class TokenService {
|
||||
) {}
|
||||
|
||||
async generateAccessToken(user: User): Promise<string> {
|
||||
if (user.deletedAt) {
|
||||
if (user.deactivatedAt || user.deletedAt) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
@ -35,12 +35,13 @@ export class TokenService {
|
||||
return this.jwtService.sign(payload);
|
||||
}
|
||||
|
||||
async generateCollabToken(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
): Promise<string> {
|
||||
async generateCollabToken(user: User, workspaceId: string): Promise<string> {
|
||||
if (user.deactivatedAt || user.deletedAt) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
const payload: JwtCollabPayload = {
|
||||
sub: userId,
|
||||
sub: user.id,
|
||||
workspaceId,
|
||||
type: JwtType.COLLAB,
|
||||
};
|
||||
|
||||
@ -42,7 +42,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
}
|
||||
const user = await this.userRepo.findById(payload.sub, payload.workspaceId);
|
||||
|
||||
if (!user || user.deletedAt) {
|
||||
if (!user || user.deactivatedAt || user.deletedAt) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
|
||||
@ -146,7 +146,6 @@ export class PageController {
|
||||
return this.pageService.getRecentPages(user.id, pagination);
|
||||
}
|
||||
|
||||
// TODO: scope to workspaces
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('/history')
|
||||
async getPageHistory(
|
||||
@ -155,6 +154,10 @@ export class PageController {
|
||||
@AuthUser() user: User,
|
||||
) {
|
||||
const page = await this.pageRepo.findById(dto.pageId);
|
||||
if (!page) {
|
||||
throw new NotFoundException('Page not found');
|
||||
}
|
||||
|
||||
const ability = await this.spaceAbility.createForUser(user, page.spaceId);
|
||||
if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) {
|
||||
throw new ForbiddenException();
|
||||
|
||||
@ -140,7 +140,7 @@ export class SearchService {
|
||||
if (suggestion.includeUsers) {
|
||||
users = await this.db
|
||||
.selectFrom('users')
|
||||
.select(['id', 'name', 'avatarUrl'])
|
||||
.select(['id', 'name', 'email', 'avatarUrl'])
|
||||
.where((eb) => eb(sql`LOWER(users.name)`, 'like', `%${query}%`))
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.where('deletedAt', 'is', null)
|
||||
|
||||
@ -3,4 +3,35 @@ import { TableCell as TiptapTableCell } from "@tiptap/extension-table-cell";
|
||||
export const TableCell = TiptapTableCell.extend({
|
||||
name: "tableCell",
|
||||
content: "paragraph+",
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
backgroundColor: {
|
||||
default: null,
|
||||
parseHTML: (element) => element.style.backgroundColor || null,
|
||||
renderHTML: (attributes) => {
|
||||
if (!attributes.backgroundColor) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
style: `background-color: ${attributes.backgroundColor}`,
|
||||
'data-background-color': attributes.backgroundColor,
|
||||
};
|
||||
},
|
||||
},
|
||||
backgroundColorName: {
|
||||
default: null,
|
||||
parseHTML: (element) => element.getAttribute('data-background-color-name') || null,
|
||||
renderHTML: (attributes) => {
|
||||
if (!attributes.backgroundColorName) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
'data-background-color-name': attributes.backgroundColorName.toLowerCase(),
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
37
packages/editor-ext/src/lib/table/header.ts
Normal file
37
packages/editor-ext/src/lib/table/header.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { TableHeader as TiptapTableHeader } from "@tiptap/extension-table-header";
|
||||
|
||||
export const TableHeader = TiptapTableHeader.extend({
|
||||
name: "tableHeader",
|
||||
content: "paragraph+",
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
backgroundColor: {
|
||||
default: null,
|
||||
parseHTML: (element) => element.style.backgroundColor || null,
|
||||
renderHTML: (attributes) => {
|
||||
if (!attributes.backgroundColor) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
style: `background-color: ${attributes.backgroundColor}`,
|
||||
'data-background-color': attributes.backgroundColor,
|
||||
};
|
||||
},
|
||||
},
|
||||
backgroundColorName: {
|
||||
default: null,
|
||||
parseHTML: (element) => element.getAttribute('data-background-color-name') || null,
|
||||
renderHTML: (attributes) => {
|
||||
if (!attributes.backgroundColorName) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
'data-background-color-name': attributes.backgroundColorName.toLowerCase(),
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./row";
|
||||
export * from "./cell";
|
||||
export * from "./header";
|
||||
|
||||
Reference in New Issue
Block a user