Skip to content

Commit f21f6f5

Browse files
committed
feat: improvements for tabs removement
1 parent f227db1 commit f21f6f5

File tree

5 files changed

+184
-12
lines changed

5 files changed

+184
-12
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
1+
import {nodeTypeFactory} from '../../../utils/schema';
2+
13
export enum TabsNode {
24
Tab = 'yfm_tab',
35
TabsList = 'yfm_tabs_list',
46
TabPanel = 'yfm_tab_panel',
57
Tabs = 'yfm_tabs',
68
}
9+
10+
export const tabActiveClassname = 'yfm-tab active';
11+
export const tabInactiveClassname = 'yfm-tab';
12+
export const tabPanelActiveClassname = 'yfm-tab-panel active';
13+
export const tabPanelInactiveClassname = 'yfm-tab-panel';
14+
15+
export const tabPanelType = nodeTypeFactory(TabsNode.TabPanel);
16+
export const tabType = nodeTypeFactory(TabsNode.Tab);
17+
export const tabsType = nodeTypeFactory(TabsNode.Tabs);
18+
export const tabListType = nodeTypeFactory(TabsNode.TabsList);

src/extensions/yfm/YfmTabs/index.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,31 @@ import {TabsNode} from './const';
66

77
import {fromYfm} from './fromYfm';
88
import {spec} from './spec';
9+
import {Node} from 'prosemirror-model';
10+
import {EditorView} from 'prosemirror-view';
11+
import {tabBackspace, tabPanelBackspace} from './plugins';
12+
import {chainCommands} from 'prosemirror-commands';
13+
14+
const ignoreMutation =
15+
(node: Node, view: EditorView, getPos: () => number) => (mutation: MutationRecord) => {
16+
if (
17+
mutation instanceof MutationRecord &&
18+
mutation.type === 'attributes' &&
19+
mutation.attributeName
20+
) {
21+
const newAttr = (mutation.target as HTMLElement).getAttribute(mutation.attributeName);
22+
23+
view.dispatch(
24+
view.state.tr.setNodeMarkup(getPos(), null, {
25+
...node.attrs,
26+
[mutation.attributeName]: String(newAttr),
27+
}),
28+
);
29+
return true;
30+
}
31+
32+
return false;
33+
};
934

