-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add term syntax DOCSTOOLS-1268 (#172)
* 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
1 parent
7c0def6
commit ddfb30b
Showing
25 changed files
with
895 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,3 +2,4 @@ import './polyfill'; | |
import './tabs'; | ||
import './code'; | ||
import './cut'; | ||
import './term'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,3 +7,4 @@ | |
@import 'print'; | ||
@import 'cut'; | ||
@import 'file'; | ||
@import 'term'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.