Skip to content

Commit

Permalink
feat: Add term syntax DOCSTOOLS-1268 (#172)
Browse files Browse the repository at this point in the history
* add term syntax

* term tests

* misc ref

* accessibility ref

* accessibility ref tests

* refactor & listen scroll inside element

* define table/code block as scrollable parents

* fit the definition into the document

* inherit font style

* add linter warning if term definition duplicated

* allow multiline term definitions

* refactor: split plugin function into parts

* chore: rename files

* change syntax *[term]: -> [*term]:

* change syntax for tests

* lint rule - term used without definition

* refactor link validation & misc

* lint rule - term inside definition not allowed

* add lint tokens if lint run

* revert extractTitle fn changes

* mobile/table style improvement

* revert extractTitle fn changes
  • Loading branch information
martyanovandrey authored Sep 13, 2022
1 parent 7c0def6 commit ddfb30b
Show file tree
Hide file tree
Showing 25 changed files with 895 additions and 9 deletions.
1 change: 1 addition & 0 deletions src/js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ import './polyfill';
import './tabs';
import './code';
import './cut';
import './term';
79 changes: 79 additions & 0 deletions src/js/term/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {
Selector,
openClass,
openDefinitionClass,
createDefinitionElement,
setDefinitionId,
setDefinitionPosition,
closeDefinition,
} from './utils';
import {getEventTarget, isCustom} from '../utils';

if (typeof document !== 'undefined') {
document.addEventListener('click', (event) => {
const openDefinition = document.getElementsByClassName(
openDefinitionClass,
)[0] as HTMLElement;
const target = getEventTarget(event) as HTMLElement;

const termId = target.getAttribute('id');
const termKey = target.getAttribute('term-key');
let definitionElement = document.getElementById(termKey + '_element');

if (termKey && !definitionElement) {
definitionElement = createDefinitionElement(target);
}

const isSameTerm = openDefinition && termId === openDefinition.getAttribute('term-id');
if (isSameTerm) {
closeDefinition(openDefinition);
return;
}

const isTargetDefinitionContent = target.closest(
[Selector.CONTENT.replace(' ', ''), openClass].join('.'),
);

if (openDefinition && !isTargetDefinitionContent) {
closeDefinition(openDefinition);
}

if (isCustom(event) || !target.matches(Selector.TITLE) || !definitionElement) {
return;
}

setDefinitionId(definitionElement, target);
setDefinitionPosition(definitionElement, target);

definitionElement.classList.toggle(openClass);
});

document.addEventListener('keydown', (event) => {
const openDefinition = document.getElementsByClassName(
openDefinitionClass,
)[0] as HTMLElement;
if (event.key === 'Escape' && openDefinition) {
closeDefinition(openDefinition);
}
});

window.addEventListener('resize', () => {
const openDefinition = document.getElementsByClassName(
openDefinitionClass,
)[0] as HTMLElement;

if (!openDefinition) {
return;
}

const termId = openDefinition.getAttribute('term-id') || '';
const termElement = document.getElementById(termId);

if (!termElement) {
openDefinition.classList.toggle(openClass);
return;
}

setDefinitionPosition(openDefinition, termElement);
});
}
147 changes: 147 additions & 0 deletions src/js/term/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
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 createDefinitionElement(termElement: HTMLElement) {
const termKey = termElement.getAttribute('term-key');
const definitionTemplate = document.getElementById(
`${termKey}_template`,
) as HTMLTemplateElement;
const definitionElement = definitionTemplate?.content.cloneNode(true).firstChild as HTMLElement;

definitionTemplate?.parentElement?.appendChild(definitionElement);
definitionTemplate.remove();

return definitionElement;
}

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 setDefinitionPosition(
definitionElement: HTMLElement,
termElement: HTMLElement,
): void {
const {
x: termX,
y: termY,
right: termRight,
left: termLeft,
width: termWidth,
} = 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 = 25;
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 definitionRightCoordinate = definitionWidth + Number(getCoords(termElement).left);
const fitDefinitionDocument =
document.body.clientWidth > definitionRightCoordinate ? 0 : definitionWidth - termWidth;

definitionElement.style.top = Number(getCoords(termElement).top + offsetTop) + 'px';
definitionElement.style.left =
Number(
getCoords(termElement).left -
definitionParentLeft +
definitionParent.offsetLeft -
fitDefinitionDocument,
) + 'px';
}

function termOnResize() {
const openDefinition = document.getElementsByClassName(openDefinitionClass)[0] as HTMLElement;

if (!openDefinition) {
return;
}
const termId = openDefinition.getAttribute('term-id') || '';
const termElement = document.getElementById(termId);

if (!termElement) {
return;
}

setDefinitionPosition(openDefinition, termElement);
}

function termParentElement(term: HTMLElement | null) {
if (!term) {
return null;
}

const closestScrollableParent = term.closest('table') || term.closest('code');

return closestScrollableParent || term.parentElement;
}

export function closeDefinition(definition: HTMLElement) {
definition.classList.remove(openClass);
const termId = definition.getAttribute('term-id') || '';
const termParent = termParentElement(document.getElementById(termId));

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)};
}
79 changes: 79 additions & 0 deletions src/scss/_term.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
.yfm-term {
&_title {
color: #027bf3;
cursor: pointer;

border-bottom: 1px dotted;

font-size: inherit;
line-height: inherit;
font-style: normal;

&:hover {
color: #004080;
}
}

&_dfn {
position: absolute;
z-index: 1000;

width: fit-content;
max-width: 450px;

@media screen and (max-width: 600px) {
& {
max-width: 80%;
}
}

visibility: hidden;
opacity: 0;

padding: 10px;

background-color: rgb(255, 255, 255);

font-size: inherit;
line-height: inherit;
font-style: normal;

border-radius: 4px;

box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
outline: none;

&::before {
content: '';
position: absolute;
z-index: -1;
top: 0;
right: 0;
bottom: 0;
left: 0;

border-radius: inherit;
box-shadow: 0 0 0 1px rgb(229, 229, 229);
}

&.open {
visibility: visible;

animation-name: popup;
animation-duration: 0.1s;
animation-timing-function: ease-out;
animation-fill-mode: forwards;

@keyframes popup {
0% {
opacity: 0;
transform: translateY(10px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
}
}
}
1 change: 1 addition & 0 deletions src/scss/yfm.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
@import 'print';
@import 'cut';
@import 'file';
@import 'term';
10 changes: 8 additions & 2 deletions src/transform/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {bold} from 'chalk';
import attrs from 'markdown-it-attrs';
import Token from 'markdown-it/lib/token';

import {log, LogLevels} from './log';
import makeHighlight from './highlight';
Expand All @@ -12,6 +13,7 @@ import anchors from './plugins/anchors';
import code from './plugins/code';
import cut from './plugins/cut';
import deflist from './plugins/deflist';
import term from './plugins/term';
import file from './plugins/file';
import imsize from './plugins/imsize';
import meta from './plugins/meta';
Expand Down Expand Up @@ -84,6 +86,7 @@ function transform(originInput: string, opts: OptionsType = {}): OutputType {
yfmTable,
file,
imsize,
term,
],
highlightLangs = {},
...customOptions
Expand All @@ -107,13 +110,14 @@ function transform(originInput: string, opts: OptionsType = {}): OutputType {
const md = initMd({html: allowHTML, linkify, highlight, breaks});
// Need for ids of headers
md.use(attrs, {leftDelimiter, rightDelimiter});

plugins.forEach((plugin) => md.use(plugin, pluginOptions));

try {
let title;
let tokens;
let titleTokens;
const env = {};
const env = {} as {[key: string]: Token[] | unknown};

tokens = md.parse(input, env);

Expand All @@ -130,8 +134,10 @@ function transform(originInput: string, opts: OptionsType = {}): OutputType {
}

const headings = getHeadings(tokens, needFlatListHeadings);
const html = md.renderer.render(tokens, md.options, env);

// add all term template tokens to the end of the html
const termTokens = (env.termTokens as Token[]) || [];
const html = md.renderer.render([...tokens, ...termTokens], md.options, env);
const assets = md.assets;
const meta = md.meta;

Expand Down
Loading

0 comments on commit ddfb30b

Please sign in to comment.