+let assert = require('assert')
+let jst_server = (await import('@ndcode/jst_server')).default
+let XDate = require('xdate')
+
+let arrays_equal =
+ (a, b) =>
+ a.length === b.length && a.every((value, index) => value === b[index])
+
+return async (env, head, body, scripts) => {
+ //let cart = await _require('/online_store/cart.jst')
+ let fa_arrow_circle_left = await env.site.get_min_svg('/_svg/fa_arrow-circle-left.svg')
+ let fa_bars = await env.site.get_min_svg('/_svg/fa_bars.svg')
+ let fa_times_circle = await env.site.get_min_svg('/_svg/fa_times-circle.svg')
+ let fa_envelope = await env.site.get_min_svg('/_svg/fa_envelope.svg')
+ let fa_unlock_alt = await env.site.get_min_svg('/_svg/fa_unlock-alt.svg')
+ let fa_search = await env.site.get_min_svg('/_svg/fa_search.svg')
+ let get_session = await _require('/_lib/get_session.jst')
+ //let icon_cart_small = await env.site.get_min_svg('/_svg/icon_cart_small.svg')
+ let icon_cross = await env.site.get_min_svg('/_svg/icon_cross.svg')
+ //let icon_tick = await env.site.get_min_svg('/_svg/icon_tick.svg')
+ let avatar_maker = await env.site.get_min_svg('/_svg/AvatarMaker.svg')
+ let page = await _require('/_lib/page.jst')
+
+ // initialize env.cart
+ //await cart(env)
+
+ // compute breadcrumbs from directories of the path
+ let component_names = env.parsed_url.pathname.split('/')
+ assert(component_names.length >= 2)
+ assert(component_names[0].length === 0)
+ assert(component_names[component_names.length - 1].length)
+ component_names = component_names.slice(1, -1)
+
+ let transaction = await env.site.database.Transaction()
+ let signed_in_as
+ let site_title, copyright
+ let component_titles // collects breadcrumb titles for current page
+ let menu_names, menu_titles // collects top level of menu for the sidebar
+ let feedback_draft
+ try {
+ let root = await transaction.get()
+
+ let session = await get_session(env, root)
+ signed_in_as = await session.get_json('signed_in_as')
+
+ let globals = await root.get('globals')
+ site_title = await globals.get_json('site_title')
+ copyright = await globals.get_json('copyright')
+
+ let navigation = await root.get('navigation')
+ if (navigation === undefined)
+ throw new jst_server.Problem(
+ 'Navigation error',
+ 'Please import the navigation tree into the database.',
+ 508
+ )
+
+ // this code is taken from get_navigation.jst and instrumented
+ let p = navigation
+ component_titles = [await p.get_json('title')] // Home
+ for (let i = 0; i < component_names.length; ++i) {
+ let children = await p.get('children')
+ p = await children.get(component_names[i])
+ if (navigation === undefined)
+ throw new jst_server.Problem(
+ 'Navigation error',
+ `Can't find the path "${
+ component_names.slice(0, i + 1).map(name => '/' + name).join('')
+ }" in the navigation tree.`,
+ 508
+ )
+ component_titles.push(await p.get_json('title'))
+ }
+
+ // similar to above but walks the top level laterally (not deeply)
+ menu_names = await navigation.get_json('menu')
+ let children = await navigation.get('children')
+ menu_titles = [await navigation.get_json('title')] // Home
+ for (let i = 0; i < menu_names.length; ++i) {
+ let child = await children.get(menu_names[i])
+ if (child === undefined)
+ throw new jst_server.Problem(
+ 'Navigation error',
+ `Can't find the path "/${menu_names[i]}" in the navigation tree.`
+ 508
+ )
+ menu_titles.push(await child.get('title'))
+ }
+
+ feedback_draft = await session.get_json('feedback_draft')
+ if (feedback_draft === undefined || env.now >= feedback_draft.expires)
+ feedback_draft = null
+ }
+ finally {
+ transaction.rollback()
+ }
+
+ // save breadcrumbs and their titles for breadcrumbs.jst
+ // note: component_titles.length === component_names.length + 1
+ // component_titles[0] corresponds to /, is 'Home' or similar
+ // component_titles[i] corresponds to component_names[i - 1], i >= 1
+ env.component_names = component_names
+ env.component_titles = component_titles
+
+ // note: menu_titles.length === menu_names.length + 1
+ // menu_titles[0] corresponds to /, is 'Home' or similar
+ // menu_titles[i] corresponds to menu_names[i - 1], i >= 1
+ // (sidebar has Home appearing at same level as its immediate children)
+
+ await page(
+ env,
+ // head
+ async _out => {
+ title {
+ `${site_title}: ${
+ component_titles[
+ component_names.length >= 2 ? 1 : component_names.length
+ ]
+ }`
+ }
+
+ await head(_out)
+ },
+ // body
+ async _out => {
+ // extract top-level directory name
+ assert(env.parsed_url.pathname.slice(0, 1) === '/')
+ let index = env.parsed_url.pathname.indexOf('/', 1)
+ let dir = index === -1 ? '' : env.parsed_url.pathname.slice(1, index)
+
+ div.container-fluid {
+ div.row {
+ div.col-md.sidebar-outer.sidebar-outer-collapsed#sidebar-outer {
+ nav.sidebar-inner.d-flex.flex-column#sidebar-inner {
+ div.mb-4 {
+ div(style="width: 128px; height: 128px;") {
+ _out.push(avatar_maker)
+ }
+ b.h1 {
+ `${site_title}`
+ }
+ }
+
+ div.mb-2 {
+ span#sidebar-signed-in-status {
+ if (signed_in_as !== undefined)
+ 'Signed in.'
+ else
+ 'Signed out.'
+ }
+ ' '
+ if (signed_in_as !== undefined)
+ a#sidebar-sign-in(href="#" hidden) {'Sign in'}
+ else
+ a#sidebar-sign-in(href="#") {'Sign in'}
+ ' '
+ if (signed_in_as !== undefined)
+ a#sidebar-sign-up(href="/my_account/sign_up/index.html" hidden) {'Sign up'}
+ else
+ a#sidebar-sign-up(href="/my_account/sign_up/index.html") {'Sign up'}
+ ' '
+ if (signed_in_as !== undefined)
+ a#sidebar-sign-out(href="#") {'Sign out'}
+ else
+ a#sidebar-sign-out(href="#" hidden) {'Sign out'}
+ }
+
+ form.mb-4(action="/search/index.html") {
+ div.input-group {
+ input.form-control(name="query" type="text" placeholder="Search" aria-describedby="search-button") {}
+ div.input-group-append {
+ button.btn.btn-outline-secondary#sidebar-search-button(type="submit") {
+ div.icon24-outer {
+ div.icon24-inner {_out.push(fa_search)}
+ }
+ }
+ }
+ }
+ }
+
+ // the active entry in the sidebar bar is based on which top-level
+ // page we are under, even if we are not directly on that page
+ // but one of its children, this may be unexpected as the active
+ // entry does not highlight on hover, but you can still click it;
+ // we determine here the path to the corresponding top-level page
+ let component_prefix = component_names.slice(0, 1)
+
+ for (let i = 0; i < menu_titles.length; ++i) {
+ // construct path to the top-level page about to be described
+ let menu_prefix =
+ i === 0 ? [] : [menu_names[i - 1]]
+ let menu_prefix_path =
+ menu_prefix.map(name => '/' + name).join('') + '/index.html'
+
+ if (arrays_equal(menu_prefix, component_prefix))
+ div.nav-item.active {
+ a.nav-link.nav-link2.grid-gutter-background(href=menu_prefix_path) {
+ `${menu_titles[i]}`
+ span.sr-only {' (current)'}
+ }
+ }
+ else
+ div.nav-item {
+ a.nav-link.nav-link2.grid-gutter-background(href=menu_prefix_path) {
+ `${menu_titles[i]}`
+ }
+ }
+ }
+ div.nav-item.mt-auto {
+ a.nav-link.nav-link2.grid-gutter-background#sidebar-give-feedback(href="#") {'Give feedback'}
+ }
+ }
+ }
+
+ div.col-md.sidebar-content {
+ // the breadcrumbs have already been determined by sidebar.jst, as
+ // the HTML title is similar to the breadcrumbs (but without links)
+ let component_names = env.component_names
+ let component_titles = env.component_titles
+
+ // present component_titles as breadcrumbs, except last one as text
+ h2.page-header.grid-gutter-background.'py-2'.mb-0 {
+ button.btn.btn-outline-secondary.sidebar-toggle.mr-3#sidebar-toggle {
+ div.icon24-outer(style="top: -1px;") {
+ div.icon24-inner {_out.push(fa_bars)}
+ }
+ span.sr-only {'Navbar toggle'}
+ }
+ for (let i = 0; i < component_names.length; ++i) {
+ a.h4(
+ href=
+ `${
+ component_names.slice(0, i).map(name => '/' + name).join('')
+ }/index.html`
+ ) {`${component_titles[i]}`}
+ ' '
+ span.h5 {'>'}
+ ' '
+ }
+ `${component_titles[component_names.length]}`
+ }
+
+ await body(_out)
+
+ footer.page-footer.grid-gutter-background.py-5 {
+ a(rel="license" href="http://creativecommons.org/licenses/by-sa/3.0/") {
+ img(alt="Creative Commons License" style="border-width:0;" src="/images/by-sa_3.0_88x31.png") {}
+ }
+ p {
+ 'This website is '
+ a(href="https://git.ndcode.org/public/nick_site.git") {
+ 'open source'
+ }
+ ' and licensed under a '
+ a(rel="license" href="http://creativecommons.org/licenses/by-sa/3.0/") {
+ 'Creative Commons Attribution-ShareAlike 3.0 Unported License'
+ }
+ '.'
+ }
+
+ p.mb-0 {`Copyright © ${new XDate(env.now).getUTCFullYear()} ${copyright}.`}
+ }
+ }
+ div.col-md.sidebar-dummy {}
+ }
+ }
+
+ // hidden part
+ div.modal#sidebar-sign-in-modal(tabindex="-1") {
+ div.modal-dialog {
+ div.modal-content {
+ div.modal-header {
+ span.h4.modal-title {'Sign in'}
+ }
+ div.modal-body {
+ form.mb-2#sidebar-sign-in-form {
+ div.row {
+ div.col-md-12 {
+ div.form-group {
+ label.form-label(for="sidebar-sign-in-email") {'Email'}
+ input.form-control#sidebar-sign-in-email(type="email" required maxlength=256) {}
+ div.invalid-feedback {'Please enter your account\'s email address.'}
+ }
+ }
+ }
+ div.row {
+ div.col-md-12 {
+ div.form-group {
+ label.form-label(for="sidebar-sign-in-password") {'Password'}
+ input.form-control#sidebar-sign-in-password(type="password" required minlength=8 maxlength=256) {}
+ div.invalid-feedback {'Please enter at least 8 characters.'}
+ }
+ }
+ }
+ }
+
+ p {
+ 'No account yet? '
+ a(href="/my_account/sign_up/index.html") {'Sign up'}
+ }
+
+ p.mb-0 {
+ 'Forgot password? '
+ a(href="/my_account/password_reset/index.html") {'Password reset'}
+ }
+
+ div.alert.alert-danger.'mt-3'.mb-0#sidebar-sign-in-alert(hidden) {}
+ }
+ div.modal-footer {
+ button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
+ div.icon24-outer.mr-2 {
+ div.icon24-inner {_out.push(fa_arrow_circle_left)}
+ }
+ 'Back'
+ }
+ button.btn.btn-primary#sidebar-sign-in-sign-in(type="button") {
+ div.icon24-outer.mr-2#sidebar-sign-in-icon {
+ div.icon24-inner {_out.push(fa_unlock_alt)}
+ }
+ //div.icon24-outer.mr-2#sidebar-sign-in-tick(hidden) {
+ // div.icon24-inner {_out.push(icon_tick)}
+ //}
+ div.icon24-outer.mr-2#sidebar-sign-in-cross(hidden) {
+ div.icon24-inner {_out.push(icon_cross)}
+ }
+ div.icon24-outer.mr-2#sidebar-sign-in-spinner(hidden) {
+ div.icon24-inner {
+ div.spinner-border.spinner-border-sm(role="status") {}
+ }
+ }
+ 'Sign in'
+ }
+ }
+ }
+ }
+ }
+
+ div.modal#sidebar-feedback-modal(tabindex="-1") {
+ div.modal-dialog {
+ div.modal-content {
+ div.modal-header {
+ span.h4.modal-title {'Give feedback'}
+ }
+ div.modal-body {
+ p {
+ 'Did you notice something not quite right, or just want to share your impression of this page?'
+ }
+
+ form#sidebar-feedback-form {
+ div.row {
+ div.col-md-12 {
+ div.form-group {
+ label.form-label(for="sidebar-feedback-message") {'Message'}
+ textarea.form-control#sidebar-feedback-message(placeholder="I noticed that..." required rows=4 maxlength=65536) {
+ if (feedback_draft)
+ `${feedback_draft.message}`
+ }
+ div.invalid-feedback {'Please let us have your thoughts.'}
+ }
+ }
+ }
+ }
+
+ div.alert.alert-danger.'mt-3'.mb-0#sidebar-feedback-alert(hidden) {}
+ }
+ div.modal-footer {
+ button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
+ div.icon24-outer.mr-2 {
+ div.icon24-inner {_out.push(fa_arrow_circle_left)}
+ }
+ 'Back'
+ }
+ if (feedback_draft)
+ button.btn.btn-primary#sidebar-feedback-send-message(type="button") {
+ div.icon24-outer.mr-2#sidebar-feedback-icon {
+ div.icon24-inner {_out.push(fa_envelope)}
+ }
+ //div.icon24-outer.mr-2#sidebar-feedback-tick(hidden) {
+ // div.icon24-inner {_out.push(icon_tick)}
+ //}
+ div.icon24-outer.mr-2#sidebar-feedback-cross(hidden) {
+ div.icon24-inner {_out.push(icon_cross)}
+ }
+ div.icon24-outer.mr-2#sidebar-feedback-spinner(hidden) {
+ div.icon24-inner {
+ div.spinner-border.spinner-border-sm(role="status") {}
+ }
+ }
+ 'Send message'
+ }
+ else
+ button.btn.btn-primary#sidebar-feedback-send-message(type="button" disabled) {
+ div.icon24-outer.mr-2#sidebar-feedback-icon {
+ div.icon24-inner {_out.push(fa_envelope)}
+ }
+ //div.icon24-outer.mr-2#sidebar-feedback-tick(hidden) {
+ // div.icon24-inner {_out.push(icon_tick)}
+ //}
+ div.icon24-outer.mr-2#sidebar-feedback-cross(hidden) {
+ div.icon24-inner {_out.push(icon_cross)}
+ }
+ div.icon24-outer.mr-2#sidebar-feedback-spinner(hidden) {
+ div.icon24-inner {
+ div.spinner-border.spinner-border-sm(role="status") {}
+ }
+ }
+ 'Send message'
+ }
+ }
+ }
+ }
+ }
+
+ div.modal#sidebar-message-modal(tabindex="-1") {
+ div.modal-dialog {
+ div.modal-content {
+ div.modal-header {
+ span.h4.modal-title {'Message'}
+ }
+ div.modal-body#sidebar-message-modal-message {
+ }
+ div.modal-footer {
+ button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
+ div.icon24-outer.mr-2 {
+ div.icon24-inner {_out.push(fa_times_circle)}
+ }
+ 'Close'
+ }
+ }
+ }
+ }
+ }
+ },
+ // scripts
+ async _out => {
+ script(src="/js/utils.js") {}
+
+ script {
+ // this function can be overridden in a further script
+ function sign_in_out(status) {
+ return false
+ }
+
+ document.addEventListener(
+ 'DOMContentLoaded',
+ () => {
+ let id_sidebar_feedback_alert = document.getElementById('sidebar-feedback-alert')
+ let id_sidebar_feedback_cross = document.getElementById('sidebar-feedback-cross')
+ let id_sidebar_feedback_form = document.getElementById('sidebar-feedback-form')
+ let id_sidebar_feedback_icon = document.getElementById('sidebar-feedback-icon')
+ let id_sidebar_feedback_message = document.getElementById('sidebar-feedback-message')
+ let id_sidebar_feedback_modal = document.getElementById('sidebar-feedback-modal')
+ let id_sidebar_feedback_send_message = document.getElementById('sidebar-feedback-send-message')
+ let id_sidebar_feedback_spinner = document.getElementById('sidebar-feedback-spinner')
+ let id_sidebar_feedback_tick = document.getElementById('sidebar-feedback-tick')
+ let id_sidebar_give_feedback = document.getElementById('sidebar-give-feedback')
+ let id_sidebar_inner = document.getElementById('sidebar-inner')
+ let id_sidebar_message_modal = document.getElementById('sidebar-message-modal')
+ let id_sidebar_message_modal_message = document.getElementById('sidebar-message-modal-message')
+ let id_sidebar_outer = document.getElementById('sidebar-outer')
+ let id_sidebar_search_button = document.getElementById('sidebar-search-button')
+ let id_sidebar_sign_in = document.getElementById('sidebar-sign-in')
+ let id_sidebar_sign_in_alert = document.getElementById('sidebar-sign-in-alert')
+ let id_sidebar_sign_in_cross = document.getElementById('sidebar-sign-in-cross')
+ let id_sidebar_sign_in_email = document.getElementById('sidebar-sign-in-email')
+ let id_sidebar_sign_in_form = document.getElementById('sidebar-sign-in-form')
+ let id_sidebar_sign_in_icon = document.getElementById('sidebar-sign-in-icon')
+ let id_sidebar_sign_in_modal = document.getElementById('sidebar-sign-in-modal')
+ let id_sidebar_sign_in_password = document.getElementById('sidebar-sign-in-password')
+ let id_sidebar_sign_in_sign_in = document.getElementById('sidebar-sign-in-sign-in')
+ let id_sidebar_sign_in_spinner = document.getElementById('sidebar-sign-in-spinner')
+ let id_sidebar_sign_in_tick = document.getElementById('sidebar-sign-in-tick')
+ let id_sidebar_sign_out = document.getElementById('sidebar-sign-out')
+ let id_sidebar_sign_up = document.getElementById('sidebar-sign-up')
+ let id_sidebar_signed_in_status = document.getElementById('sidebar-signed-in-status')
+ let id_sidebar_toggle = document.getElementById('sidebar-toggle')
+
+ // sign in form
+ id_sidebar_sign_in.addEventListener(
+ 'click',
+ () => {
+ id_sidebar_sign_in_email.value = ''
+ id_sidebar_sign_in_password.value = ''
+ id_sidebar_sign_in_sign_in.disabled = true
+ $('#sidebar-sign-in-modal').modal('show')
+ }
+ )
+
+ $('#sidebar-sign-in-modal').on(
+ 'shown.bs.modal',
+ () => {id_sidebar_sign_in_email.focus()}
+ )
+
+ let sign_in_edited = () => {
+ id_sidebar_sign_in_sign_in.disabled =
+ id_sidebar_sign_in_email.value.length === 0 &&
+ id_sidebar_sign_in_password.value.length === 0
+ id_sidebar_sign_in_icon.hidden = false
+ //id_sidebar_sign_in_tick.hidden = true
+ id_sidebar_sign_in_cross.hidden = true
+ id_sidebar_sign_in_spinner.hidden = true
+ id_sidebar_sign_in_alert.hidden = true
+ }
+
+ id_sidebar_sign_in_email.addEventListener(
+ 'input',
+ sign_in_edited
+ )
+ id_sidebar_sign_in_password.addEventListener(
+ 'input',
+ sign_in_edited
+ )
+
+ id_sidebar_sign_in_sign_in.addEventListener(
+ 'click',
+ async () => {
+ id_sidebar_sign_in_icon.hidden = false
+ //id_sidebar_sign_in_tick.hidden = true
+ id_sidebar_sign_in_cross.hidden = true
+ id_sidebar_sign_in_spinner.hidden = true
+ // the below causes an ugly flicker, so just keep the alert
+ //id_sidebar_sign_in_alert.hidden = true
+
+ if (!id_sidebar_sign_in_form.checkValidity()) {
+ id_sidebar_sign_in_form.classList.add('was-validated');
+
+ id_sidebar_sign_in_icon.hidden = true
+ id_sidebar_sign_in_cross.hidden = false
+ return
+ }
+ id_sidebar_sign_in_form.classList.remove('was-validated');
+
+ let email = id_sidebar_sign_in_email.value.slice(0, 256).toLowerCase()
+
+ id_sidebar_sign_in_icon.hidden = true
+ id_sidebar_sign_in_spinner.hidden = false
+ try {
+ await api_call(
+ '/api/account/sign_in.json',
+ email,
+ id_sidebar_sign_in_password.value.slice(0, 256)
+ )
+ }
+ catch (error) {
+ let problem = Problem.from(error)
+
+ if (problem.title === 'Email not yet verified') {
+ location.href = `/my_account/send_verification_email?email=${encodeURIComponent(email)}`
+ return
+ }
+
+ id_sidebar_sign_in_cross.hidden = false
+ id_sidebar_sign_in_spinner.hidden = true
+
+ id_sidebar_sign_in_alert.textContent = problem.detail
+ //id_sidebar_sign_in_alert.classList.remove('alert-success')
+ //id_sidebar_sign_in_alert.classList.add('alert-danger')
+ id_sidebar_sign_in_alert.hidden = false
+ return
+ }
+ //id_sidebar_sign_in_tick.hidden = false
+ //id_sidebar_sign_in_spinner.hidden = true
+ //id_sidebar_sign_in_alert.textContent = `You are now signed in as "${email}".`
+ //id_sidebar_sign_in_alert.classList.add('alert-success')
+ //id_sidebar_sign_in_alert.classList.remove('alert-danger')
+ //id_sidebar_sign_in_alert.hidden = false
+
+ if (sign_in_out(true))
+ // if location has been changed, leave the spinner and do
+ // not show status/dialog, as it causes an annoying flicker
+ return
+
+ id_sidebar_signed_in_status.textContent = 'Signed in.'
+ id_sidebar_sign_in.hidden = true
+ id_sidebar_sign_up.hidden = true
+ id_sidebar_sign_out.hidden = false
+
+ id_sidebar_sign_in_icon.hidden = false
+ id_sidebar_sign_in_spinner.hidden = true
+ id_sidebar_sign_in_alert.hidden = true
+ id_sidebar_message_modal_message.textContent = `You are now signed in as "${email}".`
+ $('#sidebar-sign-in-modal').modal('hide')
+ $('#sidebar-message-modal').modal('show')
+ }
+ )
+
+ // sign out button
+ id_sidebar_sign_out.addEventListener(
+ 'click',
+ async () => {
+ try {
+ await api_call(
+ '/api/account/sign_out.json'
+ )
+ }
+ catch (error) {
+ let problem = Problem.from(error)
+
+ id_sidebar_message_modal_message.textContent = problem.detail
+ $('#sidebar-sign-in-modal').modal('hide')
+ $('#sidebar-message-modal').modal('show')
+ return
+ }
+
+ if (sign_in_out(false))
+ // if location has been changed, leave the spinner and do
+ // not show status/dialog, as it causes an annoying flicker
+ return
+
+ id_sidebar_signed_in_status.textContent = 'Signed out.'
+ id_sidebar_sign_in.hidden = false
+ id_sidebar_sign_up.hidden = false
+ id_sidebar_sign_out.hidden = true
+
+ id_sidebar_message_modal_message.textContent = `You are now signed out.`
+ $('#sidebar-sign-in-modal').modal('hide')
+ $('#sidebar-message-modal').modal('show')
+ }
+ )
+
+ // feedback form
+ id_sidebar_give_feedback.addEventListener(
+ 'click',
+ () => {
+ // hack to move cursor to end of textarea
+ let temp = id_sidebar_feedback_message.value
+ id_sidebar_feedback_message.value = ''
+ id_sidebar_feedback_message.value = temp
+
+ $('#sidebar-feedback-modal').modal('show')
+ return false
+ }
+ )
+
+ $('#sidebar-feedback-modal').on(
+ 'shown.bs.modal',
+ () => {id_sidebar_feedback_message.focus()}
+ )
+
+ let feedback_input_semaphore = new BinarySemaphore(false)
+ ;(
+ async () => {
+ while (true) {
+ await feedback_input_semaphore.acquire()
+ await new Promise(resolve => setTimeout(resolve, 3000))
+ feedback_input_semaphore.try_acquire()
+ await api_call(
+ '/api/feedback/set_draft.json',
+ id_sidebar_feedback_message.value.length === 0 ?
+ null :
+ {
+ message: id_sidebar_feedback_message.value.slice(0, 65536)
+ }
+ )
+ }
+ }
+ )() // ignore returned promise (start thread)
+
+ let feedback_edited = () => {
+ feedback_input_semaphore.release()
+
+ id_sidebar_feedback_send_message.disabled =
+ id_sidebar_feedback_message.value.length === 0
+ id_sidebar_feedback_icon.hidden = false
+ //id_sidebar_feedback_tick.hidden = true
+ id_sidebar_feedback_cross.hidden = true
+ id_sidebar_feedback_spinner.hidden = true
+ id_sidebar_feedback_alert.hidden = true
+ }
+
+ id_sidebar_feedback_message.addEventListener(
+ 'input',
+ feedback_edited
+ )
+
+ id_sidebar_feedback_send_message.addEventListener(
+ 'click',
+ async () => {
+ id_sidebar_feedback_icon.hidden = false
+ //id_sidebar_feedback_tick.hidden = true
+ id_sidebar_feedback_cross.hidden = true
+ id_sidebar_feedback_spinner.hidden = true
+ // the below causes an ugly flicker, so just keep the alert
+ //id_sidebar_feedback_alert.hidden = true
+
+ if (!id_sidebar_feedback_form.checkValidity()) {
+ id_sidebar_feedback_form.classList.add('was-validated');
+
+ id_sidebar_feedback_icon.hidden = true
+ id_sidebar_feedback_cross.hidden = false
+ return
+ }
+ id_sidebar_feedback_form.classList.remove('was-validated');
+
+ id_sidebar_feedback_icon.hidden = true
+ id_sidebar_feedback_spinner.hidden = false
+ try {
+ await api_call(
+ '/api/feedback/send_message.json',
+ location.href,
+ id_sidebar_feedback_message.value.slice(0, 65536)
+ )
+ }
+ catch (error) {
+ let problem = Problem.from(error)
+
+ id_sidebar_feedback_cross.hidden = false
+ id_sidebar_feedback_spinner.hidden = true
+
+ id_sidebar_feedback_alert.textContent = problem.detail
+ //id_sidebar_feedback_alert.classList.remove('alert-success')
+ //id_sidebar_feedback_alert.classList.add('alert-danger')
+ id_sidebar_feedback_alert.hidden = false
+ return
+ }
+ //id_sidebar_feedback_tick.hidden = false
+ //id_sidebar_feedback_spinner.hidden = true
+ //id_sidebar_feedback_alert.alertContent = 'We have received your message. We will be in touch as soon as possible.'
+ //id_sidebar_feedback_alert.classList.add('alert-success')
+ //id_sidebar_feedback_alert.classList.remove('alert-danger')
+ //id_sidebar_feedback_alert.hidden = false
+
+ id_sidebar_feedback_icon.hidden = false
+ id_sidebar_feedback_spinner.hidden = true
+ id_sidebar_feedback_alert.hidden = true
+ id_sidebar_message_modal_message.textContent = 'Thanks! We have received your feedback.'
+ $('#sidebar-feedback-modal').modal('hide')
+ $('#sidebar-message-modal').modal('show')
+ }
+ )
+
+ // sidebar
+ let sidebar_outer_computed_style = window.getComputedStyle(
+ id_sidebar_outer
+ )
+ let sidebar_toggle_computed_style = window.getComputedStyle(
+ id_sidebar_toggle
+ )
+ let sidebar_is_collapsed =
+ () =>
+ sidebar_toggle_computed_style.display !== 'none' &&
+ id_sidebar_outer.classList.contains(
+ 'sidebar-outer-collapsed'
+ )
+ let sidebar_collapse_update = () => {
+ if (sidebar_outer_computed_style.position === 'sticky') { // md and up
+ id_sidebar_outer.style.flexBasis =
+ sidebar_is_collapsed() ?
+ '0px' :
+ `${id_sidebar_inner.clientWidth}px`
+ id_sidebar_outer.style.removeProperty('height')
+ }
+ else {
+ id_sidebar_outer.style.height =
+ sidebar_is_collapsed() ?
+ '0px' :
+ `${id_sidebar_inner.clientHeight}px`
+ id_sidebar_outer.style.removeProperty('flex-basis')
+ }
+ }
+ window.addEventListener('resize', sidebar_collapse_update)
+ sidebar_collapse_update()
+
+ id_sidebar_outer.addEventListener(
+ 'transitionend',
+ () => {
+ // transitions are only allowed after clicking collapse button,
+ // otherwise they can be triggered by media queries on resize
+ id_sidebar_outer.style.removeProperty('transition-property')
+ }
+ )
+
+ id_sidebar_toggle.addEventListener(
+ 'click',
+ () => {
+ if (
+ id_sidebar_outer.classList.contains(
+ 'sidebar-outer-collapsed'
+ )
+ )
+ id_sidebar_outer.classList.remove(
+ 'sidebar-outer-collapsed'
+ )
+ else
+ id_sidebar_outer.classList.add(
+ 'sidebar-outer-collapsed'
+ )
+ id_sidebar_outer.style.transitionProperty =
+ sidebar_outer_computed_style.position === 'sticky' ?
+ 'flex-basis' : // md and up
+ 'height'
+ sidebar_collapse_update()
+ }
+ )
+ }
+ )
+ }
+
+ await scripts(_out)
+ }
+ )
+}