integrate jsonschema2markdown; add support for inline markdown rendering from templates
This commit is contained in:
175
extras/components/color-scheme-toggle-button.js
Normal file
175
extras/components/color-scheme-toggle-button.js
Normal file
@@ -0,0 +1,175 @@
|
||||
(() => {
|
||||
|
||||
const animation = 'linear .5s';
|
||||
const color_scheme = 'color_scheme';
|
||||
const color_scheme_attr = 'data-color-scheme';
|
||||
const transition_attr = 'data-color-transition-enabled';
|
||||
const prefers_dark_scheme = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
// Make sure we set the correct starting color scheme where loading
|
||||
|
||||
const override = localStorage.getItem(color_scheme);
|
||||
|
||||
if (override) {
|
||||
document.body.setAttribute(color_scheme_attr, override);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (document.body.scrollHeight > 30000) {
|
||||
console.warn('document too large, disabling color theme transition animation');
|
||||
return;
|
||||
}
|
||||
|
||||
document.body.setAttribute(transition_attr, '');
|
||||
}, 50);
|
||||
|
||||
const size = 1.25;
|
||||
|
||||
const styles = `
|
||||
:host {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
:host .wrapper {
|
||||
width: ${size}rem;
|
||||
height: ${size}rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 100%;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:host .wrapper .icons {
|
||||
width: ${size * 2}rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
transition: transform ${animation};
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:host .wrapper .icons[data-mode='light'] {
|
||||
/* */
|
||||
}
|
||||
|
||||
:host .wrapper .icons[data-mode='dark'] {
|
||||
transform: translateX(-${size}rem);
|
||||
}
|
||||
|
||||
:host svg.icon {
|
||||
--icon-size: ${size}rem;
|
||||
transition: opacity ${animation};
|
||||
}
|
||||
|
||||
:host svg.icon.sun {
|
||||
color: var(--theme-text-body);
|
||||
}
|
||||
|
||||
:host [data-mode='dark'] svg.icon.sun {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
:host svg.icon.moon {
|
||||
color: var(--theme-text-body);
|
||||
}
|
||||
|
||||
:host [data-mode='light'] svg.icon.moon {
|
||||
opacity: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const template = `
|
||||
<style>${styles}</style>
|
||||
<div class="wrapper" title="Toggle Light / Dark Mode" tabindex="0" role="button" aria-pressed="false">
|
||||
<div class="icons">
|
||||
{{{ icons.sun }}}
|
||||
{{{ icons.moon }}}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
customElements.define('color-scheme-toggle-button',
|
||||
class ColorSchemeToggleButton extends HTMLElement {
|
||||
#wrapper = null;
|
||||
#icons = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.shadowRoot.innerHTML = template;
|
||||
this.#wrapper = this.shadowRoot.querySelector('.wrapper');
|
||||
this.#icons = this.shadowRoot.querySelector('.icons');
|
||||
this.update_icons();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.addEventListener('click', this.onSelect);
|
||||
this.addEventListener('keydown', this.onKeydown);
|
||||
this.addEventListener('keyup', this.onKeyup);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.removeEventListener('click', this.onSelect);
|
||||
this.removeEventListener('keydown', this.onKeydown);
|
||||
this.removeEventListener('keyup', this.onKeyup);
|
||||
}
|
||||
|
||||
onKeydown = (/** @type KeyboardEvent */ event) => {
|
||||
if (event.keyCode === 32) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
|
||||
if (event.keyCode === 13) {
|
||||
event.preventDefault();
|
||||
this.onSelect();
|
||||
}
|
||||
};
|
||||
|
||||
onKeyup = (/** @type KeyboardEvent */ event) => {
|
||||
if (event.keyCode === 32) {
|
||||
event.preventDefault();
|
||||
this.onSelect();
|
||||
}
|
||||
};
|
||||
|
||||
onSelect = () => {
|
||||
toggle();
|
||||
this.update_icons();
|
||||
};
|
||||
|
||||
update_icons() {
|
||||
const current = get_current();
|
||||
|
||||
this.#icons.setAttribute('data-mode', current);
|
||||
this.#wrapper.setAttribute('aria-pressed', current === 'dark');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function get_current() {
|
||||
const override = localStorage.getItem(color_scheme);
|
||||
|
||||
if (override) {
|
||||
return override;
|
||||
}
|
||||
|
||||
return prefers_dark_scheme.matches ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (document.body.hasAttribute(color_scheme_attr)) {
|
||||
localStorage.removeItem(color_scheme);
|
||||
document.body.removeAttribute(color_scheme_attr);
|
||||
}
|
||||
|
||||
else {
|
||||
const preference = prefers_dark_scheme.matches ? 'light' : 'dark';
|
||||
|
||||
localStorage.setItem(color_scheme, preference);
|
||||
document.body.setAttribute(color_scheme_attr, preference);
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
129
extras/components/outline-button.js
Normal file
129
extras/components/outline-button.js
Normal file
@@ -0,0 +1,129 @@
|
||||
(() => {
|
||||
|
||||
const size = 1.25;
|
||||
const outline_attr = 'data-show-outline';
|
||||
|
||||
const styles = `
|
||||
:host {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
:host .wrapper {
|
||||
width: ${size}rem;
|
||||
height: ${size}rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 100%;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:host svg.icon {
|
||||
--icon-size: ${size}rem;
|
||||
color: var(--theme-text-body);
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
left: 0.5rem;
|
||||
pointer-events: none;
|
||||
transition: color linear .5s;
|
||||
}
|
||||
`;
|
||||
|
||||
const template = `
|
||||
<style>${styles}</style>
|
||||
<div class="wrapper" title="Toggle Outline" tabindex="0" role="button" aria-pressed="false">
|
||||
{{{ icons.list }}}
|
||||
</div>
|
||||
<nav id="outline-panel" aria-hidden="true">
|
||||
<!-- todo: i18n -->
|
||||
<h2>Outline</h2>
|
||||
</nav>
|
||||
`;
|
||||
|
||||
customElements.define('outline-button',
|
||||
class OutlineButton extends HTMLElement {
|
||||
#wrapper = null;
|
||||
#outline = null;
|
||||
#outline_built = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.shadowRoot.innerHTML = template;
|
||||
this.#wrapper = this.shadowRoot.querySelector('.wrapper');
|
||||
|
||||
this.#outline = this.shadowRoot.querySelector('#outline-panel');
|
||||
this.#outline.parentNode.removeChild(this.#outline);
|
||||
document.body.appendChild(this.#outline);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.addEventListener('click', this.onSelect);
|
||||
this.addEventListener('keydown', this.onKeydown);
|
||||
this.addEventListener('keyup', this.onKeyup);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.removeEventListener('click', this.onSelect);
|
||||
this.removeEventListener('keydown', this.onKeydown);
|
||||
this.removeEventListener('keyup', this.onKeyup);
|
||||
}
|
||||
|
||||
onKeydown = (/** @type KeyboardEvent */ event) => {
|
||||
if (event.keyCode === 32) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
|
||||
if (event.keyCode === 13) {
|
||||
event.preventDefault();
|
||||
this.onSelect();
|
||||
}
|
||||
};
|
||||
|
||||
onKeyup = (/** @type KeyboardEvent */ event) => {
|
||||
if (event.keyCode === 32) {
|
||||
event.preventDefault();
|
||||
this.onSelect();
|
||||
}
|
||||
};
|
||||
|
||||
onSelect = () => {
|
||||
if (! this.#outline_built) {
|
||||
const root_selector = this.getAttribute('content-root');
|
||||
this.#outline.innerHTML += `<ol>${build_outline(root_selector)}</ol>`;
|
||||
this.#outline_built = true;
|
||||
}
|
||||
|
||||
if (document.body.hasAttribute(outline_attr)) {
|
||||
document.body.removeAttribute(outline_attr);
|
||||
this.#outline.setAttribute('aria-hidden', 'true');
|
||||
this.#wrapper.setAttribute('aria-pressed', 'false');
|
||||
}
|
||||
|
||||
else {
|
||||
document.body.setAttribute(outline_attr, '');
|
||||
this.#outline.removeAttribute('aria-hidden');
|
||||
this.#wrapper.setAttribute('aria-pressed', 'true');
|
||||
}
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
function build_outline(root_selector) {
|
||||
const root = document.querySelector(root_selector);
|
||||
const headings = root.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
||||
const outline = [ ];
|
||||
|
||||
for (const heading of headings) {
|
||||
outline.push(`
|
||||
<li data-depth="${heading.tagName.toLowerCase()}">
|
||||
<a href="#${heading.id}">${heading.innerText}</a>
|
||||
</li>
|
||||
`);
|
||||
}
|
||||
|
||||
return outline.join('');
|
||||
}
|
||||
|
||||
})();
|
105
extras/components/outline-inline.js
Normal file
105
extras/components/outline-inline.js
Normal file
@@ -0,0 +1,105 @@
|
||||
(() => {
|
||||
|
||||
const template = `
|
||||
<style>
|
||||
ol {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li[data-depth='h1'] {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
margin-inline-start: 1rem;
|
||||
}
|
||||
|
||||
li[data-depth='h2'] {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin-inline-start: 2rem;
|
||||
}
|
||||
|
||||
li[data-depth='h3'] {
|
||||
margin-inline-start: 3rem;
|
||||
}
|
||||
|
||||
li[data-depth='h4'] {
|
||||
font-size: 0.9rem;
|
||||
margin-inline-start: 4rem;
|
||||
}
|
||||
|
||||
li[data-depth='h5'] {
|
||||
font-size: 0.9rem;
|
||||
margin-inline-start: 5rem;
|
||||
}
|
||||
|
||||
li[data-depth='h6'] {
|
||||
font-size: 0.9rem;
|
||||
margin-inline-start: 6rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--theme-text-link);
|
||||
}
|
||||
|
||||
a:active,
|
||||
a:hover,
|
||||
a:focus {
|
||||
color: var(--theme-text-link-active);
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: var(--theme-text-link-visited);
|
||||
}
|
||||
</style>
|
||||
<nav id="outline-inline" aria-hidden="true">
|
||||
<em>Outline will render when the page has loaded...</em>
|
||||
</nav>
|
||||
`;
|
||||
|
||||
customElements.define('outline-inline',
|
||||
class OutlineButton extends HTMLElement {
|
||||
#outline = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.shadowRoot.innerHTML = template;
|
||||
this.#outline = this.shadowRoot.querySelector('#outline-inline');
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (document.readyState === 'complete') {
|
||||
this.#render();
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
this.#render();
|
||||
});
|
||||
}
|
||||
|
||||
#render() {
|
||||
const root_selector = this.getAttribute('content-root');
|
||||
const outline = build_outline(root_selector);
|
||||
this.#outline.innerHTML = `<ol>${outline}</ol>`;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function build_outline(root_selector) {
|
||||
const root = document.querySelector(root_selector);
|
||||
const headings = root.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
||||
const outline = [ ];
|
||||
|
||||
for (const heading of headings) {
|
||||
outline.push(`
|
||||
<li data-depth="${heading.tagName.toLowerCase()}">
|
||||
<a href="#${heading.id}">${heading.innerText}</a>
|
||||
</li>
|
||||
`);
|
||||
}
|
||||
|
||||
return outline.join('');
|
||||
}
|
||||
|
||||
})();
|
Reference in New Issue
Block a user