1035
export const YfmTabs: ExtensionAuto = (builder) => {
1136
builder
@@ -19,10 +44,8 @@ export const YfmTabs: ExtensionAuto = (builder) => {
1944
},
2045
// FIX: ignore mutation and don't rerender node when yfm.js switch tab
2146
// @ts-expect-error
22-
view: () => () => ({
23-
ignoreMutation(mutation) {
24-
return mutation instanceof MutationRecord && mutation.type === 'attributes';
25-
},
47+
view: () => (node, view, getPos) => ({
48+
ignoreMutation: ignoreMutation(node, view, getPos),
2649
}),
2750
}))
2851
.addNode(TabsNode.TabsList, () => ({
@@ -42,10 +65,8 @@ export const YfmTabs: ExtensionAuto = (builder) => {
4265
},
4366
// FIX: ignore mutation and don't rerender node when yfm.js switch tab
4467
// @ts-expect-error
45-
view: () => () => ({
46-
ignoreMutation(mutation) {
47-
return mutation instanceof MutationRecord && mutation.type === 'attributes';
48-
},
68+
view: () => (node, view, getPos) => ({
69+
ignoreMutation: ignoreMutation(node, view, getPos),
4970
}),
5071
}))
5172
.addNode(TabsNode.Tabs, () => ({
@@ -55,5 +76,8 @@ export const YfmTabs: ExtensionAuto = (builder) => {
5576
tokenSpec: fromYfm[TabsNode.Tabs],
5677
tokenName: 'tabs',
5778
},
79+
}))
80+
.addKeymap(() => ({
81+
Backspace: chainCommands(tabPanelBackspace, tabBackspace),
5882
}));
5983
};
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import {Command, TextSelection} from 'prosemirror-state';
2+
import {findChildren, findParentNodeOfType} from 'prosemirror-utils';
3+
import {
4+
tabActiveClassname,
5+
tabInactiveClassname,
6+
tabListType,
7+
tabPanelActiveClassname,
8+
tabPanelInactiveClassname,
9+
tabPanelType,
10+
tabsType,
11+
tabType,
12+
} from './const';
13+
import {findChildIndex} from '../../../table-utils/helpers';
14+
import {get$Cursor} from '../../../utils/selection';
15+
16+
export const tabPanelBackspace: Command = (state) => {
17+
const $cursor = get$Cursor(state.selection);
18+
if (
19+
$cursor?.node($cursor.depth - 1).type === tabPanelType(state.schema) &&
20+
$cursor.start($cursor.depth - 1) === $cursor.pos - 1
21+
) {
22+
return true;
23+
}
24+
return false;
25+
};
26+
27+
export const tabBackspace: Command = (state, dispatch) => {
28+
const tabToRemove = findParentNodeOfType(tabType(state.schema))(state.selection);
29+
const tabsParentNode = findParentNodeOfType(tabsType(state.schema))(state.selection);
30+
31+
if (
32+
tabsParentNode &&
33+
tabToRemove &&
34+
state.selection.from === tabToRemove.pos + 1 &&
35+
state.selection.from === state.selection.to
36+
) {
37+
const tabList = findChildren(tabsParentNode.node, (tabNode) => {
38+
return tabNode.type.name === tabListType(state.schema).name;
39+
})[0];
40+
const tabToRemoveIdx = findChildIndex(tabList.node, tabToRemove.node);
41+
42+
const tabNodes = findChildren(
43+
tabList.node,
44+
(node) => node.type.name === tabType(state.schema).name,
45+
);
46+
47+
const tabPanels = findChildren(tabsParentNode.node, (tabNode) => {
48+
return tabNode.type.name === tabPanelType(state.schema).name;
49+
});
50+
51+
const panelToRemove = tabPanels.filter(
52+
(tabNode) => tabNode.node.attrs['aria-labelledby'] === tabToRemove.node.attrs['id'],
53+
)[0];
54+
55+
if (panelToRemove && dispatch) {
56+
// Change relative pos to absolute
57+
panelToRemove.pos = panelToRemove.pos + tabsParentNode.pos;
58+
const {tr} = state;
59+
60+
if (tabNodes.length <= 1) {
61+
tr.delete(tabsParentNode.pos, tabsParentNode.pos + tabsParentNode.node.nodeSize);
62+
} else {
63+
const newTabIdx = tabToRemoveIdx - 1 < 0 ? 1 : tabToRemoveIdx - 1;
64+
65+
// Change relative pos to absolute
66+
tabNodes.forEach((v) => {
67+
v.pos = v.pos + tabsParentNode.pos + 2;
68+
});
69+
70+
const newTabNode = tabNodes[newTabIdx];
71+
72+
const newTabPanelNode = tabPanels[newTabIdx];
73+
// Change relative pos to absolute
74+
newTabPanelNode.pos = newTabPanelNode.pos + tabsParentNode.pos + 1;
75+
76+
// Find all active tabs and make them inactive
77+
const activeTabs = tabNodes.filter(
78+
(v) => v.node.attrs['class'] === tabActiveClassname,
79+
);
80+
81+
if (activeTabs.length) {
82+
activeTabs.forEach((tab) => {
83+
tr.setNodeMarkup(tab.pos, null, {
84+
...tab.node.attrs,
85+
class: tabInactiveClassname,
86+
});
87+
});
88+
}
89+
90+
// Find all active panels and make them inactive
91+
const activePanels = tabPanels.filter(
92+
(v) => v.node.attrs['class'] === tabPanelActiveClassname,
93+
);
94+
if (activePanels.length) {
95+
activePanels.forEach((tabPanel) => {
96+
tr.setNodeMarkup(
97+
tr.mapping.map(tabPanel.pos + tabsParentNode.pos + 1),
98+
null,
99+
{
100+
...tabPanel.node.attrs,
101+
class: tabPanelInactiveClassname,
102+
},
103+
);
104+
});
105+
}
106+
107+
tr
108+
// Delete panel
109+
.delete(panelToRemove.pos, panelToRemove.pos + panelToRemove.node.nodeSize)
110+
// Delete tab
111+
.delete(tabToRemove.pos, tabToRemove.pos + tabToRemove.node.nodeSize)
112+
// Set new active tab
113+
.setNodeMarkup(tr.mapping.map(newTabNode.pos), null, {
114+
...newTabNode.node.attrs,
115+
class: tabActiveClassname,
116+
})
117+
// Set new active panel
118+
.setNodeMarkup(tr.mapping.map(newTabPanelNode.pos), null, {
119+
...newTabPanelNode.node.attrs,
120+
class: tabPanelActiveClassname,
121+
})
122+
.setSelection(
123+
TextSelection.create(
124+
tr.doc,
125+
tr.mapping.map(newTabNode.pos + newTabNode.node.nodeSize - 1),
126+
),
127+
);
128+
}
129+
dispatch(tr);
130+
131+
return true;
132+
}
133+
}
134+
135+
return false;
136+
};

src/extensions/yfm/YfmTabs/spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const spec: Record<TabsNode, NodeSpec> = {
1212
tabindex: {default: 'unknown'},
1313
},
1414
marks: '',
15-
content: 'inline*',
15+
content: 'text*',
1616
group: 'block',
1717
parseDOM: [{tag: 'div.yfm-tab'}],
1818
toDOM(node) {
@@ -45,7 +45,7 @@ export const spec: Record<TabsNode, NodeSpec> = {
4545
[TabsNode.Tabs]: {
4646
allowGapCursor: true,
4747
attrs: {class: {default: 'unknown'}},
48-
content: 'yfm_tabs_list* yfm_tab_panel*',
48+
content: 'yfm_tabs_list yfm_tab_panel+',
4949
group: 'block',
5050
parseDOM: [{tag: 'div.yfm-tabs'}],
5151
toDOM(node) {

src/extensions/yfm/YfmTabs/toYfm.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ export const toYfm: Record<TabsNode, SerializerNodeToken> = {
2222

2323
tabList.forEach((tab, _, i) => {
2424
state.write('- ' + tab.textContent + '\n\n');
25-
state.renderList(children[i + 1], ' ', () => ' ');
25+
if (children[i + 1]) state.renderList(children[i + 1], ' ', () => ' ');
2626
});
2727

28-
state.write('{% endlist %}');
28+
state.write('{% endlist %}\n\n');
2929
},
3030
[TabsNode.TabsList]: (state, node) => {
3131
state.renderList(node, ' ', () => (node.attrs.bullet || '-') + ' ');

0 commit comments

Comments
 (0)