Skip to content

Commit

Permalink
feat: improvements for tabs removement
Browse files Browse the repository at this point in the history
  • Loading branch information
smsochneg committed Jan 24, 2023
1 parent f227db1 commit f21f6f5
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 12 deletions.
12 changes: 12 additions & 0 deletions src/extensions/yfm/YfmTabs/const.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import {nodeTypeFactory} from '../../../utils/schema';

export enum TabsNode {
Tab = 'yfm_tab',
TabsList = 'yfm_tabs_list',
TabPanel = 'yfm_tab_panel',
Tabs = 'yfm_tabs',
}

export const tabActiveClassname = 'yfm-tab active';
export const tabInactiveClassname = 'yfm-tab';
export const tabPanelActiveClassname = 'yfm-tab-panel active';
export const tabPanelInactiveClassname = 'yfm-tab-panel';

export const tabPanelType = nodeTypeFactory(TabsNode.TabPanel);
export const tabType = nodeTypeFactory(TabsNode.Tab);
export const tabsType = nodeTypeFactory(TabsNode.Tabs);
export const tabListType = nodeTypeFactory(TabsNode.TabsList);
40 changes: 32 additions & 8 deletions src/extensions/yfm/YfmTabs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,31 @@ import {TabsNode} from './const';

import {fromYfm} from './fromYfm';
import {spec} from './spec';
import {Node} from 'prosemirror-model';
import {EditorView} from 'prosemirror-view';
import {tabBackspace, tabPanelBackspace} from './plugins';
import {chainCommands} from 'prosemirror-commands';

const ignoreMutation =
(node: Node, view: EditorView, getPos: () => number) => (mutation: MutationRecord) => {
if (
mutation instanceof MutationRecord &&
mutation.type === 'attributes' &&
mutation.attributeName
) {
const newAttr = (mutation.target as HTMLElement).getAttribute(mutation.attributeName);

view.dispatch(
view.state.tr.setNodeMarkup(getPos(), null, {
...node.attrs,
[mutation.attributeName]: String(newAttr),
}),
);
return true;
}

return false;
};

export const YfmTabs: ExtensionAuto = (builder) => {
builder
Expand All @@ -19,10 +44,8 @@ export const YfmTabs: ExtensionAuto = (builder) => {
},
// FIX: ignore mutation and don't rerender node when yfm.js switch tab
// @ts-expect-error
view: () => () => ({
ignoreMutation(mutation) {
return mutation instanceof MutationRecord && mutation.type === 'attributes';
},
view: () => (node, view, getPos) => ({
ignoreMutation: ignoreMutation(node, view, getPos),
}),
}))
.addNode(TabsNode.TabsList, () => ({
Expand All @@ -42,10 +65,8 @@ export const YfmTabs: ExtensionAuto = (builder) => {
},
// FIX: ignore mutation and don't rerender node when yfm.js switch tab
// @ts-expect-error
view: () => () => ({
ignoreMutation(mutation) {
return mutation instanceof MutationRecord && mutation.type === 'attributes';
},
view: () => (node, view, getPos) => ({
ignoreMutation: ignoreMutation(node, view, getPos),
}),
}))
.addNode(TabsNode.Tabs, () => ({
Expand All @@ -55,5 +76,8 @@ export const YfmTabs: ExtensionAuto = (builder) => {
tokenSpec: fromYfm[TabsNode.Tabs],
tokenName: 'tabs',
},
}))
.addKeymap(() => ({
Backspace: chainCommands(tabPanelBackspace, tabBackspace),
}));
};
136 changes: 136 additions & 0 deletions src/extensions/yfm/YfmTabs/plugins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import {Command, TextSelection} from 'prosemirror-state';
import {findChildren, findParentNodeOfType} from 'prosemirror-utils';
import {
tabActiveClassname,
tabInactiveClassname,
tabListType,
tabPanelActiveClassname,
tabPanelInactiveClassname,
tabPanelType,
tabsType,
tabType,
} from './const';
import {findChildIndex} from '../../../table-utils/helpers';
import {get$Cursor} from '../../../utils/selection';

export const tabPanelBackspace: Command = (state) => {
const $cursor = get$Cursor(state.selection);
if (
$cursor?.node($cursor.depth - 1).type === tabPanelType(state.schema) &&
$cursor.start($cursor.depth - 1) === $cursor.pos - 1
) {
return true;
}
return false;
};

export const tabBackspace: Command = (state, dispatch) => {
const tabToRemove = findParentNodeOfType(tabType(state.schema))(state.selection);
const tabsParentNode = findParentNodeOfType(tabsType(state.schema))(state.selection);

if (
tabsParentNode &&
tabToRemove &&
state.selection.from === tabToRemove.pos + 1 &&
state.selection.from === state.selection.to
) {
const tabList = findChildren(tabsParentNode.node, (tabNode) => {
return tabNode.type.name === tabListType(state.schema).name;
})[0];
const tabToRemoveIdx = findChildIndex(tabList.node, tabToRemove.node);

const tabNodes = findChildren(
tabList.node,
(node) => node.type.name === tabType(state.schema).name,
);

const tabPanels = findChildren(tabsParentNode.node, (tabNode) => {
return tabNode.type.name === tabPanelType(state.schema).name;
});

const panelToRemove = tabPanels.filter(
(tabNode) => tabNode.node.attrs['aria-labelledby'] === tabToRemove.node.attrs['id'],
)[0];

if (panelToRemove && dispatch) {
// Change relative pos to absolute
panelToRemove.pos = panelToRemove.pos + tabsParentNode.pos;
const {tr} = state;

if (tabNodes.length <= 1) {
tr.delete(tabsParentNode.pos, tabsParentNode.pos + tabsParentNode.node.nodeSize);
} else {
const newTabIdx = tabToRemoveIdx - 1 < 0 ? 1 : tabToRemoveIdx - 1;

// Change relative pos to absolute
tabNodes.forEach((v) => {
v.pos = v.pos + tabsParentNode.pos + 2;
});

const newTabNode = tabNodes[newTabIdx];

const newTabPanelNode = tabPanels[newTabIdx];
// Change relative pos to absolute
newTabPanelNode.pos = newTabPanelNode.pos + tabsParentNode.pos + 1;

// Find all active tabs and make them inactive
const activeTabs = tabNodes.filter(
(v) => v.node.attrs['class'] === tabActiveClassname,
);

if (activeTabs.length) {
activeTabs.forEach((tab) => {
tr.setNodeMarkup(tab.pos, null, {
...tab.node.attrs,
class: tabInactiveClassname,
});
});
}

// Find all active panels and make them inactive
const activePanels = tabPanels.filter(
(v) => v.node.attrs['class'] === tabPanelActiveClassname,
);
if (activePanels.length) {
activePanels.forEach((tabPanel) => {
tr.setNodeMarkup(
tr.mapping.map(tabPanel.pos + tabsParentNode.pos + 1),
null,
{
...tabPanel.node.attrs,
class: tabPanelInactiveClassname,
},
);
});
}

tr
// Delete panel
.delete(panelToRemove.pos, panelToRemove.pos + panelToRemove.node.nodeSize)
// Delete tab
.delete(tabToRemove.pos, tabToRemove.pos + tabToRemove.node.nodeSize)
// Set new active tab
.setNodeMarkup(tr.mapping.map(newTabNode.pos), null, {
...newTabNode.node.attrs,
class: tabActiveClassname,
})
// Set new active panel
.setNodeMarkup(tr.mapping.map(newTabPanelNode.pos), null, {
...newTabPanelNode.node.attrs,
class: tabPanelActiveClassname,
})
.setSelection(
TextSelection.create(
tr.doc,
tr.mapping.map(newTabNode.pos + newTabNode.node.nodeSize - 1),
),
);
}
dispatch(tr);

return true;
}
}

return false;
};
4 changes: 2 additions & 2 deletions src/extensions/yfm/YfmTabs/spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const spec: Record<TabsNode, NodeSpec> = {
tabindex: {default: 'unknown'},
},
marks: '',
content: 'inline*',
content: 'text*',
group: 'block',
parseDOM: [{tag: 'div.yfm-tab'}],
toDOM(node) {
Expand Down Expand Up @@ -45,7 +45,7 @@ export const spec: Record<TabsNode, NodeSpec> = {
[TabsNode.Tabs]: {
allowGapCursor: true,
attrs: {class: {default: 'unknown'}},
content: 'yfm_tabs_list* yfm_tab_panel*',
content: 'yfm_tabs_list yfm_tab_panel+',
group: 'block',
parseDOM: [{tag: 'div.yfm-tabs'}],
toDOM(node) {
Expand Down
4 changes: 2 additions & 2 deletions src/extensions/yfm/YfmTabs/toYfm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ export const toYfm: Record<TabsNode, SerializerNodeToken> = {

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

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

0 comments on commit f21f6f5

Please sign in to comment.