Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support initial and hide #56

Merged
merged 1 commit into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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 @@
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 @@
}
}

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,17 +181,27 @@
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) => {
const root = tab.parentNode!;

Check warning on line 204 in src/runtime/TabsController.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Forbidden non-null assertion
const elements = root.children;

for (let i = 0; i < elements.length; i += 2) {
Expand All @@ -194,16 +209,29 @@

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 @@
);
}

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 Expand Up @@ -299,7 +339,7 @@

private getTabDataFromHTMLElement(target: HTMLElement): Tab | null {
if (this.areTabsVertical(target)) {
const tab = target.dataset.diplodocVerticalTab ? target : target.parentElement!;

Check warning on line 342 in src/runtime/TabsController.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Forbidden non-null assertion

const key = tab.dataset.diplodocKey;
const group = (tab.closest(Selector.TABS) as HTMLElement)?.dataset.diplodocGroup;
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
Loading