Skip to content

Commit

Permalink
feat: support initial and hide
Browse files Browse the repository at this point in the history
  • Loading branch information
v8tenko committed Aug 20, 2024
1 parent f00e5c5 commit 3b59a67
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 34 deletions.
2 changes: 2 additions & 0 deletions src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const TAB_CLASSNAME = 'yfm-tab';
export const TAB_PANEL_CLASSNAME = 'yfm-tab-panel';
export const ACTIVE_CLASSNAME = 'active';
export const VERTICAL_TAB_CLASSNAME = 'yfm-vertical-tab';
export const VERTICAL_TAB_FORCED_OPEN = 'data-diplodoc-radio-forced';

export const GROUP_DATA_KEY = 'data-diplodoc-group';
export const TAB_DATA_KEY = 'data-diplodoc-key';
Expand All @@ -17,6 +18,7 @@ export const TAB_ACTIVE_KEY = 'data-diplodoc-is-active';
export const TAB_RADIO_KEY = 'data-diplodoc-input';

export const DEFAULT_TABS_GROUP_PREFIX = 'defaultTabsGroup-';
export const ACTIVE_TAB_TEXT = '{selected}';

export interface Tab {
group?: string;
Expand Down
27 changes: 19 additions & 8 deletions src/plugin/getTabId.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
import GithubSlugger from 'github-slugger';

import {Tab} from './transform';
import {ACTIVE_TAB_TEXT} from '../common';

const CUSTOM_ID_REGEXP = /\[?{ ?#(\S+) ?}]?/;

const sluggersStorage = new Map<string, GithubSlugger>();

function parseName(name: string) {
const parts = name.match(CUSTOM_ID_REGEXP);
if (!parts) {
return {
name,
customAnchor: null,
};
let customAnchor: string | null = null;
let pure = name;

if (parts) {
pure = name.replace(parts[0], '');
customAnchor = parts[1];
} else {
pure = name;
customAnchor = null;
}

if (pure.includes(ACTIVE_TAB_TEXT)) {
pure = pure.replace(ACTIVE_TAB_TEXT, '');
}

return {
name: name.replace(parts[0], '').trim(),
customAnchor: parts[1],
name: pure.trim(),
customAnchor,
};
}

Expand All @@ -40,5 +49,7 @@ export function getName(tab: Tab) {
}

function getRawId(tab: Tab): string {
return parseName(tab.name).customAnchor || tab.name;
const {customAnchor, name} = parseName(tab.name);

return customAnchor || name;
}
86 changes: 72 additions & 14 deletions src/plugin/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import MarkdownIt from 'markdown-it';
import StateCore from 'markdown-it/lib/rules_core/state_core';
import Token from 'markdown-it/lib/token';

import {addHiddenProperty, generateID} from './utils';
import {addHiddenProperty, generateID, trim, unquote} from './utils';
import {copyRuntimeFiles} from './copyRuntimeFiles';
import {getName, getTabId, getTabKey} from './getTabId';

import {
ACTIVE_CLASSNAME,
ACTIVE_TAB_TEXT,
DEFAULT_TABS_GROUP_PREFIX,
GROUP_DATA_KEY,
TABS_CLASSNAME,
Expand All @@ -19,6 +21,7 @@ import {
TAB_DATA_VERTICAL_TAB,
TAB_PANEL_CLASSNAME,
VERTICAL_TAB_CLASSNAME,
VERTICAL_TAB_FORCED_OPEN,
} from '../common';

export type PluginOptions = {
Expand All @@ -30,7 +33,7 @@ export type PluginOptions = {

export type TabsOrientation = 'radio' | 'horizontal';

const TAB_RE = /`?{% list tabs( group=([^ ]*))?( (radio)|(horizontal))? %}`?/;
const TAB_RE = /`?{% list tabs .*?%}`?/;

let runsCounter = 0;

Expand Down Expand Up @@ -162,6 +165,13 @@ function insertTabs(
tabListClose.block = true;

const areTabsVerticalClass = align === 'radio' && TABS_VERTICAL_CLASSNAME;
const activeTabsCount = tabs.filter(isTabSelected).length;

if (activeTabsCount > 1) {
throw new Error('Unable to render tabs with more than 1 active element');
}

const hasDefaultOpenTab = activeTabsCount !== 0;

tabsOpen.attrSet(
'class',
Expand Down Expand Up @@ -192,6 +202,12 @@ function insertTabs(
const tab = tabs[i];
const tabId = getTabId(tab, {runId});
const tabKey = getTabKey(tab);
const didTabHasActiveAttr = isTabSelected(tab);
/* if user did not provide default open tab we fallback to first tab (in default tabs only) */
const isTabActive = hasDefaultOpenTab
? didTabHasActiveAttr
: align === 'horizontal' && i === 0;

tab.name = getName(tab);

const tabPanelId = generateID();
Expand All @@ -211,13 +227,13 @@ function insertTabs(
tabPanelClose.block = true;
tabOpen.attrSet(TAB_DATA_ID, tabId);
tabOpen.attrSet(TAB_DATA_KEY, tabKey);
tabOpen.attrSet(TAB_ACTIVE_KEY, i === 0 ? 'true' : 'false');
tabOpen.attrSet('class', TAB_CLASSNAME);
tabOpen.attrJoin('class', 'yfm-tab-group');
tabOpen.attrSet('role', 'tab');
tabOpen.attrSet('aria-controls', tabPanelId);
tabOpen.attrSet('aria-selected', 'false');
tabOpen.attrSet('tabindex', i === 0 ? '0' : '-1');
tabOpen.attrSet(TAB_ACTIVE_KEY, 'false');
tabPanelOpen.attrSet('id', tabPanelId);
tabPanelOpen.attrSet('class', TAB_PANEL_CLASSNAME);
tabPanelOpen.attrSet('role', 'tabpanel');
Expand All @@ -229,15 +245,17 @@ function insertTabs(
tabOpen.attrJoin('class', VERTICAL_TAB_CLASSNAME);
}

if (i === 0) {
if (align === 'horizontal') {
if (isTabActive) {
if (align === 'radio') {
tabOpen.attrSet(VERTICAL_TAB_FORCED_OPEN, 'true');
verticalTabOpen.attrSet('checked', 'true');
tabPanelOpen.attrJoin('class', ACTIVE_CLASSNAME);
} else {
tabOpen.attrSet(TAB_ACTIVE_KEY, i === 0 ? 'true' : 'false');
tabOpen.attrJoin('class', ACTIVE_CLASSNAME);
tabOpen.attrSet('aria-selected', 'true');
} else {
verticalTabOpen.attrSet('checked', 'true');
tabPanelOpen.attrJoin('class', ACTIVE_CLASSNAME);
}

tabPanelOpen.attrJoin('class', ACTIVE_CLASSNAME);
}

if (align === 'radio') {
Expand All @@ -264,6 +282,12 @@ function insertTabs(
return tabsTokens.length;
}

function isTabSelected(tab: Tab) {
const {name} = tab;

return name.includes(ACTIVE_TAB_TEXT);
}

function findCloseTokenIdx(tokens: Token[], idx: number) {
let level = 0;
let i = idx;
Expand Down Expand Up @@ -291,14 +315,49 @@ function matchCloseToken(tokens: Token[], i: number) {
);
}

type TabsProps = {
content: string;
orientation: TabsOrientation;
group: string;
};

function matchOpenToken(tokens: Token[], i: number) {
return (
tokens[i].type === 'paragraph_open' &&
tokens[i + 1].type === 'inline' &&
tokens[i + 1].content.match(TAB_RE)
TAB_RE.test(tokens[i + 1].content) &&
props(tokens[i + 1].content)
);
}

function props(target: string): TabsProps {
target = trim(target.replace('list tabs', ''));

const props = target.split(' ');
const result: TabsProps = {
content: target,
orientation: 'horizontal',
group: `${DEFAULT_TABS_GROUP_PREFIX}${generateID()}`,
};

for (const prop of props) {
const [key, value] = prop.split('=').map(trim);

switch (key) {
case 'horizontal':
case 'radio':
result.orientation = key as TabsOrientation;
break;
case 'group':
result.group = unquote(value);
break;
default:
// TODO: lint unknown tabs props
}
}

return result;
}
export function transform({
runtimeJsPath = '_assets/tabs-extension.js',
runtimeCssPath = '_assets/tabs-extension.css',
Expand All @@ -318,7 +377,7 @@ export function transform({

while (i < tokens.length) {
const match = matchOpenToken(tokens, i);
const openTag = match && match[0];
const openTag = match && match.content;
const isNotEscaped = openTag && !(openTag.startsWith('`') && openTag.endsWith('`'));

if (!match || !isNotEscaped) {
Expand All @@ -334,16 +393,15 @@ export function transform({
continue;
}

const tabsGroup = match[2] || `${DEFAULT_TABS_GROUP_PREFIX}${generateID()}`;
const orientation = (match[4] || 'horizontal') as TabsOrientation;
const tabsGroup = match.group;

const {tabs} = findTabs(state.tokens, i + 3);

if (tabs.length > 0) {
insertTabs(
tabs,
state,
orientation,
match.orientation,
{start: i, end: closeTokenIdx + 2},
{
containerClasses,
Expand Down
8 changes: 8 additions & 0 deletions src/plugin/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ export function generateID() {
return id.substring(id.length - 8);
}

export function trim(target: string) {
return target.trim();
}

export function unquote(target: string) {
return target.match(/^(["']).*\1$/) ? target.slice(1, -1) : target;
}

export function addHiddenProperty<
B extends Record<string | symbol, unknown>,
F extends string | symbol,
Expand Down
62 changes: 51 additions & 11 deletions src/runtime/TabsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
TAB_DATA_KEY,
TAB_PANEL_CLASSNAME,
Tab,
VERTICAL_TAB_FORCED_OPEN,
} from '../common';
import type {TabsOrientation} from '../plugin/transform';
import {
Expand Down Expand Up @@ -152,7 +153,7 @@ export class TabsController {
const previousTargetOffset =
scrollableParent && getOffsetByScrollableParent(targetTab, scrollableParent);

const updatedTabs = this.updateHTML({group, key, align}, align);
const updatedTabs = this.updateHTML({group, key, align}, targetTab, align);

if (updatedTabs > 0) {
this.fireSelectTabEvent({group, key, align}, targetTab?.dataset.diplodocId);
Expand All @@ -163,10 +164,14 @@ export class TabsController {
}
}

private updateHTML(tab: Required<Tab>, align: TabsOrientation) {
private updateHTML(
tab: Required<Tab>,
target: HTMLElement | undefined,
align: TabsOrientation,
) {
switch (align) {
case 'radio': {
return this.updateHTMLVertical(tab);
return this.updateHTMLVertical(tab, target);
}
case 'horizontal': {
return this.updateHTMLHorizontal(tab);
Expand All @@ -176,13 +181,23 @@ export class TabsController {
return 0;
}

private updateHTMLVertical(tab: Required<Tab>) {
private updateHTMLVertical(tab: Required<Tab>, target: HTMLElement | undefined) {
const {group, key} = tab;

const {isForced, root} = this.didTabOpenForce(target);

const singleTabSelector = isForced
? `.yfm-vertical-tab[${VERTICAL_TAB_FORCED_OPEN}="true"]`
: '';

const tabs = this._document.querySelectorAll(
`${Selector.TABS}[${GROUP_DATA_KEY}="${group}"] ${Selector.TAB}[${TAB_DATA_KEY}="${key}"]`,
`${Selector.TABS}[${GROUP_DATA_KEY}="${group}"] ${Selector.TAB}[${TAB_DATA_KEY}="${key}"]${singleTabSelector}`,
);

if (isForced) {
root?.removeAttribute(VERTICAL_TAB_FORCED_OPEN);
}

let updated = 0;

tabs.forEach((tab) => {
Expand All @@ -194,16 +209,29 @@ export class TabsController {

const input = title.children.item(0) as HTMLInputElement;

if (title === tab) {
const checked = input.checked;

if (checked) {
title.classList.remove('active');
content?.classList.remove('active');

input.removeAttribute('checked');
} else {
title.classList.add('active');
content?.classList.add('active');

input.setAttribute('checked', 'true');
}

continue;
}

if (input.hasAttribute('checked')) {
title.classList.remove('active');
content?.classList.remove('active');
input.removeAttribute('checked');
}

if (title === tab) {
title.classList.add('active');
content?.classList.add('active');
input.setAttribute('checked', 'true');
input.removeAttribute('checked');
}

updated++;
Expand Down Expand Up @@ -272,6 +300,18 @@ export class TabsController {
);
}

private didTabOpenForce(target?: HTMLElement) {
if (!target) {
return {};
}

const root = target.dataset.diplodocVerticalTab ? target : target.parentElement;

const isForced = typeof root?.dataset.diplodocRadioForced !== 'undefined';

return {root, isForced};
}

private fireSelectTabEvent(tab: Required<Tab>, diplodocId?: string) {
const {group, key, align} = tab;

Expand Down
2 changes: 1 addition & 1 deletion test/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,12 @@ describe('plugin', () => {
const attrs = [
'data-diplodoc-id',
'data-diplodoc-key',
'data-diplodoc-is-active',
'class',
'role',
'aria-controls',
'aria-selected',
'tabindex',
'data-diplodoc-is-active',
];

// ACT
Expand Down

0 comments on commit 3b59a67

Please sign in to comment.