diff --git a/admin/site/themes/afp/static/js/scroll-spy.js b/admin/site/themes/afp/static/js/scroll-spy.js --- a/admin/site/themes/afp/static/js/scroll-spy.js +++ b/admin/site/themes/afp/static/js/scroll-spy.js @@ -1,147 +1,138 @@ /** * Scrollspy, inspired from bootstrap. Original license: * -------------------------------------------------------------------------- * Bootstrap (v5.1.3): scrollspy.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * * -------------------------------------------------------------------------- */ const EVENT_SPY_ACTIVATE = 'activate.spy' const CLASS_ACTIVE = 'active' const CLASS_SPY_LINK = 'spy-link' /** * Class definition */ class ScrollSpy { constructor(element, target, target_suffix = "", offset = 0.5) { ScrollSpy.instance = this this._element = element this._offset = offset this._offsets = [] this._link_ids = [] this._active_id = null this._scrollHeight = 0 this._target = target - this._eps = 0.01 this._target_suffix = target_suffix window.onscroll = () => this._process() this.refresh() this._process() } - scroll_to(id) { - const elem = document.getElementById(id + this._target_suffix) - if (elem) { - const offset = -window.innerHeight * (this._offset - this._eps) - window.scroll(0, elem.offsetTop + offset) - } - } - refresh() { this._clear() this._offsets = [] this._link_ids = [] this._scrollHeight = this._get_scroll_height() const targets = [] for (const link of document.getElementById(this._target).getElementsByClassName(CLASS_SPY_LINK)) { // visible and has id if (link.id && !is_collapsed(link)) { const target = document.getElementById(this._get_target_id(link)) if (target) { const targetBCR = target.getBoundingClientRect() if (targetBCR.width || targetBCR.height) { targets.push([targetBCR.top + window.pageYOffset, link.id]) } } } } for (const item of targets.sort((a, b) => a[0] - b[0])) { this._offsets.push(item[0]) this._link_ids.push(item[1]) } } // Private _get_target_id(link) { return link.getAttribute('href').slice(1) + this._target_suffix } _get_scroll_height() { return window.scrollHeight || Math.max( document.body.scrollHeight, document.documentElement.scrollHeight ) } _process() { const scroll_top = window.pageYOffset + this._offset * window.innerHeight const scroll_height = this._get_scroll_height() const max_scroll = this._offset * window.innerHeight + scroll_height - window.innerHeight if (this._scrollHeight !== scroll_height) { this.refresh() } if (scroll_top >= max_scroll) { const target_id = this._link_ids[this._link_ids.length - 1] if (this._active_id !== target_id) { this._activate(target_id) } return } if (this._active_id && scroll_top < this._offsets[0] && this._offsets[0] > 0) { this._clear() return } for (let i = this._offsets.length; i--;) { const isActiveTarget = this._active_id !== this._link_ids[i] && scroll_top >= this._offsets[i] && (typeof this._offsets[i + 1] === 'undefined' || scroll_top < this._offsets[i + 1]) if (isActiveTarget) { this._activate(this._link_ids[i]) } } } _activate(link_id) { this._clear() this._active_id = link_id const link = document.getElementById(link_id) if (link) { const elem = document.getElementById(this._get_target_id(link)) link.classList.add(CLASS_ACTIVE) elem.classList.add(CLASS_ACTIVE) const event = new Event(EVENT_SPY_ACTIVATE) event.relatedTarget = link window.dispatchEvent(event) } } _clear() { if (this._active_id) { const link = document.getElementById(this._active_id) if (link) { const elem = document.getElementById(this._get_target_id(link)) if (link.classList.contains(CLASS_ACTIVE)) link.classList.remove(CLASS_ACTIVE) if (elem.classList.contains(CLASS_ACTIVE)) elem.classList.remove(CLASS_ACTIVE) } } } static instance } \ No newline at end of file diff --git a/admin/site/themes/afp/static/js/theory.js b/admin/site/themes/afp/static/js/theory.js --- a/admin/site/themes/afp/static/js/theory.js +++ b/admin/site/themes/afp/static/js/theory.js @@ -1,279 +1,287 @@ /* constants */ const ID_THEORY_LIST = 'theories' const CLASS_LOADER = 'loader' const CLASS_ANIMATION = 'animation' const ATTRIBUTE_THEORY_SRC = 'theory-src' const CLASS_NAVBAR_TYPE = 'theory-navbar-type' const CLASS_THY_NAV = 'thy-nav' const PARAM_NAVBAR_TYPE = 'theory-navbar-type' const ID_NAVBAR_TYPE_SELECTOR = 'navbar-type-selector' const ID_NAVBAR = 'theory-navbar' const NAVBAR_TYPES = ['fact', 'type', 'const'] /* routing */ function target(base_href, rel_href) { const href_parts = rel_href.split('/') if (href_parts.length === 1) return `#${href_parts[0]}` else if (href_parts.length === 3 && href_parts[0] === '..' && href_parts[1] !== '..') { return `../${href_parts[1].toLowerCase()}/#${href_parts[2]}` } else return `${base_href}/../${rel_href}` } -function to_id(thy_name, ref) { +function to_ref(thy_name, ref) { if (ref) return `${thy_name}.html#${ref}` else return `${thy_name}.html` } -const to_fresh_id = (id) => `${id}#` +const to_id = (id) => `${id}#` const to_svg_id = (id) => `${id}#svg` const to_container_id = (id) => `${id}#container` const to_collapsible_id = (id) => `${id}#collapsible` const to_spinner_id = (id) => `${id}#spinner` const to_nav_id = (id) => `${id}#nav` const to_ul_id = (id) => `${id}#ul` const of_ul_id = (id) => id.split('#').slice(0, -1).join('#') const to_a_id = (id) => `${id}#a` /* document translation */ function translate(base_href, thy_name, thy_body) { const thy_refs = [...thy_body.getElementsByTagName('span')].map((span) => { let ref = span.getAttribute('id') if (ref) { - span.setAttribute('id', to_fresh_id(to_id(thy_name, ref))) + span.setAttribute('id', to_id(to_ref(thy_name, ref))) } return ref }).filter(e => e) for (const link of thy_body.getElementsByTagName('a')) { const rel_href = link.getAttribute('href') link.setAttribute('href', target(base_href, rel_href)) } return thy_refs } /* theory lazy-loading */ async function fetch_theory_body(href) { const html_str = await fetch(href).then((http_res) => { if (http_res.status !== 200) return Promise.resolve(`${http_res.statusText}`) else return http_res.text() }).catch((_) => { console.log(`Could not load theory at '${href}'. Redirecting...`) window.location.replace(href) }) const parser = new DOMParser() const html = parser.parseFromString(html_str, 'text/html') return html.getElementsByTagName('body')[0] } async function load_theory(thy_name, href) { const thy_body = await fetch_theory_body(href) const refs = translate(href, thy_name, thy_body) const collapse = document.getElementById(to_collapsible_id(thy_name)) collapse.append(...Array(...thy_body.children).slice(1)) return refs } async function open_theory(thy_name) { const container = document.getElementById(to_container_id(thy_name)) if (container) { if (document.getElementById(to_collapsible_id(thy_name))) open(container) else { const collapsible = parse_elem(`
`) container.appendChild(collapsible) open(container) let refs = await load_theory(thy_name, container.getAttribute(ATTRIBUTE_THEORY_SRC)) await load_theory_nav(thy_name, refs) const spinner = document.getElementById(to_spinner_id(thy_name)) spinner.parentNode.removeChild(spinner) } } } function nav_tree_rec(thy_name, path, key, ref_parts, type) { const rec_ref = ref_parts.filter(e => e.length > 0) const ref = `${path.join('.')}.${key}|${type}` - const id = to_id(thy_name, ref) + const id = to_ref(thy_name, ref) let res if (rec_ref.length < ref_parts.length) { res = `${escape_html(key)}` } else { - const head_id = to_id(thy_name, `${[...path, key, ...ref_parts[0]].join('.')}|${type}`) + const head_id = to_ref(thy_name, `${[...path, key, ...ref_parts[0]].join('.')}|${type}`) res = `${escape_html(key)}` } if (rec_ref.length > 1) { const by_key = group_by(rec_ref) const children = Object.keys(by_key).map((key1) => `
  • ${nav_tree_rec(thy_name, [...path, key], key1, by_key[key1], type)}
  • `) return ` ${res} ` } else return res } function nav_tree(thy_name, refs, type) { let trees = Object.entries(group_by(refs || [])).map(([key, parts]) => `
  • ${nav_tree_rec(thy_name, [thy_name], key, parts, type)}
  • `) return parse_elem(` `) } const cached_refs = Object.fromEntries(NAVBAR_TYPES.map(t => [t, {}])) const load_theory_nav = (thy_name, refs) => { let selected = get_query(PARAM_NAVBAR_TYPE) || NAVBAR_TYPES[0] let by_type = group_by(refs.filter(ref => ref.includes('|')).map((id) => id.split('|').reverse())) let type_selector = document.getElementById(ID_NAVBAR_TYPE_SELECTOR) let options = [...type_selector.options].map(e => e.value) for (let [type, elems] of Object.entries(by_type)) { if (NAVBAR_TYPES.includes(type) && !options.includes(type)) { type_selector.appendChild(parse_elem(``)) } let parts_by_thy = group_by(elems.map((s) => s[0].split('.'))) if (NAVBAR_TYPES.includes(type)) cached_refs[type][thy_name] = parts_by_thy[thy_name] } let tree = nav_tree(thy_name, cached_refs[selected][thy_name], selected) document.getElementById(to_nav_id(thy_name)).appendChild(tree) ScrollSpy.instance.refresh() } /* state */ let navbar_last_opened = [] /* controls */ +const scroll_to = (ref) => { + const elem = document.getElementById(to_id(decodeURI(ref))) + if (elem) { + const offset = -window.innerHeight * 0.49 + window.scroll(0, elem.offsetTop + offset) + } +} + const follow_theory_hash = async () => { let hash = window.location.hash if (hash.length > 1) { const id = hash.slice(1) const thy_name = strip_suffix(id.split('#')[0], '.html') await open_theory(thy_name) - ScrollSpy.instance.scroll_to(id) + scroll_to(id) } } const toggle_theory = async (thy_name) => { - const hash = `#${to_id(thy_name)}` + const hash = `#${to_ref(thy_name)}` const collapsible = document.getElementById(to_container_id(thy_name)) if (collapsible) { if (!collapse(collapsible)) { if (window.location.hash === hash) open(collapsible) else window.location.hash = hash } } else window.location.hash = hash } const change_selector = (type) => { let old_type = get_query(PARAM_NAVBAR_TYPE) if (!old_type || old_type !== type) { set_query(PARAM_NAVBAR_TYPE, type) for (const elem of document.getElementsByClassName(CLASS_NAVBAR_TYPE)) { let thy_name = of_ul_id(elem.id) elem.replaceWith(nav_tree(thy_name, cached_refs[type][thy_name], type)) } ScrollSpy.instance.refresh() } } const open_tree = (elem) => { if (elem.classList.contains(CLASS_COLLAPSIBLE)) { if (open(elem)) navbar_last_opened.push(elem) } if (elem.parentElement) open_tree(elem.parentElement) } const sync_navbar = (link) => { for (const elem of navbar_last_opened){ collapse(elem) } open_tree(link.parentElement) link.scrollIntoView({block: "center"}) } /* setup */ const init = async () => { const theory_list = document.getElementById(ID_THEORY_LIST) const navbar = document.getElementById(ID_NAVBAR) if (theory_list && navbar) { const thy_names = [] for (const theory of theory_list.children) { thy_names.push(theory.id) const href = theory.getAttribute('href') const thy_name = theory.id const thy_collapsible = parse_elem(`
    -

    +

    ${thy_name}

    `) theory.replaceWith(thy_collapsible) } const type = get_query(PARAM_NAVBAR_TYPE) ? get_query(PARAM_NAVBAR_TYPE) : NAVBAR_TYPES[0] navbar.appendChild(parse_elem(`
  • `)) navbar.append(...thy_names.map((thy_name) => parse_elem(`
  • - + ${thy_name}
  • `))) navbar.insertAdjacentElement('afterend', document.createElement('hr')); window.onhashchange = follow_theory_hash window.addEventListener(EVENT_SPY_ACTIVATE, (e) => sync_navbar(e.relatedTarget)) new ScrollSpy(document.body, ID_NAVBAR, "#") await follow_theory_hash() } } document.addEventListener('DOMContentLoaded', init)