mirror of
https://github.com/docmost/docmost.git
synced 2026-08-19 18:41:36 +10:00
tabs menu
This commit is contained in:
@@ -61,6 +61,22 @@ const TabsMenu = React.memo(({ editor }: EditorMenuProps) => {
|
||||
editor.chain().focus().insertTab("right").run();
|
||||
}, [editor]);
|
||||
|
||||
const handleMoveTabRight = useCallback(() => {
|
||||
editor.chain().focus().moveTab("right").run();
|
||||
}, [editor]);
|
||||
|
||||
const handleMoveTabLeft = useCallback(() => {
|
||||
editor.chain().focus().moveTab("left").run();
|
||||
}, [editor]);
|
||||
|
||||
const handleDeleteTab = useCallback(() => {
|
||||
editor.chain().focus().deleteTab().run();
|
||||
}, [editor]);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
editor.chain().focus().delete().run();
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<BubbleMenu
|
||||
style={{ zIndex: 99 }}
|
||||
@@ -98,7 +114,7 @@ const TabsMenu = React.memo(({ editor }: EditorMenuProps) => {
|
||||
</Tooltip>
|
||||
<Tooltip position="top" label={t("Delete tab")} withinPortal={false}>
|
||||
<ActionIcon
|
||||
onClick={handleAddTabLeft}
|
||||
onClick={handleDeleteTab}
|
||||
variant="subtle"
|
||||
size="lg"
|
||||
aria-label={t("Delete tab")}
|
||||
@@ -111,7 +127,7 @@ const TabsMenu = React.memo(({ editor }: EditorMenuProps) => {
|
||||
|
||||
<Tooltip position="top" label={t("Move tab left")} withinPortal={false}>
|
||||
<ActionIcon
|
||||
onClick={handleAddTabLeft}
|
||||
onClick={handleMoveTabLeft}
|
||||
variant="subtle"
|
||||
size="lg"
|
||||
aria-label={t("Move tab left")}
|
||||
@@ -122,7 +138,7 @@ const TabsMenu = React.memo(({ editor }: EditorMenuProps) => {
|
||||
|
||||
<Tooltip position="top" label={t("Move tab right")} withinPortal={false}>
|
||||
<ActionIcon
|
||||
onClick={handleAddTabLeft}
|
||||
onClick={handleMoveTabRight}
|
||||
variant="subtle"
|
||||
size="lg"
|
||||
aria-label={t("Move tab right")}
|
||||
@@ -130,12 +146,11 @@ const TabsMenu = React.memo(({ editor }: EditorMenuProps) => {
|
||||
<IconChevronRight size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<div className={classes.divider} />
|
||||
|
||||
<Tooltip position="top" label={t("Delete")} withinPortal={false}>
|
||||
<ActionIcon
|
||||
onClick={handleAddTabLeft}
|
||||
onClick={handleDelete}
|
||||
variant="subtle"
|
||||
size="lg"
|
||||
aria-label={t("Delete")}
|
||||
|
||||
@@ -14,7 +14,8 @@ declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
tabs: {
|
||||
insertTabs: () => ReturnType;
|
||||
insertTab: (pos: string) => ReturnType;
|
||||
insertTab: (pos: 'right' | 'left') => ReturnType;
|
||||
moveTab: (pos: 'right' | 'left') => ReturnType;
|
||||
setActiveTab: (index: number, tabsPos: number) => ReturnType;
|
||||
updateTabLabel: (
|
||||
index: number,
|
||||
@@ -104,11 +105,6 @@ export const Tabs = Node.create<TabsOptions>({
|
||||
return null;
|
||||
};
|
||||
|
||||
const resolveTarget = (state: EditorState, pos: number) => {
|
||||
const node = state.doc.nodeAt(pos);
|
||||
return { node, pos: pos };
|
||||
};
|
||||
|
||||
const getTabPos = (doc: PMNode, tabsPos: number, tabIndex: number) => {
|
||||
const pos = doc.resolve(tabsPos + 1);
|
||||
return pos.posAtIndex(tabIndex, pos.depth);
|
||||
@@ -157,6 +153,23 @@ export const Tabs = Node.create<TabsOptions>({
|
||||
return nextTabPos;
|
||||
};
|
||||
|
||||
const selectTabPanel = (
|
||||
tr: Transaction,
|
||||
tabsPos: number,
|
||||
tabPos: number,
|
||||
) => {
|
||||
const tabsNode = tr.doc.nodeAt(tabsPos);
|
||||
if (tabsNode?.type.name !== 'tabs' || tabsNode.childCount <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tabNode = tr.doc.nodeAt(tabPos);
|
||||
const labelSize = tabNode?.child(0).nodeSize ?? 0;
|
||||
const panelContentPos = tabPos + 1 + labelSize + 1;
|
||||
|
||||
tr.setSelection(TextSelection.near(tr.doc.resolve(panelContentPos), 1));
|
||||
};
|
||||
|
||||
return {
|
||||
insertTabs:
|
||||
() =>
|
||||
@@ -191,7 +204,7 @@ export const Tabs = Node.create<TabsOptions>({
|
||||
},
|
||||
|
||||
insertTab:
|
||||
(pos: string) =>
|
||||
(pos: 'left' | 'right') =>
|
||||
({ state, tr, dispatch }) => {
|
||||
const tabs = resolveTabs(state);
|
||||
if (!tabs || tabs.node.childCount <= 0) return false;
|
||||
@@ -227,46 +240,74 @@ export const Tabs = Node.create<TabsOptions>({
|
||||
);
|
||||
if (activeTabPos == null) return false;
|
||||
|
||||
const tabNode = tr.doc.nodeAt(activeTabPos);
|
||||
const labelSize = tabNode?.child(0).nodeSize ?? 0;
|
||||
const panelContentPos = activeTabPos + 1 + labelSize + 1;
|
||||
selectTabPanel(tr, tabs.pos, insertedTabPos);
|
||||
if (dispatch) dispatch(tr);
|
||||
return true;
|
||||
},
|
||||
|
||||
tr.setSelection(
|
||||
TextSelection.near(tr.doc.resolve(panelContentPos), 1),
|
||||
moveTab:
|
||||
(pos: 'left' | 'right') =>
|
||||
({ state, tr, dispatch }) => {
|
||||
const tabs = resolveTabs(state);
|
||||
if (!tabs || tabs.node.childCount <= 1) return false;
|
||||
|
||||
const currentTabIndex = clampIndex(
|
||||
tabs.node.attrs.activeTab,
|
||||
tabs.node.childCount,
|
||||
);
|
||||
|
||||
if (dispatch) dispatch(tr);
|
||||
const targetIndex =
|
||||
pos === 'left' ? currentTabIndex - 1 : currentTabIndex + 1;
|
||||
|
||||
if (targetIndex < 0 || targetIndex >= tabs.node.childCount)
|
||||
return false;
|
||||
|
||||
const currentTabPos = getTabPos(state.doc, tabs.pos, currentTabIndex);
|
||||
const currentTabNode = state.doc.nodeAt(currentTabPos);
|
||||
if (!currentTabNode) return false;
|
||||
|
||||
const mappedCurrentTabPos = tr.mapping.map(currentTabPos);
|
||||
tr.delete(
|
||||
mappedCurrentTabPos,
|
||||
mappedCurrentTabPos + currentTabNode.nodeSize,
|
||||
);
|
||||
|
||||
const insertPos = getTabPos(tr.doc, tabs.pos, targetIndex);
|
||||
tr.insert(insertPos, currentTabNode);
|
||||
|
||||
const movedTabPos = applyActiveTabState(
|
||||
tr,
|
||||
tabs.pos,
|
||||
targetIndex,
|
||||
targetIndex,
|
||||
);
|
||||
selectTabPanel(tr, tabs.pos, movedTabPos);
|
||||
|
||||
if (dispatch) dispatch(tr.scrollIntoView());
|
||||
return true;
|
||||
},
|
||||
|
||||
setActiveTab:
|
||||
(index, tabsPos) =>
|
||||
({ state, tr, dispatch }) => {
|
||||
const target = resolveTarget(state, tabsPos);
|
||||
if (!target || target.node.childCount <= 0) return false;
|
||||
const tabsNode = state.doc.nodeAt(tabsPos);
|
||||
if (tabsNode?.childCount <= 0) return false;
|
||||
|
||||
const nextIndex = clampIndex(index, target.node.childCount);
|
||||
const nextIndex = clampIndex(index, tabsNode.childCount);
|
||||
const prevIndex = clampIndex(
|
||||
target.node.attrs.activeTab,
|
||||
target.node.childCount,
|
||||
tabsNode.attrs.activeTab,
|
||||
tabsNode.childCount,
|
||||
);
|
||||
|
||||
const activeTabPos = applyActiveTabState(
|
||||
tr,
|
||||
target.pos,
|
||||
tabsPos,
|
||||
prevIndex,
|
||||
nextIndex,
|
||||
);
|
||||
if (activeTabPos == null) return false;
|
||||
|
||||
const tabNode = tr.doc.nodeAt(activeTabPos);
|
||||
const labelSize = tabNode?.child(0).nodeSize ?? 0;
|
||||
const panelContentPos = activeTabPos + 1 + labelSize + 1;
|
||||
|
||||
tr.setSelection(
|
||||
TextSelection.near(tr.doc.resolve(panelContentPos), 1),
|
||||
);
|
||||
|
||||
selectTabPanel(tr, tabsPos, activeTabPos);
|
||||
if (dispatch) dispatch(tr.scrollIntoView());
|
||||
return true;
|
||||
},
|
||||
@@ -274,11 +315,11 @@ export const Tabs = Node.create<TabsOptions>({
|
||||
updateTabLabel:
|
||||
(index, label, tabsPos) =>
|
||||
({ state, tr, dispatch }) => {
|
||||
const target = resolveTarget(state, tabsPos);
|
||||
if (!target) return false;
|
||||
const tabsNode = state.doc.nodeAt(tabsPos);
|
||||
if (!tabsNode) return false;
|
||||
|
||||
const labelIndex = clampIndex(index, target.node.childCount);
|
||||
const $tabs = state.doc.resolve(target.pos + 1);
|
||||
const labelIndex = clampIndex(index, tabsNode.childCount);
|
||||
const $tabs = state.doc.resolve(tabsPos + 1);
|
||||
const tabPos = $tabs.posAtIndex(labelIndex, $tabs.depth);
|
||||
|
||||
const labelNode = state.doc.nodeAt(tabPos + 1);
|
||||
@@ -290,6 +331,54 @@ export const Tabs = Node.create<TabsOptions>({
|
||||
state.schema.text(label || ' '),
|
||||
);
|
||||
|
||||
if (dispatch) dispatch(tr);
|
||||
return true;
|
||||
},
|
||||
|
||||
deleteTab:
|
||||
() =>
|
||||
({ state, tr, dispatch }) => {
|
||||
const tabs = resolveTabs(state);
|
||||
if (!tabs || tabs.node.childCount <= 1) return false;
|
||||
|
||||
const currentTabIndex = clampIndex(
|
||||
tabs.node.attrs.activeTab,
|
||||
tabs.node.childCount,
|
||||
);
|
||||
|
||||
const currentTabPos = getTabPos(state.doc, tabs.pos, currentTabIndex);
|
||||
const currentTabNode = state.doc.nodeAt(currentTabPos);
|
||||
if (!currentTabNode) return false;
|
||||
|
||||
const nextTabIndex =
|
||||
currentTabIndex < tabs.node.childCount - 1
|
||||
? currentTabIndex
|
||||
: currentTabIndex - 1;
|
||||
|
||||
tr.delete(currentTabPos, currentTabPos + currentTabNode.nodeSize);
|
||||
|
||||
const activeTabPos = applyActiveTabState(
|
||||
tr,
|
||||
tabs.pos,
|
||||
nextTabIndex,
|
||||
nextTabIndex,
|
||||
);
|
||||
if (activeTabPos == null) return false;
|
||||
|
||||
selectTabPanel(tr, tabs.pos, activeTabPos);
|
||||
|
||||
if (dispatch) dispatch(tr);
|
||||
return true;
|
||||
},
|
||||
|
||||
delete:
|
||||
() =>
|
||||
({ state, tr, dispatch }) => {
|
||||
const tabs = resolveTabs(state);
|
||||
if (!tabs || tabs.node.childCount <= 1) return false;
|
||||
|
||||
tr.delete(tabs.pos, tabs.pos + tabs.node.nodeSize);
|
||||
|
||||
if (dispatch) dispatch(tr);
|
||||
return true;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user