UNPKG

@diplodoc/transform

Version:

A simple transformer of text in YFM (Yandex Flavored Markdown) to HTML

227 lines (176 loc) 7.51 kB
export const Selector = { TITLE: '.yfm .yfm-term_title', CONTENT: '.yfm .yfm-term_dfn', }; export const openClass = 'open'; export const openDefinitionClass = Selector.CONTENT.replace(/\./g, '') + ' ' + openClass; let isListenerNeeded = true; export function setDefinitionId(definitionElement: HTMLElement, termElement: HTMLElement): void { const termId = termElement.getAttribute('id') || Math.random().toString(36).substr(2, 8); definitionElement?.setAttribute('term-id', termId); } export function setDefinitonAriaAttributes( definitionElement: HTMLElement, termElement: HTMLElement, ): void { const ariaLive = termElement.getAttribute('aria-live') || 'polite'; definitionElement?.setAttribute('aria-live', ariaLive); definitionElement?.setAttribute('aria-modal', 'true'); } export function setDefinitionPosition( definitionElement: HTMLElement, termElement: HTMLElement, ): void { const { x: termX, y: termY, right: termRight, left: termLeft, width: termWidth, height: termHeight, } = termElement.getBoundingClientRect(); const termParent = termParentElement(termElement); if (!termParent) { return; } const {right: termParentRight, left: termParentLeft} = termParent.getBoundingClientRect(); if ((termParentRight < termLeft || termParentLeft > termRight) && !isListenerNeeded) { closeDefinition(definitionElement); return; } if (isListenerNeeded && termParent) { termParent.addEventListener('scroll', termOnResize); isListenerNeeded = false; } const relativeX = Number(definitionElement.getAttribute('relativeX')); const relativeY = Number(definitionElement.getAttribute('relativeY')); if (relativeX === termX && relativeY === termY) { return; } definitionElement.setAttribute('relativeX', String(termX)); definitionElement.setAttribute('relativeY', String(termY)); const offsetTop = termHeight + 5; const definitionParent = definitionElement.parentElement; if (!definitionParent) { return; } const {width: definitionWidth} = definitionElement.getBoundingClientRect(); const {left: definitionParentLeft} = definitionParent.getBoundingClientRect(); // If definition not fit document change base alignment const definitionLeftCoordinate = Number(getCoords(termElement).left); const definitionRightCoordinate = definitionWidth + definitionLeftCoordinate; const definitionOutOfScreenOnLeft = definitionLeftCoordinate - definitionWidth < 0; const definitionOutOfScreenOnRight = definitionRightCoordinate > document.body.clientWidth; const isAlignSwapped = definitionOutOfScreenOnRight || document.dir === 'rtl'; const fitDefinitionDocument = isAlignSwapped && !definitionOutOfScreenOnLeft ? definitionWidth - termWidth : 0; const customHeaderTop = getCoords(definitionParent).top - definitionParent.offsetTop; const offsetRight = 5; const shiftLeft = definitionOutOfScreenOnRight ? definitionRightCoordinate - document.body.clientWidth + offsetRight : 0; const offsetLeft = getCoords(termElement).left - definitionParentLeft + definitionParent.offsetLeft - fitDefinitionDocument; const isShiftLeftNeeded = offsetLeft + definitionWidth >= document.body.clientWidth; definitionElement.style.top = Number(getCoords(termElement).top + offsetTop - customHeaderTop) + 'px'; definitionElement.style.left = Number(offsetLeft - (isShiftLeftNeeded ? shiftLeft : 0)) + 'px'; } function termOnResize() { const openedDefinition = document.getElementsByClassName(openDefinitionClass)[0] as HTMLElement; if (!openedDefinition) { return; } const termId = openedDefinition.getAttribute('term-id') || ''; const termElement = document.getElementById(termId); if (!termElement) { return; } setDefinitionPosition(openedDefinition, termElement); } function termParentElement(term: HTMLElement | null) { if (!term) { return null; } const closestScrollableParent = term.closest('table') || term.closest('code'); return closestScrollableParent || term.parentElement; } export function openDefinition(target: HTMLElement) { const openedDefinition = document.getElementsByClassName(openDefinitionClass)[0] as HTMLElement; const termId = target.getAttribute('id'); const termKey = target.getAttribute('term-key'); const definitionElement = document.getElementById(termKey + '_element'); const isSameTerm = openedDefinition && termId === openedDefinition.getAttribute('term-id'); if (isSameTerm) { closeDefinition(openedDefinition); return; } const isTargetDefinitionContent = target.closest( [Selector.CONTENT.replace(' ', ''), openClass].join('.'), ); if (openedDefinition && !isTargetDefinitionContent) { closeDefinition(openedDefinition); } if (!target.matches(Selector.TITLE) || !definitionElement) { return; } setDefinitionId(definitionElement, target); setDefinitonAriaAttributes(definitionElement, target); setDefinitionPosition(definitionElement, target); definitionElement.classList.toggle(openClass); trapFocus(definitionElement); } export function closeDefinition(definition: HTMLElement) { definition.classList.remove(openClass); const term = getTermByDefinition(definition); const termParent = termParentElement(term); if (!termParent) { return; } termParent.removeEventListener('scroll', termOnResize); isListenerNeeded = true; } function getCoords(elem: HTMLElement) { const box = elem.getBoundingClientRect(); const body = document.body; const docEl = document.documentElement; const scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop; const scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft; const clientTop = docEl.clientTop || body.clientTop || 0; const clientLeft = docEl.clientLeft || body.clientLeft || 0; const top = box.top + scrollTop - clientTop; const left = box.left + scrollLeft - clientLeft; return {top: Math.round(top), left: Math.round(left)}; } export function trapFocus(element: HTMLElement) { const focusableElements = element.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])', ); const firstFocusableElement = focusableElements[0] as HTMLElement; const lastFocusableElement = focusableElements[focusableElements.length - 1] as HTMLElement; if (firstFocusableElement) { firstFocusableElement.focus(); } element.addEventListener('keydown', function (e) { const isTabPressed = e.key === 'Tab' || e.keyCode === 9; if (!isTabPressed) { return; } if (e.shiftKey) { if (document.activeElement === firstFocusableElement) { lastFocusableElement.focus(); e.preventDefault(); } } else if (document.activeElement === lastFocusableElement) { firstFocusableElement.focus(); e.preventDefault(); } }); } export function getTermByDefinition(definition: HTMLElement) { const termId = definition.getAttribute('term-id'); return termId ? document.getElementById(termId) : null; }