diff --git a/packages/client/src/components/tiptap/components/table/index.tsx b/packages/client/src/components/tiptap/components/table/index.tsx
deleted file mode 100644
index 852790bb..00000000
--- a/packages/client/src/components/tiptap/components/table/index.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
-import { Space, Popover, Tag, Input } from '@douyinfe/semi-ui';
-
-export const TableWrapper = ({ editor, node, updateAttributes }) => {
- const isEditable = editor.isEditable;
- const { color, text } = node.attrs;
- const content = {text || '点击设置状态'};
-
- console.log(node.attrs);
-
- return (
-
-
-
- );
-};
diff --git a/packages/client/src/components/tiptap/extensions/table.ts b/packages/client/src/components/tiptap/extensions/table.ts
index 4023b259..83aeb7ed 100644
--- a/packages/client/src/components/tiptap/extensions/table.ts
+++ b/packages/client/src/components/tiptap/extensions/table.ts
@@ -1,5 +1,14 @@
import { Table as BuiltInTable } from '@tiptap/extension-table';
+import { TableView } from '../views/tableView';
-export const Table = BuiltInTable.configure({
+export const Table = BuiltInTable.extend({
+ // @ts-ignore
+ addOptions() {
+ return {
+ ...this.parent?.(),
+ View: TableView,
+ };
+ },
+}).configure({
resizable: true,
});
diff --git a/packages/client/src/components/tiptap/extensions/tableCell.ts b/packages/client/src/components/tiptap/extensions/tableCell.ts
deleted file mode 100644
index 12d1df9a..00000000
--- a/packages/client/src/components/tiptap/extensions/tableCell.ts
+++ /dev/null
@@ -1,120 +0,0 @@
-import { TableCell as BuiltInTable } from '@tiptap/extension-table-cell';
-import { Plugin, PluginKey } from 'prosemirror-state';
-import { Decoration, DecorationSet } from 'prosemirror-view';
-import {
- getCellsInColumn,
- isRowSelected,
- isTableSelected,
- selectRow,
- selectTable,
-} from '../services/table';
-
-export const TableCell = BuiltInTable.extend({
- addProseMirrorPlugins() {
- return [
- new Plugin({
- key: new PluginKey(`${this.name}FloatMenu`),
- // view: () =>
- // new FloatMenuView({
- // editor: this.editor,
- // // has one selected should show
- // shouldShow: ({ editor }) => {
- // if (!editor.isEditable) {
- // return false;
- // }
- // const cells = getCellsInColumn(0)(editor.state.selection);
- // return !!cells?.some((cell, index) =>
- // isRowSelected(index)(editor.state.selection)
- // );
- // },
- // init: (dom, editor) => {
- // const insertTop = buttonView({
- // id: "insert-top",
- // name: this.options.dictionary.insertTop,
- // icon: DoubleUp({}),
- // });
- // insertTop.button.addEventListener("click", () => {
- // editor.chain().addRowBefore().run();
- // });
- // const insertBottom = buttonView({
- // id: "insert-bottom",
- // name: this.options.dictionary.insertBottom,
- // icon: DoubleDown({}),
- // });
- // insertBottom.button.addEventListener("click", () => {
- // editor.chain().addRowAfter().run();
- // });
- // const remove = buttonView({
- // name: this.options.dictionary.delete,
- // icon: Delete({}),
- // });
- // remove.button.addEventListener("click", () => {
- // if (isTableSelected(editor.state.selection)) {
- // editor.chain().deleteTable().run();
- // } else {
- // editor.chain().deleteRow().run();
- // }
- // });
-
- // dom.append(insertTop.button);
- // dom.append(insertBottom.button);
- // dom.append(remove.button);
- // },
- // }),
- props: {
- decorations: (state) => {
- const { doc, selection } = state;
- const decorations: Decoration[] = [];
- const cells = getCellsInColumn(0)(selection);
-
- if (cells) {
- cells.forEach(({ pos }, index) => {
- if (index === 0) {
- decorations.push(
- Decoration.widget(pos + 1, () => {
- const grip = document.createElement('a');
- grip.classList.add('grip-table');
- if (isTableSelected(selection)) {
- grip.classList.add('selected');
- }
- grip.addEventListener('mousedown', (event) => {
- event.preventDefault();
- event.stopImmediatePropagation();
- this.editor.view.dispatch(selectTable(this.editor.state.tr));
- });
- return grip;
- })
- );
- }
- decorations.push(
- Decoration.widget(pos + 1, () => {
- const rowSelected = isRowSelected(index)(selection);
- const grip = document.createElement('a');
- grip.classList.add('grip-row');
- if (rowSelected) {
- grip.classList.add('selected');
- }
- if (index === 0) {
- grip.classList.add('first');
- }
- if (index === cells.length - 1) {
- grip.classList.add('last');
- }
- grip.addEventListener('mousedown', (event) => {
- event.preventDefault();
- event.stopImmediatePropagation();
- this.editor.view.dispatch(selectRow(index)(this.editor.state.tr));
- });
- return grip;
- })
- );
- });
- }
-
- return DecorationSet.create(doc, decorations);
- },
- },
- }),
- ];
- },
-});
diff --git a/packages/client/src/components/tiptap/extensions/tableCell.tsx b/packages/client/src/components/tiptap/extensions/tableCell.tsx
new file mode 100644
index 00000000..b93e9cfb
--- /dev/null
+++ b/packages/client/src/components/tiptap/extensions/tableCell.tsx
@@ -0,0 +1,162 @@
+import ReactDOM from 'react-dom';
+import { Button } from '@douyinfe/semi-ui';
+import { IconDelete, IconPlus } from '@douyinfe/semi-icons';
+import { TableCell as BuiltInTableCell } from '@tiptap/extension-table-cell';
+import { Tooltip } from 'components/tooltip';
+import { Plugin, PluginKey } from 'prosemirror-state';
+import { Decoration, DecorationSet } from 'prosemirror-view';
+import {
+ getCellsInRow,
+ getCellsInColumn,
+ isRowSelected,
+ isTableSelected,
+ selectRow,
+ selectTable,
+} from '../services/table';
+import { elementInViewport } from '../services/dom';
+import { FloatMenuView } from '../views/floatMenuView';
+
+export const TableCell = BuiltInTableCell.extend({
+ addProseMirrorPlugins() {
+ const extensionThis = this;
+ let selectedRowIndex = -1;
+
+ return [
+ new Plugin({
+ key: new PluginKey(`${this.name}FloatMenu`),
+ view: () =>
+ new FloatMenuView({
+ editor: this.editor,
+ shouldShow: ({ editor }, floatMenuView) => {
+ if (!editor.isEditable) {
+ return false;
+ }
+ const cells = getCellsInColumn(0)(editor.state.selection);
+ if (selectedRowIndex > -1) {
+ const rowCells = getCellsInRow(selectedRowIndex)(editor.state.selection);
+ if (rowCells && rowCells[0]) {
+ const node = editor.view.nodeDOM(rowCells[0].pos) as HTMLElement;
+ if (node) {
+ const el = node.querySelector('a.grip-row') as HTMLElement;
+ if (el) {
+ console.log({ el });
+ floatMenuView.parentNode = el;
+ // const intersectionObserver = new IntersectionObserver(function (entries) {
+ // console.log('ob');
+ // if (entries[0].intersectionRatio <= 0) {
+ // floatMenuView.hide();
+ // }
+ // });
+ // intersectionObserver.observe(el);
+ }
+ }
+ }
+ }
+ return !!cells?.some((cell, index) => isRowSelected(index)(editor.state.selection));
+ },
+ init: (dom, editor) => {
+ dom.classList.add('table-controller-wrapper');
+ dom.classList.add('row');
+ ReactDOM.render(
+ <>
+
+ }
+ onClick={() => {
+ editor.chain().addRowBefore().run();
+ }}
+ />
+
+
+ }
+ onClick={() => {
+ editor.chain().deleteRow().run();
+ }}
+ />
+
+
+ }
+ onClick={() => {
+ editor.chain().addRowAfter().run();
+ }}
+ />
+
+ >,
+ dom
+ );
+ },
+ }),
+ props: {
+ decorations: (state) => {
+ if (!extensionThis.editor.isEditable) {
+ return;
+ }
+
+ const { doc, selection } = state;
+ const decorations: Decoration[] = [];
+ const cells = getCellsInColumn(0)(selection);
+
+ if (cells) {
+ cells.forEach(({ pos }, index) => {
+ if (index === 0) {
+ decorations.push(
+ Decoration.widget(pos + 1, () => {
+ const grip = document.createElement('a');
+ grip.classList.add('grip-table');
+ if (isTableSelected(selection)) {
+ grip.classList.add('selected');
+ }
+ grip.addEventListener('mousedown', (event) => {
+ event.preventDefault();
+ event.stopImmediatePropagation();
+ selectedRowIndex = -1;
+ this.editor.view.dispatch(selectTable(this.editor.state.tr));
+ });
+ return grip;
+ })
+ );
+ }
+ decorations.push(
+ Decoration.widget(pos + 1, () => {
+ const rowSelected = isRowSelected(index)(selection);
+ const grip = document.createElement('a');
+ grip.classList.add('grip-row');
+ if (rowSelected) {
+ grip.classList.add('selected');
+ }
+ if (index === 0) {
+ grip.classList.add('first');
+ }
+ if (index === cells.length - 1) {
+ grip.classList.add('last');
+ }
+ grip.addEventListener('mousedown', (event) => {
+ event.preventDefault();
+ event.stopImmediatePropagation();
+ selectedRowIndex = index;
+ this.editor.view.dispatch(selectRow(index)(this.editor.state.tr));
+ });
+ return grip;
+ })
+ );
+ });
+ }
+
+ return DecorationSet.create(doc, decorations);
+ },
+ },
+ }),
+ ];
+ },
+});
diff --git a/packages/client/src/components/tiptap/extensions/tableHeader.ts b/packages/client/src/components/tiptap/extensions/tableHeader.ts
deleted file mode 100644
index 8047fce5..00000000
--- a/packages/client/src/components/tiptap/extensions/tableHeader.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import { TableHeader as BuiltInTableHeader } from '@tiptap/extension-table-header';
-import { Plugin, PluginKey } from 'prosemirror-state';
-import { Decoration, DecorationSet } from 'prosemirror-view';
-import { getCellsInRow, isColumnSelected, isTableSelected, selectColumn } from '../services/table';
-
-export const TableHeader = BuiltInTableHeader.extend({
- addProseMirrorPlugins() {
- return [
- new Plugin({
- key: new PluginKey(`${this.name}FloatMenu`),
- // view: () =>
- // new FloatMenuView({
- // editor: this.editor,
- // // has one selected should show
- // shouldShow: ({ editor }) => {
- // if (!editor.isEditable) {
- // return false;
- // }
- // const selection = editor.state.selection;
- // if (isTableSelected(selection)) {
- // return false;
- // }
- // const cells = getCellsInRow(0)(selection);
- // return !!cells?.some((cell, index) =>
- // isColumnSelected(index)(selection)
- // );
- // },
- // init: (dom, editor) => {
- // const insertLeft = buttonView({
- // name: this.options.dictionary.insertLeft,
- // icon: DoubleLeft({}),
- // });
- // insertLeft.button.addEventListener("click", () => {
- // editor.chain().addColumnBefore().run();
- // });
- // const insertRight = buttonView({
- // name: this.options.dictionary.insertRight,
- // icon: DoubleRight({}),
- // });
- // insertRight.button.addEventListener("click", () => {
- // editor.chain().addColumnAfter().run();
- // });
- // const remove = buttonView({
- // name: this.options.dictionary.delete,
- // icon: Delete({}),
- // });
- // remove.button.addEventListener("click", () => {
- // editor.chain().deleteColumn().run();
- // });
-
- // dom.append(insertLeft.button);
- // dom.append(insertRight.button);
- // dom.append(remove.button);
- // },
- // }),
- props: {
- decorations: (state) => {
- const { doc, selection } = state;
- const decorations: Decoration[] = [];
- const cells = getCellsInRow(0)(selection);
-
- if (cells) {
- cells.forEach(({ pos }, index) => {
- decorations.push(
- Decoration.widget(pos + 1, () => {
- const colSelected = isColumnSelected(index)(selection);
- const grip = document.createElement('a');
- grip.classList.add('grip-column');
- if (colSelected) {
- grip.classList.add('selected');
- }
- if (index === 0) {
- grip.classList.add('first');
- } else if (index === cells.length - 1) {
- grip.classList.add('last');
- }
- grip.addEventListener('mousedown', (event) => {
- event.preventDefault();
- event.stopImmediatePropagation();
- this.editor.view.dispatch(selectColumn(index)(this.editor.state.tr));
- });
- return grip;
- })
- );
- });
- }
-
- return DecorationSet.create(doc, decorations);
- },
- },
- }),
- ];
- },
-});
diff --git a/packages/client/src/components/tiptap/extensions/tableHeader.tsx b/packages/client/src/components/tiptap/extensions/tableHeader.tsx
new file mode 100644
index 00000000..2f2485ee
--- /dev/null
+++ b/packages/client/src/components/tiptap/extensions/tableHeader.tsx
@@ -0,0 +1,116 @@
+import ReactDOM from 'react-dom';
+import { Button, Space } from '@douyinfe/semi-ui';
+import { IconDelete, IconPlus } from '@douyinfe/semi-icons';
+import { TableHeader as BuiltInTableHeader } from '@tiptap/extension-table-header';
+import { Tooltip } from 'components/tooltip';
+import { Plugin, PluginKey } from 'prosemirror-state';
+import { Decoration, DecorationSet } from 'prosemirror-view';
+import { getCellsInRow, isColumnSelected, isTableSelected, selectColumn } from '../services/table';
+import { FloatMenuView } from '../views/floatMenuView';
+
+export const TableHeader = BuiltInTableHeader.extend({
+ addProseMirrorPlugins() {
+ const extensionThis = this;
+
+ return [
+ new Plugin({
+ key: new PluginKey(`${this.name}FloatMenu`),
+ view: () =>
+ new FloatMenuView({
+ editor: this.editor,
+ shouldShow: ({ editor }) => {
+ if (!editor.isEditable) {
+ return false;
+ }
+ const selection = editor.state.selection;
+ if (isTableSelected(selection)) {
+ return false;
+ }
+ const cells = getCellsInRow(0)(selection);
+ return !!cells?.some((cell, index) => isColumnSelected(index)(selection));
+ },
+ init: (dom, editor) => {
+ dom.classList.add('table-controller-wrapper');
+ ReactDOM.render(
+
+
+ }
+ onClick={() => {
+ editor.chain().addColumnBefore().run();
+ }}
+ />
+
+
+ }
+ onClick={() => {
+ editor.chain().deleteColumn().run();
+ }}
+ />
+
+
+ }
+ onClick={() => {
+ editor.chain().addColumnAfter().run();
+ }}
+ />
+
+ ,
+ dom
+ );
+ },
+ }),
+ props: {
+ decorations: (state) => {
+ if (!extensionThis.editor.isEditable) {
+ return;
+ }
+
+ const { doc, selection } = state;
+ const decorations: Decoration[] = [];
+ const cells = getCellsInRow(0)(selection);
+
+ if (cells) {
+ cells.forEach(({ pos }, index) => {
+ decorations.push(
+ Decoration.widget(pos + 1, () => {
+ const colSelected = isColumnSelected(index)(selection);
+ const grip = document.createElement('a');
+ grip.classList.add('grip-column');
+ if (colSelected) {
+ grip.classList.add('selected');
+ }
+ if (index === 0) {
+ grip.classList.add('first');
+ } else if (index === cells.length - 1) {
+ grip.classList.add('last');
+ }
+ grip.addEventListener('mousedown', (event) => {
+ event.preventDefault();
+ event.stopImmediatePropagation();
+ this.editor.view.dispatch(selectColumn(index)(this.editor.state.tr));
+ });
+ return grip;
+ })
+ );
+ });
+ }
+
+ return DecorationSet.create(doc, decorations);
+ },
+ },
+ }),
+ ];
+ },
+});
diff --git a/packages/client/src/components/tiptap/extensions/trailingNode.tsx b/packages/client/src/components/tiptap/extensions/trailingNode.ts
similarity index 100%
rename from packages/client/src/components/tiptap/extensions/trailingNode.tsx
rename to packages/client/src/components/tiptap/extensions/trailingNode.ts
diff --git a/packages/client/src/components/tiptap/menubar.tsx b/packages/client/src/components/tiptap/menubar.tsx
index bc891764..9ead027b 100644
--- a/packages/client/src/components/tiptap/menubar.tsx
+++ b/packages/client/src/components/tiptap/menubar.tsx
@@ -3,14 +3,14 @@ import { Space, Button, Tooltip } from '@douyinfe/semi-ui';
import { IconUndo, IconRedo } from '@douyinfe/semi-icons';
import { IconClear } from 'components/icons';
import { Divider } from './components/divider';
-import { MediaInsertMenu } from './menus/media-insert';
+import { MediaInsertMenu } from './menus/mediaInsert';
import { Paragraph } from './menus/components/paragraph';
-import { FontSize } from './menus/components/font-size';
-import { BaseMenu } from './menus/base-menu';
+import { FontSize } from './menus/components/fontSize';
+import { BaseMenu } from './menus/baseMenu';
import { AlignMenu } from './menus/align';
import { ListMenu } from './menus/list';
-import { BaseInsertMenu } from './menus/base-insert';
-import { BaseBubbleMenu } from './menus/base-bubble-menu';
+import { BaseInsertMenu } from './menus/baseInsert';
+import { BaseBubbleMenu } from './menus/baseBubbleMenu';
import { ImageBubbleMenu } from './menus/image';
import { BannerBubbleMenu } from './menus/banner';
import { LinkBubbleMenu } from './menus/link';
diff --git a/packages/client/src/components/tiptap/menus/banner.tsx b/packages/client/src/components/tiptap/menus/banner.tsx
index 6c485f10..2af78f6a 100644
--- a/packages/client/src/components/tiptap/menus/banner.tsx
+++ b/packages/client/src/components/tiptap/menus/banner.tsx
@@ -7,7 +7,7 @@ import {
IconInfoCircle,
} from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip';
-import { BubbleMenu } from './components/bubble-menu';
+import { BubbleMenu } from './components/bubbleMenu';
import { Divider } from '../components/divider';
import { Banner } from '../extensions/banner';
import { deleteNode } from '../services/deleteNode';
diff --git a/packages/client/src/components/tiptap/menus/base-bubble-menu.tsx b/packages/client/src/components/tiptap/menus/baseBubbleMenu.tsx
similarity index 97%
rename from packages/client/src/components/tiptap/menus/base-bubble-menu.tsx
rename to packages/client/src/components/tiptap/menus/baseBubbleMenu.tsx
index 6257b1fa..c35b2af3 100644
--- a/packages/client/src/components/tiptap/menus/base-bubble-menu.tsx
+++ b/packages/client/src/components/tiptap/menus/baseBubbleMenu.tsx
@@ -13,7 +13,7 @@ import { Table } from '../extensions/table';
import { Katex } from '../extensions/katex';
import { DocumentReference } from '../extensions/documentReference';
import { DocumentChildren } from '../extensions/documentChildren';
-import { BaseMenu } from './base-menu';
+import { BaseMenu } from './baseMenu';
const OTHER_BUBBLE_MENU_TYPES = [
Title.name,
diff --git a/packages/client/src/components/tiptap/menus/base-insert.tsx b/packages/client/src/components/tiptap/menus/baseInsert.tsx
similarity index 100%
rename from packages/client/src/components/tiptap/menus/base-insert.tsx
rename to packages/client/src/components/tiptap/menus/baseInsert.tsx
diff --git a/packages/client/src/components/tiptap/menus/base-menu.tsx b/packages/client/src/components/tiptap/menus/baseMenu.tsx
similarity index 100%
rename from packages/client/src/components/tiptap/menus/base-menu.tsx
rename to packages/client/src/components/tiptap/menus/baseMenu.tsx
diff --git a/packages/client/src/components/tiptap/menus/components/bubble-menu/bubble-menu-plugin.tsx b/packages/client/src/components/tiptap/menus/components/bubbleMenu/bubbleMenuPlugin.tsx
similarity index 100%
rename from packages/client/src/components/tiptap/menus/components/bubble-menu/bubble-menu-plugin.tsx
rename to packages/client/src/components/tiptap/menus/components/bubbleMenu/bubbleMenuPlugin.tsx
diff --git a/packages/client/src/components/tiptap/menus/components/bubble-menu/index.tsx b/packages/client/src/components/tiptap/menus/components/bubbleMenu/index.tsx
similarity index 98%
rename from packages/client/src/components/tiptap/menus/components/bubble-menu/index.tsx
rename to packages/client/src/components/tiptap/menus/components/bubbleMenu/index.tsx
index 265d0577..5e1e1295 100644
--- a/packages/client/src/components/tiptap/menus/components/bubble-menu/index.tsx
+++ b/packages/client/src/components/tiptap/menus/components/bubbleMenu/index.tsx
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
-import { BubbleMenuPlugin, BubbleMenuPluginProps } from './bubble-menu-plugin';
+import { BubbleMenuPlugin, BubbleMenuPluginProps } from './bubbleMenuPlugin';
type Optional = Pick, K> & Omit;
diff --git a/packages/client/src/components/tiptap/menus/components/font-size.tsx b/packages/client/src/components/tiptap/menus/components/fontSize.tsx
similarity index 100%
rename from packages/client/src/components/tiptap/menus/components/font-size.tsx
rename to packages/client/src/components/tiptap/menus/components/fontSize.tsx
diff --git a/packages/client/src/components/tiptap/menus/image.tsx b/packages/client/src/components/tiptap/menus/image.tsx
index 4d0485e1..ed84ea20 100644
--- a/packages/client/src/components/tiptap/menus/image.tsx
+++ b/packages/client/src/components/tiptap/menus/image.tsx
@@ -9,7 +9,7 @@ import {
} from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip';
import { Upload } from 'components/upload';
-import { BubbleMenu } from './components/bubble-menu';
+import { BubbleMenu } from './components/bubbleMenu';
import { Divider } from '../components/divider';
import { Image } from '../extensions/image';
import { getImageOriginSize } from '../services/image';
diff --git a/packages/client/src/components/tiptap/menus/link.tsx b/packages/client/src/components/tiptap/menus/link.tsx
index 14ff126a..3bc2a233 100644
--- a/packages/client/src/components/tiptap/menus/link.tsx
+++ b/packages/client/src/components/tiptap/menus/link.tsx
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import { Space, Button, Input } from '@douyinfe/semi-ui';
import { IconExternalOpen, IconUnlink, IconTickCircle } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip';
-import { BubbleMenu } from './components/bubble-menu';
+import { BubbleMenu } from './components/bubbleMenu';
import { Link } from '../extensions/link';
export const LinkBubbleMenu = ({ editor }) => {
diff --git a/packages/client/src/components/tiptap/menus/media-insert.tsx b/packages/client/src/components/tiptap/menus/mediaInsert.tsx
similarity index 100%
rename from packages/client/src/components/tiptap/menus/media-insert.tsx
rename to packages/client/src/components/tiptap/menus/mediaInsert.tsx
diff --git a/packages/client/src/components/tiptap/menus/table.tsx b/packages/client/src/components/tiptap/menus/table.tsx
index f638c255..2e148927 100644
--- a/packages/client/src/components/tiptap/menus/table.tsx
+++ b/packages/client/src/components/tiptap/menus/table.tsx
@@ -11,13 +11,13 @@ import {
IconDeleteTable,
} from 'components/icons';
import { Tooltip } from 'components/tooltip';
-import { BubbleMenu } from './components/bubble-menu';
+import { BubbleMenu } from './components/bubbleMenu';
import { Table } from '../extensions/table';
export const TableBubbleMenu = ({ editor }) => {
return (
editor.isActive(Table.name)}
diff --git a/packages/client/src/components/tiptap/services/dom.ts b/packages/client/src/components/tiptap/services/dom.ts
index 16a789ee..7941b1b9 100644
--- a/packages/client/src/components/tiptap/services/dom.ts
+++ b/packages/client/src/components/tiptap/services/dom.ts
@@ -9,3 +9,23 @@ export const getParents = (element) => {
return parents;
};
+
+export function elementInViewport(el) {
+ let top = el.offsetTop;
+ let left = el.offsetLeft;
+ const width = el.offsetWidth;
+ const height = el.offsetHeight;
+
+ while (el.offsetParent) {
+ el = el.offsetParent;
+ top += el.offsetTop;
+ left += el.offsetLeft;
+ }
+
+ return (
+ top < window.pageYOffset + window.innerHeight &&
+ left < window.pageXOffset + window.innerWidth &&
+ top + height > window.pageYOffset &&
+ left + width > window.pageXOffset
+ );
+}
diff --git a/packages/client/src/components/tiptap/services/markdown/index.ts b/packages/client/src/components/tiptap/services/markdown/index.ts
index 553a8eab..1aae44d9 100644
--- a/packages/client/src/components/tiptap/services/markdown/index.ts
+++ b/packages/client/src/components/tiptap/services/markdown/index.ts
@@ -10,7 +10,11 @@ import splitMixedLists from './markedownSplitMixedList';
import markdownUnderline from './markdownUnderline';
import markdownBanner from './markdownBanner';
-export const markdown = markdownit('commonmark', { html: false, breaks: false })
+export const markdown = markdownit({
+ html: true,
+ linkify: true,
+ typographer: true,
+})
.enable('strikethrough')
.use(sub)
.use(sup)
diff --git a/packages/client/src/components/tiptap/views/floatMenuView.tsx b/packages/client/src/components/tiptap/views/floatMenuView.tsx
new file mode 100644
index 00000000..c294a77c
--- /dev/null
+++ b/packages/client/src/components/tiptap/views/floatMenuView.tsx
@@ -0,0 +1,150 @@
+import { Editor, isNodeSelection, posToDOMRect, Range } from '@tiptap/core';
+import tippy, { Instance, Props } from 'tippy.js';
+import { EditorView } from 'prosemirror-view';
+import { EditorState } from 'prosemirror-state';
+
+export type FloatMenuViewOptions = {
+ editor: Editor;
+ getReferenceClientRect?: (props: {
+ editor: Editor;
+ range: Range;
+ oldState?: EditorState;
+ }) => DOMRect;
+ shouldShow: (
+ props: { editor: Editor; range: Range; oldState?: EditorState },
+ instance: FloatMenuView
+ ) => boolean;
+ init: (dom: HTMLElement, editor: Editor) => void;
+ update?: (
+ dom: HTMLElement,
+ props: {
+ editor: Editor;
+ oldState?: EditorState;
+ range: Range;
+ show: () => void;
+ hide: () => void;
+ }
+ ) => void;
+ tippyOptions?: Partial;
+};
+
+export class FloatMenuView {
+ public editor: Editor;
+ public parentNode: null | HTMLElement;
+ private dom: HTMLElement;
+ private popup: Instance;
+ private _update: FloatMenuViewOptions['update'];
+ private shouldShow: FloatMenuViewOptions['shouldShow'];
+ private getReferenceClientRect: NonNullable = ({
+ editor,
+ range,
+ }) => {
+ const { view, state } = editor;
+ if (isNodeSelection(state.selection)) {
+ const node = view.nodeDOM(range.from) as HTMLElement;
+
+ if (node) {
+ return node.getBoundingClientRect();
+ }
+ }
+ return posToDOMRect(view, range.from, range.to);
+ };
+
+ constructor(props: FloatMenuViewOptions) {
+ this.editor = props.editor;
+ this.shouldShow = props.shouldShow;
+ if (props.getReferenceClientRect) {
+ this.getReferenceClientRect = props.getReferenceClientRect;
+ }
+ this._update = props.update;
+ this.dom = document.createElement('div');
+
+ // init
+ props.init(this.dom, this.editor);
+
+ // popup
+ this.popup = tippy(document.body, {
+ appendTo: () => document.body,
+ getReferenceClientRect: null,
+ content: this.dom,
+ interactive: true,
+ trigger: 'manual',
+ placement: 'top',
+ hideOnClick: 'toggle',
+ ...(props.tippyOptions ?? {}),
+ });
+ }
+
+ public update(view: EditorView, oldState?: EditorState) {
+ const { state, composing } = view;
+ const { doc, selection } = state;
+ const isSame = oldState && oldState.doc.eq(doc) && oldState.selection.eq(selection);
+
+ if (composing || isSame) {
+ return;
+ }
+
+ const { ranges } = selection;
+ const from = Math.min(...ranges.map((range) => range.$from.pos));
+ const to = Math.max(...ranges.map((range) => range.$to.pos));
+
+ const shouldShow = this.shouldShow?.(
+ {
+ editor: this.editor,
+ oldState,
+ range: {
+ from,
+ to,
+ },
+ },
+ this
+ );
+
+ if (!shouldShow) {
+ this.hide();
+ return;
+ }
+
+ this._update?.(this.dom, {
+ editor: this.editor,
+ oldState,
+ range: {
+ from,
+ to,
+ },
+ show: this.show.bind(this),
+ hide: this.hide.bind(this),
+ });
+
+ this.popup.setProps({
+ getReferenceClientRect: () => {
+ if (this.parentNode) {
+ return this.parentNode.getBoundingClientRect();
+ }
+
+ return this.getReferenceClientRect({
+ editor: this.editor,
+ oldState,
+ range: {
+ from,
+ to,
+ },
+ });
+ },
+ });
+
+ this.show();
+ }
+
+ show() {
+ this.popup.show();
+ }
+
+ hide() {
+ this.popup.hide();
+ }
+
+ public destroy() {
+ this.popup.destroy();
+ }
+}
diff --git a/packages/client/src/components/tiptap/views/tableView.tsx b/packages/client/src/components/tiptap/views/tableView.tsx
new file mode 100644
index 00000000..85638037
--- /dev/null
+++ b/packages/client/src/components/tiptap/views/tableView.tsx
@@ -0,0 +1,103 @@
+// @ts-nocheck
+import { NodeView } from 'prosemirror-view';
+import { Node as ProseMirrorNode } from 'prosemirror-model';
+
+export function updateColumns(
+ node: ProseMirrorNode,
+ colgroup: Element,
+ table: Element,
+ cellMinWidth: number,
+ overrideCol?: number,
+ overrideValue?: any
+) {
+ let totalWidth = 0;
+ let fixedWidth = true;
+ let nextDOM = colgroup.firstChild;
+ const row = node.firstChild;
+
+ for (let i = 0, col = 0; i < row.childCount; i += 1) {
+ const { colspan, colwidth } = row.child(i).attrs;
+
+ for (let j = 0; j < colspan; j += 1, col += 1) {
+ const hasWidth = overrideCol === col ? overrideValue : colwidth && colwidth[j];
+ const cssWidth = hasWidth ? `${hasWidth}px` : '';
+
+ totalWidth += hasWidth || cellMinWidth;
+
+ if (!hasWidth) {
+ fixedWidth = false;
+ }
+
+ if (!nextDOM) {
+ colgroup.appendChild(document.createElement('col')).style.width = cssWidth;
+ } else {
+ if (nextDOM.style.width !== cssWidth) {
+ nextDOM.style.width = cssWidth;
+ }
+
+ nextDOM = nextDOM.nextSibling;
+ }
+ }
+ }
+
+ while (nextDOM) {
+ const after = nextDOM.nextSibling;
+
+ nextDOM.parentNode.removeChild(nextDOM);
+ nextDOM = after;
+ }
+
+ if (fixedWidth) {
+ table.style.width = `${totalWidth}px`;
+ table.style.minWidth = '';
+ } else {
+ table.style.width = '';
+ table.style.minWidth = `${totalWidth}px`;
+ }
+}
+
+export class TableView implements NodeView {
+ node: ProseMirrorNode;
+
+ cellMinWidth: number;
+
+ dom: Element;
+
+ table: Element;
+
+ colgroup: Element;
+
+ contentDOM: Element;
+
+ constructor(node: ProseMirrorNode, cellMinWidth: number) {
+ this.node = node;
+ this.cellMinWidth = cellMinWidth;
+ this.dom = document.createElement('div');
+ this.dom.className = 'tableWrapper';
+ this.innerDom = document.createElement('div');
+ this.innerDom.className = 'tableInnerWrapper';
+ this.dom.appendChild(this.innerDom);
+ this.table = this.innerDom.appendChild(document.createElement('table'));
+ this.colgroup = this.table.appendChild(document.createElement('colgroup'));
+ updateColumns(node, this.colgroup, this.table, cellMinWidth);
+ this.contentDOM = this.table.appendChild(document.createElement('tbody'));
+ }
+
+ update(node: ProseMirrorNode) {
+ if (node.type !== this.node.type) {
+ return false;
+ }
+
+ this.node = node;
+ updateColumns(node, this.colgroup, this.table, this.cellMinWidth);
+
+ return true;
+ }
+
+ ignoreMutation(mutation: MutationRecord | { type: 'selection'; target: Element }) {
+ return (
+ mutation.type === 'attributes' &&
+ (mutation.target === this.table || this.colgroup.contains(mutation.target))
+ );
+ }
+}
diff --git a/packages/client/src/components/tooltip/index.tsx b/packages/client/src/components/tooltip/index.tsx
index 9df92e6b..877dc079 100644
--- a/packages/client/src/components/tooltip/index.tsx
+++ b/packages/client/src/components/tooltip/index.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import { Tooltip as SemiTooltip } from '@douyinfe/semi-ui';
+import { Position } from '@douyinfe/semi-ui/tooltip';
import { useToggle } from 'hooks/useToggle';
let id = 0;
@@ -7,13 +8,25 @@ let id = 0;
interface IProps {
content: React.ReactNode;
hideOnClick?: boolean;
+ position?: Position;
}
-export const Tooltip: React.FC = ({ content, hideOnClick = false, children }) => {
+export const Tooltip: React.FC = ({
+ content,
+ hideOnClick = false,
+ position = 'top',
+ children,
+}) => {
const [visible, toggleVisible] = useToggle(false);
return (
-
+
{
toggleVisible(true);
diff --git a/packages/client/src/styles/extension.scss b/packages/client/src/styles/extension.scss
index 08655565..9ba4fc22 100644
--- a/packages/client/src/styles/extension.scss
+++ b/packages/client/src/styles/extension.scss
@@ -6,6 +6,26 @@
box-shadow: var(--box-shadow);
background-color: var(--semi-color-nav-bg);
overflow-x: auto;
+
+ &.table-bubble-menu {
+ transform: translateY(-1em);
+ }
+}
+
+.table-controller-wrapper {
+ display: flex;
+ padding: 4px;
+ border-radius: 3px;
+ box-shadow: var(--box-shadow);
+ background-color: var(--semi-color-nav-bg);
+ transform: translateY(-1em);
+
+ &.row {
+ column-gap: 8px;
+ flex-direction: column;
+ transform: translate(-100%, 75%);
+ margin-left: -1.2em;
+ }
}
.resizeable-image-container {
@@ -49,7 +69,7 @@
background-color: var(--color);
border: 1px solid rgba(128, 128, 128, 0.3);
border-radius: 2px;
- content: " ";
+ content: ' ';
display: inline-block;
height: 1em;
margin-bottom: 0.15em;
diff --git a/packages/client/src/styles/prosemirror.scss b/packages/client/src/styles/prosemirror.scss
index 6e0d58c7..6594f069 100644
--- a/packages/client/src/styles/prosemirror.scss
+++ b/packages/client/src/styles/prosemirror.scss
@@ -110,7 +110,6 @@
border-bottom: 1px solid var(--semi-color-border);
}
- h1,
h2,
h3,
h4,
@@ -131,6 +130,16 @@
h1:not(.title):before {
content: 'H1';
+
+ &::before {
+ display: inline-block;
+ font-family: var(--tiptap-font-family-mono);
+ color: var(--tiptap-color-text-secondly);
+ font-size: 13px;
+ line-height: 0;
+ margin-left: -24px;
+ width: 24px;
+ }
}
h2::before {
@@ -284,16 +293,13 @@
.tableWrapper {
width: 100%;
- overflow: auto;
- padding: 1em 0 0 1em;
+ margin: 1em 0;
}
table {
border-collapse: collapse;
table-layout: fixed;
width: 100%;
- margin: 14px 0;
- overflow: visible;
td,
th {
@@ -305,6 +311,7 @@
padding: 3px 5px;
position: relative;
vertical-align: top;
+ overflow: visible;
> * {
margin-bottom: 0;