mirror of https://github.com/fantasticit/think.git
feat: improve tiptap
parent
1835f8504b
commit
ea34e23422
|
@ -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 = <Tag color={color}>{text || '点击设置状态'}</Tag>;
|
||||
|
||||
console.log(node.attrs);
|
||||
|
||||
return (
|
||||
<NodeViewWrapper as="div">
|
||||
<table>
|
||||
<NodeViewContent></NodeViewContent>
|
||||
</table>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
|
@ -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(
|
||||
<>
|
||||
<Tooltip content="向前插入一行" position="left">
|
||||
<Button
|
||||
size="small"
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
icon={<IconPlus />}
|
||||
onClick={() => {
|
||||
editor.chain().addRowBefore().run();
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip content="删除当前行" position="left">
|
||||
<Button
|
||||
size="small"
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
icon={<IconDelete />}
|
||||
onClick={() => {
|
||||
editor.chain().deleteRow().run();
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip content="向后插入一行" position="left" hideOnClick>
|
||||
<Button
|
||||
size="small"
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
icon={<IconPlus />}
|
||||
onClick={() => {
|
||||
editor.chain().addRowAfter().run();
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</>,
|
||||
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);
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
|
@ -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);
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
|
@ -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(
|
||||
<Space>
|
||||
<Tooltip content="向前插入一列">
|
||||
<Button
|
||||
size="small"
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
icon={<IconPlus />}
|
||||
onClick={() => {
|
||||
editor.chain().addColumnBefore().run();
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip content="向后插入一列">
|
||||
<Button
|
||||
size="small"
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
icon={<IconDelete />}
|
||||
onClick={() => {
|
||||
editor.chain().deleteColumn().run();
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip content="删除当前列" hideOnClick>
|
||||
<Button
|
||||
size="small"
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
icon={<IconPlus />}
|
||||
onClick={() => {
|
||||
editor.chain().addColumnAfter().run();
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space>,
|
||||
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);
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { BubbleMenuPlugin, BubbleMenuPluginProps } from './bubble-menu-plugin';
|
||||
import { BubbleMenuPlugin, BubbleMenuPluginProps } from './bubbleMenuPlugin';
|
||||
|
||||
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
|
||||
|
|
@ -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';
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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 (
|
||||
<BubbleMenu
|
||||
className={'bubble-menu'}
|
||||
className={'bubble-menu table-bubble-menu'}
|
||||
editor={editor}
|
||||
pluginKey="table-bubble-menu"
|
||||
shouldShow={() => editor.isActive(Table.name)}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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<Props>;
|
||||
};
|
||||
|
||||
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<FloatMenuViewOptions['getReferenceClientRect']> = ({
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<IProps> = ({ content, hideOnClick = false, children }) => {
|
||||
export const Tooltip: React.FC<IProps> = ({
|
||||
content,
|
||||
hideOnClick = false,
|
||||
position = 'top',
|
||||
children,
|
||||
}) => {
|
||||
const [visible, toggleVisible] = useToggle(false);
|
||||
|
||||
return (
|
||||
<SemiTooltip visible={visible} content={content} zIndex={10000} trigger={'custom'}>
|
||||
<SemiTooltip
|
||||
visible={visible}
|
||||
content={content}
|
||||
zIndex={10000}
|
||||
trigger={'custom'}
|
||||
position={position}
|
||||
>
|
||||
<span
|
||||
onMouseEnter={() => {
|
||||
toggleVisible(true);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue