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