Upgrade to nick_site commit f981fa57, adds alerts and inactive sidebar option
[ndcode_site.git] / _lib / navbar.jst
1 let assert = require('assert')
2 let jst_server = (await import('@ndcode/jst_server')).default
3 let XDate = require('xdate')
4
5 let arrays_equal =
6   (a, b) =>
7     a.length === b.length && a.every((value, index) => value === b[index])
8
9 return async (env, head, body, scripts) => {
10   //let cart = await _require('/online_store/cart.jst')
11   let fa_arrow_circle_left = await env.site.get_min_svg('/_svg/fa_arrow-circle-left.svg')
12   let fa_times_circle = await env.site.get_min_svg('/_svg/fa_times-circle.svg')
13   let fa_envelope = await env.site.get_min_svg('/_svg/fa_envelope.svg')
14   let fa_unlock_alt = await env.site.get_min_svg('/_svg/fa_unlock-alt.svg')
15   let fa_search = await env.site.get_min_svg('/_svg/fa_search.svg')
16   let get_session = await _require('/_lib/get_session.jst')
17   //let icon_cart_small = await env.site.get_min_svg('/_svg/icon_cart_small.svg')
18   let icon_cross = await env.site.get_min_svg('/_svg/icon_cross.svg')
19   //let icon_tick = await env.site.get_min_svg('/_svg/icon_tick.svg')
20   let logo_large = await env.site.get_min_svg('/_svg/logo_large.svg')
21   let page = await _require('/_lib/page.jst')
22
23   // initialize env.cart
24   //await cart(env)
25
26   // compute breadcrumbs from directories of the path
27   let component_names = env.parsed_url.pathname.split('/')
28   assert(component_names.length >= 2)
29   assert(component_names[0].length === 0)
30   assert(component_names[component_names.length - 1].length)
31   component_names = component_names.slice(1, -1)
32
33   let transaction = await env.site.database.Transaction()
34   let signed_in_as
35   let site_title, copyright
36   let component_titles // collects breadcrumb titles for current page
37   let menu_names, menu_titles // collects top level of menu for the navbar
38   let feedback_draft
39   try {
40     let root = await transaction.get()
41
42     let session = await get_session(env, root)
43     signed_in_as = await session.get_json('signed_in_as')
44
45     let globals = await root.get('globals')
46     site_title = await globals.get_json('site_title')
47     copyright = await globals.get_json('copyright')
48
49     let navigation = await root.get('navigation')
50     if (navigation === undefined)
51       throw new jst_server.Problem(
52         'Navigation error',
53         'Please import the navigation tree into the database.',
54         508
55       )
56
57     // this code is taken from get_navigation.jst and instrumented
58     let p = navigation
59     component_titles = [await p.get_json('title')] // Home
60     for (let i = 0; i < component_names.length; ++i) {
61       let children = await p.get('children')
62       p = await children.get(component_names[i])
63       if (navigation === undefined)
64         throw new jst_server.Problem(
65           'Navigation error',
66           `Can't find the path "${
67             component_names.slice(0, i + 1).map(name => '/' + name).join('')
68           }" in the navigation tree.`,
69           508
70         )
71       component_titles.push(await p.get_json('title'))
72     }
73
74     // similar to above but walks the top level laterally (not deeply)
75     menu_names = await navigation.get_json('menu')
76     let children = await navigation.get('children')
77     menu_titles = [await navigation.get_json('title')] // Home
78     for (let i = 0; i < menu_names.length; ++i) {
79       let child = await children.get(menu_names[i])
80       if (child === undefined)
81         throw new jst_server.Problem(
82           'Navigation error',
83           `Can't find the path "/${menu_names[i]}" in the navigation tree.`
84           508
85         )
86       menu_titles.push(await child.get('title'))
87     }
88
89     feedback_draft = await session.get_json('feedback_draft')
90     if (feedback_draft === undefined || env.now >= feedback_draft.expires)
91       feedback_draft = null
92   }
93   finally {
94     transaction.rollback()
95   }
96
97   // save breadcrumbs and their titles for breadcrumbs.jst
98   // note: component_titles.length === component_names.length + 1
99   // component_titles[0] corresponds to /, is 'Home' or similar
100   // component_titles[i] corresponds to component_names[i - 1], i >= 1
101   env.component_names = component_names
102   env.component_titles = component_titles
103
104   // note: menu_titles.length === menu_names.length + 1
105   // menu_titles[0] corresponds to /, is 'Home' or similar
106   // menu_titles[i] corresponds to menu_names[i - 1], i >= 1
107   // (navbar has Home appearing at same level as its immediate children)
108
109   await page(
110     env,
111     // head
112     async _out => {
113       title {
114         `${site_title}: ${
115           component_titles[
116             component_names.length >= 2 ? 1 : component_names.length
117           ]
118         }`
119       }
120
121       await head(_out)
122     },
123     // body
124     async _out => {
125       // extract top-level directory name
126       assert(env.parsed_url.pathname.slice(0, 1) === '/')
127       let index = env.parsed_url.pathname.indexOf('/', 1)
128       let dir = index === -1 ? '' : env.parsed_url.pathname.slice(1, index)
129
130       div.scrollbar-fix-outer {
131         div.scrollbar-fix-inner {
132           div.container {
133             div.row.align-items-center.py-3 {
134               div.col-sm-8 {
135                 _out.push(logo_large)
136               }
137               div.'col-sm-4' {
138                 div.'mb-1'.text-right {
139                   span#navbar-signed-in-status {
140                     if (signed_in_as !== undefined)
141                       'Signed in.' //`Signed in as ${signed_in_as}.`
142                     else
143                       'Browsing as guest.'
144                   }
145                   ' '
146                   if (signed_in_as !== undefined)
147                     a#navbar-sign-in(href="#" hidden) {'Sign in'}
148                   else
149                     a#navbar-sign-in(href="#") {'Sign in'}
150                   ' '
151                   if (signed_in_as !== undefined)
152                     a#navbar-sign-up(href="/my_account/sign_up/index.html" hidden) {'Sign up'}
153                   else
154                     a#navbar-sign-up(href="/my_account/sign_up/index.html") {'Sign up'}
155                   ' '
156                   if (signed_in_as !== undefined)
157                     a#navbar-sign-out(href="#") {'Sign out'}
158                   else
159                     a#navbar-sign-out(href="#" hidden) {'Sign out'}
160                 }
161
162                 form(action="/search/index.html") {
163                   div.input-group {
164                     input.form-control(name="query" type="text" placeholder="Search" aria-describedby="search-button") {}
165                     div.input-group-append {
166                       button.btn.btn-outline-secondary#navbar-search-button(type="submit") {
167                         div.icon24-outer {
168                           div.icon24-inner {_out.push(fa_search)}
169                         }
170                       }
171                     }
172                   }
173                 }
174               }
175
176               //div.'col-sm-1'.vbottom {
177               //  // a nested div is used to avoid hover colour on the padding
178               //  div.nav-li-a(style="text-align: center;") {
179               //    a(href="/online_store/view_cart/index.html") {
180               //      div.cart-icon {
181               //        _out.push(icon_cart_small)
182               //      }
183               //      div.cart-number {
184               //        div.cart-circle {
185               //          `${(env.cart.items || []).length}`
186               //        }
187               //      }
188               //    }
189               //  }
190               //}
191             }
192             nav.navbar.navbar-expand-lg.navbar-dark.bg-primary.container-background {
193               //a.navbar-brand(href="#") {'Navbar'}
194               //' '
195               button.navbar-toggler(type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation") {
196                 span.navbar-toggler-icon {}
197               }
198               div.collapse.navbar-collapse#navbarSupportedContent {
199                 ul.navbar-nav.mr-auto {
200                   // the active entry in the navbar bar is based on which top-level
201                   // page we are under, even if we are not directly on that page
202                   // but one of its children, this may be unexpected as the active
203                   // entry does not highlight on hover, but you can still click it;
204                   // we determine here the path to the corresponding top-level page
205                   let component_prefix = component_names.slice(0, 1)
206
207                   for (let i = 0; i < menu_titles.length; ++i) {
208                     // construct path to the top-level page about to be described
209                     let menu_prefix =
210                       i == 0 ? [] : [menu_names[i - 1]]
211                     let menu_prefix_path =
212                       menu_prefix.map(name => '/' + name).join('') + '/index.html'
213
214                     if (arrays_equal(menu_prefix, component_prefix))
215                       li.nav-item.active {
216                         a.nav-link(href=menu_prefix_path) {
217                           `${menu_titles[i]}`
218                           span.sr-only {' (current)'}
219                         }
220                       }
221                     else
222                       li.nav-item {
223                         a.nav-link(href=menu_prefix_path) {
224                           `${menu_titles[i]}`
225                         }
226                       }
227                   }
228                   //li.nav-item.dropdown {
229                   //  a.nav-link.dropdown-toggle#navbarDropdown(href="#" role="button" data-toggle="dropdown" aria-expanded="false") {
230                   //    'Dropdown'
231                   //  }
232                   //  div.dropdown-menu(aria-labelledby="navbarDropdown") {
233                   //    a.dropdown-item(href="#") {
234                   //      'Action'
235                   //    }
236                   //    ' '
237                   //    a.dropdown-item(href="#") {
238                   //      'Another action'
239                   //    }
240                   //    div.dropdown-divider {}
241                   //    a.dropdown-item(href="#") {
242                   //      'Something else here'
243                   //    }
244                   //  }
245                   //}
246                   //li.nav-item {
247                   //  a.nav-link.disabled {
248                   //    'Disabled'
249                   //  }
250                   //}
251                 }
252                 ul.navbar-nav.ml-auto {
253                   li.nav-item {
254                     a.nav-link#navbar-give-feedback(href="#") {'Give feedback'}
255                   }
256                 }
257               }
258             }
259
260             await body(_out)
261
262             footer.page-footer.container-background.py-5 {
263               a(rel="license" href="http://creativecommons.org/licenses/by-sa/3.0/") {
264                 img(alt="Creative Commons License" style="border-width:0;" src="/images/by-sa_3.0_88x31.png") {}
265               }
266               p {
267                 'This website is '
268                 a(href="https://git.ndcode.org/public/ndcode_site.git") {
269                   'open source'
270                 }
271                 ' and licensed under a '
272                 a(rel="license" href="http://creativecommons.org/licenses/by-sa/3.0/") {
273                   'Creative Commons Attribution-ShareAlike 3.0 Unported License'
274                 }
275                 '.'
276               }
277
278               p {'Example code fragments embedded within the text are placed in the public domain unless otherwise noted.'}
279
280               p.mb-0 {`Copyright © ${new XDate(env.now).getUTCFullYear()} ${copyright}.`}
281             }
282           }
283         }
284       }
285
286       // hidden part
287       div.modal#navbar-sign-in-modal(tabindex="-1") {
288         div.modal-dialog {
289           div.modal-content {
290             div.modal-header {
291               span.h4.modal-title {'Sign in'}
292             }
293             div.modal-body {
294               form.mb-2#navbar-sign-in-form {
295                 div.row {
296                   div.col-md-12 {
297                     div.form-group {
298                       label.form-label(for="navbar-sign-in-email") {'Email'}
299                       input.form-control#navbar-sign-in-email(type="email" required maxlength=256) {}
300                       div.invalid-feedback {'Please enter your account\'s email address.'}
301                     }
302                   }
303                 }
304                 div.row {
305                   div.col-md-12 {
306                     div.form-group {
307                       label.form-label(for="navbar-sign-in-password") {'Password'}
308                       input.form-control#navbar-sign-in-password(type="password" required minlength=8 maxlength=256) {}
309                       div.invalid-feedback {'Please enter at least 8 characters.'}
310                     }
311                   }
312                 }
313               }
314
315               p {
316                 'No account yet? '
317                 a(href="/my_account/sign_up/index.html") {'Sign up'}
318               }
319
320               p.mb-0 {
321                 'Forgot password? '
322                 a(href="/my_account/password_reset/index.html") {'Password reset'}
323               }
324
325               div.alert.alert-danger.'mt-3'.mb-0#navbar-sign-in-alert(hidden) {}
326             }
327             div.modal-footer {
328               button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
329                 div.icon24-outer.mr-2 {
330                   div.icon24-inner {_out.push(fa_arrow_circle_left)}
331                 }
332                 'Back'
333               }
334               button.btn.btn-primary#navbar-sign-in-sign-in(type="button") {
335                 div.icon24-outer.mr-2#navbar-sign-in-icon {
336                   div.icon24-inner {_out.push(fa_unlock_alt)}
337                 }
338                 //div.icon24-outer.mr-2#navbar-sign-in-tick(hidden) {
339                 //  div.icon24-inner {_out.push(icon_tick)}
340                 //}
341                 div.icon24-outer.mr-2#navbar-sign-in-cross(hidden) {
342                   div.icon24-inner {_out.push(icon_cross)}
343                 }
344                 div.icon24-outer.mr-2#navbar-sign-in-spinner(hidden) {
345                   div.icon24-inner {
346                     div.spinner-border.spinner-border-sm(role="status") {}
347                   }
348                 }
349                 'Sign in'
350               }
351             }
352           }
353         }
354       }
355
356       div.modal#navbar-feedback-modal(tabindex="-1") {
357         div.modal-dialog {
358           div.modal-content {
359             div.modal-header {
360               span.h4.modal-title {'Give feedback'}
361             }
362             div.modal-body {
363               p {
364                 'Did you notice something not quite right, or just want to share your impression of this page?'
365               }
366
367               form#navbar-feedback-form {
368                 div.row {
369                   div.col-md-12 {
370                     div.form-group {
371                       label.form-label(for="navbar-feedback-message") {'Message'}
372                       textarea.form-control#navbar-feedback-message(placeholder="I noticed that..." required rows=4 maxlength=65536) {
373                         if (feedback_draft)
374                           `${feedback_draft.message}`
375                       }
376                       div.invalid-feedback {'Please let us have your thoughts.'}
377                     }
378                   }
379                 }
380               }
381
382               div.alert.alert-danger.'mt-3'.mb-0#navbar-feedback-alert(hidden) {}
383             }
384             div.modal-footer {
385               button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
386                 div.icon24-outer.mr-2 {
387                   div.icon24-inner {_out.push(fa_arrow_circle_left)}
388                 }
389                 'Back'
390               }
391               if (feedback_draft)
392                 button.btn.btn-primary#navbar-feedback-send-message(type="button") {
393                   div.icon24-outer.mr-2#navbar-feedback-icon {
394                     div.icon24-inner {_out.push(fa_envelope)}
395                   }
396                   //div.icon24-outer.mr-2#navbar-feedback-tick(hidden) {
397                   //  div.icon24-inner {_out.push(icon_tick)}
398                   //}
399                   div.icon24-outer.mr-2#navbar-feedback-cross(hidden) {
400                     div.icon24-inner {_out.push(icon_cross)}
401                   }
402                   div.icon24-outer.mr-2#navbar-feedback-spinner(hidden) {
403                     div.icon24-inner {
404                       div.spinner-border.spinner-border-sm(role="status") {}
405                     }
406                   }
407                   'Send message'
408                 }
409               else
410                 button.btn.btn-primary#navbar-feedback-send-message(type="button" disabled) {
411                   div.icon24-outer.mr-2#navbar-feedback-icon {
412                     div.icon24-inner {_out.push(fa_envelope)}
413                   }
414                   //div.icon24-outer.mr-2#navbar-feedback-tick(hidden) {
415                   //  div.icon24-inner {_out.push(icon_tick)}
416                   //}
417                   div.icon24-outer.mr-2#navbar-feedback-cross(hidden) {
418                     div.icon24-inner {_out.push(icon_cross)}
419                   }
420                   div.icon24-outer.mr-2#navbar-feedback-spinner(hidden) {
421                     div.icon24-inner {
422                       div.spinner-border.spinner-border-sm(role="status") {}
423                     }
424                   }
425                   'Send message'
426                 }
427             }
428           }
429         }
430       }
431
432       div.modal#navbar-message-modal(tabindex="-1") {
433         div.modal-dialog {
434           div.modal-content {
435             div.modal-header {
436               span.h4.modal-title {'Message'}
437             }
438             div.modal-body#navbar-message-modal-message {
439             }
440             div.modal-footer {
441               button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
442                 div.icon24-outer.mr-2 {
443                   div.icon24-inner {_out.push(fa_times_circle)}
444                 }
445                 'Close'
446               }
447             }
448           }
449         }
450       }
451     },
452     // scripts
453     async _out => {
454       script(src="/js/utils.js") {}
455
456       script {
457         // this function can be overridden in a further script
458         function sign_in_out(status) {
459           return false
460         }
461
462         document.addEventListener(
463           'DOMContentLoaded',
464           () => {
465             let id_navbar_feedback_alert = document.getElementById('navbar-feedback-alert')
466             let id_navbar_feedback_cross = document.getElementById('navbar-feedback-cross')
467             let id_navbar_feedback_form = document.getElementById('navbar-feedback-form')
468             let id_navbar_feedback_icon = document.getElementById('navbar-feedback-icon')
469             let id_navbar_feedback_message = document.getElementById('navbar-feedback-message')
470             let id_navbar_feedback_modal = document.getElementById('navbar-feedback-modal')
471             let id_navbar_feedback_send_message = document.getElementById('navbar-feedback-send-message')
472             let id_navbar_feedback_spinner = document.getElementById('navbar-feedback-spinner')
473             let id_navbar_feedback_tick = document.getElementById('navbar-feedback-tick')
474             let id_navbar_give_feedback = document.getElementById('navbar-give-feedback')
475             let id_navbar_message_modal = document.getElementById('navbar-message-modal')
476             let id_navbar_message_modal_message = document.getElementById('navbar-message-modal-message')
477             let id_navbar_search_button = document.getElementById('navbar-search-button')
478             let id_navbar_sign_in = document.getElementById('navbar-sign-in')
479             let id_navbar_sign_in_alert = document.getElementById('navbar-sign-in-alert')
480             let id_navbar_sign_in_cross = document.getElementById('navbar-sign-in-cross')
481             let id_navbar_sign_in_email = document.getElementById('navbar-sign-in-email')
482             let id_navbar_sign_in_form = document.getElementById('navbar-sign-in-form')
483             let id_navbar_sign_in_icon = document.getElementById('navbar-sign-in-icon')
484             let id_navbar_sign_in_modal = document.getElementById('navbar-sign-in-modal')
485             let id_navbar_sign_in_password = document.getElementById('navbar-sign-in-password')
486             let id_navbar_sign_in_sign_in = document.getElementById('navbar-sign-in-sign-in')
487             let id_navbar_sign_in_spinner = document.getElementById('navbar-sign-in-spinner')
488             let id_navbar_sign_in_tick = document.getElementById('navbar-sign-in-tick')
489             let id_navbar_sign_out = document.getElementById('navbar-sign-out')
490             let id_navbar_sign_up = document.getElementById('navbar-sign-up')
491             let id_navbar_signed_in_status = document.getElementById('navbar-signed-in-status')
492             //let id_navbarDropdown = document.getElementById('navbarDropdown')
493             //let id_navbarSupportedContent = document.getElementById('navbarSupportedContent')
494
495             // sign in form
496             id_navbar_sign_in.addEventListener(
497               'click',
498               () => {
499                 id_navbar_sign_in_email.value = ''
500                 id_navbar_sign_in_password.value = ''
501                 id_navbar_sign_in_sign_in.disabled = true
502                 $('#navbar-sign-in-modal').modal('show')
503               }
504             )
505
506             $('#navbar-sign-in-modal').on(
507               'shown.bs.modal',
508               () => {id_navbar_sign_in_email.focus()}
509             )
510
511             let sign_in_edited = () => {
512               id_navbar_sign_in_sign_in.disabled =
513                 id_navbar_sign_in_email.value.length === 0 &&
514                   id_navbar_sign_in_password.value.length === 0
515               id_navbar_sign_in_icon.hidden = false
516               //id_navbar_sign_in_tick.hidden = true
517               id_navbar_sign_in_cross.hidden = true
518               id_navbar_sign_in_spinner.hidden = true
519               id_navbar_sign_in_alert.hidden = true
520             }
521
522             id_navbar_sign_in_email.addEventListener(
523               'input',
524               sign_in_edited
525             )
526             id_navbar_sign_in_password.addEventListener(
527               'input',
528               sign_in_edited
529             )
530
531             id_navbar_sign_in_sign_in.addEventListener(
532               'click',
533               async () => {
534                 id_navbar_sign_in_icon.hidden = false
535                 //id_navbar_sign_in_tick.hidden = true
536                 id_navbar_sign_in_cross.hidden = true
537                 id_navbar_sign_in_spinner.hidden = true
538                 // the below causes an ugly flicker, so just keep the alert
539                 //id_navbar_sign_in_alert.hidden = true
540
541                 if (!id_navbar_sign_in_form.checkValidity()) {
542                   id_navbar_sign_in_form.classList.add('was-validated');
543
544                   id_navbar_sign_in_icon.hidden = true
545                   id_navbar_sign_in_cross.hidden = false
546                   return
547                 }
548                 id_navbar_sign_in_form.classList.remove('was-validated');
549
550                 let email = id_navbar_sign_in_email.value.slice(0, 256).toLowerCase()
551
552                 id_navbar_sign_in_icon.hidden = true
553                 id_navbar_sign_in_spinner.hidden = false
554                 try {
555                   await api_call(
556                     '/api/account/sign_in.json',
557                     email,
558                     id_navbar_sign_in_password.value.slice(0, 256)
559                   )
560                 }
561                 catch (error) {
562                   let problem = Problem.from(error)
563
564                   if (problem.title === 'Email not yet verified') {
565                     location.href = `/my_account/send_verification_email?email=${encodeURIComponent(email)}`
566                     return
567                   }
568
569                   id_navbar_sign_in_cross.hidden = false
570                   id_navbar_sign_in_spinner.hidden = true
571
572                   id_navbar_sign_in_alert.textContent = problem.detail
573                   //id_navbar_sign_in_alert.classList.remove('alert-success')
574                   //id_navbar_sign_in_alert.classList.add('alert-danger')
575                   id_navbar_sign_in_alert.hidden = false
576                   return
577                 }
578                 //id_navbar_sign_in_tick.hidden = false
579                 //id_navbar_sign_in_spinner.hidden = true
580                 //id_navbar_sign_in_alert.textContent = `You are now signed in as "${email}".`
581                 //id_navbar_sign_in_alert.classList.add('alert-success')
582                 //id_navbar_sign_in_alert.classList.remove('alert-danger')
583                 //id_navbar_sign_in_alert.hidden = false
584
585                 if (sign_in_out(true))
586                   // if location has been changed, leave the spinner and do
587                   // not show status/dialog, as it causes an annoying flicker
588                   return
589
590                 id_navbar_signed_in_status.textContent = 'Signed in.'
591                 id_navbar_sign_in.hidden = true
592                 id_navbar_sign_up.hidden = true
593                 id_navbar_sign_out.hidden = false
594
595                 id_navbar_sign_in_icon.hidden = false
596                 id_navbar_sign_in_spinner.hidden = true
597                 id_navbar_sign_in_alert.hidden = true
598                 id_navbar_message_modal_message.textContent = `You are now signed in as "${email}".`
599                 $('#navbar-sign-in-modal').modal('hide')
600                 $('#navbar-message-modal').modal('show')
601               }
602             )
603
604             // sign out button
605             id_navbar_sign_out.addEventListener(
606               'click',
607               async () => {
608                 try {
609                   await api_call(
610                     '/api/account/sign_out.json'
611                   )
612                 }
613                 catch (error) {
614                   let problem = Problem.from(error)
615
616                   id_navbar_message_modal_message.textContent = problem.detail
617                   $('#navbar-sign-in-modal').modal('hide')
618                   $('#navbar-message-modal').modal('show')
619                   return
620                 }
621
622                 if (sign_in_out(false))
623                   // if location has been changed, leave the spinner and do
624                   // not show status/dialog, as it causes an annoying flicker
625                   return
626
627                 id_navbar_signed_in_status.textContent = 'Signed out.'
628                 id_navbar_sign_in.hidden = false
629                 id_navbar_sign_up.hidden = false
630                 id_navbar_sign_out.hidden = true
631
632                 id_navbar_message_modal_message.textContent = `You are now signed out.`
633                 $('#navbar-sign-in-modal').modal('hide')
634                 $('#navbar-message-modal').modal('show')
635               }
636             )
637
638             // feedback form
639             id_navbar_give_feedback.addEventListener(
640               'click',
641               () => {
642                 // hack to move cursor to end of textarea
643                 let temp = id_navbar_feedback_message.value
644                 id_navbar_feedback_message.value = ''
645                 id_navbar_feedback_message.value = temp
646
647                 $('#navbar-feedback-modal').modal('show')
648                 return false
649               }
650             )
651
652             $('#navbar-feedback-modal').on(
653               'shown.bs.modal',
654               () => {id_navbar_feedback_message.focus()}
655             )
656
657             let feedback_input_semaphore = new BinarySemaphore(false)
658             ;(
659               async () => {
660                 while (true) {
661                   await feedback_input_semaphore.acquire()
662                   await new Promise(resolve => setTimeout(resolve, 3000))
663                   feedback_input_semaphore.try_acquire()
664                   await api_call(
665                     '/api/feedback/set_draft.json',
666                     id_navbar_feedback_message.value.length === 0 ?
667                       null :
668                       {
669                         message: id_navbar_feedback_message.value.slice(0, 65536)
670                       }
671                   )
672                 }
673               }
674             )() // ignore returned promise (start thread)
675
676             let feedback_edited = () => {
677               feedback_input_semaphore.release()
678
679               id_navbar_feedback_send_message.disabled =
680                 id_navbar_feedback_message.value.length === 0
681               id_navbar_feedback_icon.hidden = false
682               //id_navbar_feedback_tick.hidden = true
683               id_navbar_feedback_cross.hidden = true
684               id_navbar_feedback_spinner.hidden = true
685               id_navbar_feedback_alert.hidden = true
686             }
687
688             id_navbar_feedback_message.addEventListener(
689               'input',
690               feedback_edited
691             )
692
693             id_navbar_feedback_send_message.addEventListener(
694               'click',
695               async () => {
696                 id_navbar_feedback_icon.hidden = false
697                 //id_navbar_feedback_tick.hidden = true
698                 id_navbar_feedback_cross.hidden = true
699                 id_navbar_feedback_spinner.hidden = true
700                 // the below causes an ugly flicker, so just keep the alert
701                 //id_navbar_feedback_alert.hidden = true
702
703                 if (!id_navbar_feedback_form.checkValidity()) {
704                   id_navbar_feedback_form.classList.add('was-validated');
705
706                   id_navbar_feedback_icon.hidden = true
707                   id_navbar_feedback_cross.hidden = false
708                   return
709                 }
710                 id_navbar_feedback_form.classList.remove('was-validated');
711
712                 id_navbar_feedback_icon.hidden = true
713                 id_navbar_feedback_spinner.hidden = false
714                 try {
715                   await api_call(
716                     '/api/feedback/send_message.json',
717                     location.href,
718                     id_navbar_feedback_message.value.slice(0, 65536)
719                   )
720                 }
721                 catch (error) {
722                   let problem = Problem.from(error)
723
724                   id_navbar_feedback_cross.hidden = false
725                   id_navbar_feedback_spinner.hidden = true
726
727                   id_navbar_feedback_alert.textContent = problem.detail
728                   //id_navbar_feedback_alert.classList.remove('alert-success')
729                   //id_navbar_feedback_alert.classList.add('alert-danger')
730                   id_navbar_feedback_alert.hidden = false
731                   return
732                 }
733                 //id_navbar_feedback_tick.hidden = false
734                 //id_navbar_feedback_spinner.hidden = true
735                 //id_navbar_feedback_alert.alertContent = 'We have received your message. We will be in touch as soon as possible.'
736                 //id_navbar_feedback_alert.classList.add('alert-success')
737                 //id_navbar_feedback_alert.classList.remove('alert-danger')
738                 //id_navbar_feedback_alert.hidden = false
739
740                 id_navbar_feedback_icon.hidden = false
741                 id_navbar_feedback_spinner.hidden = true
742                 id_navbar_feedback_alert.hidden = true
743                 id_navbar_message_modal_message.textContent = 'Thanks! We have received your feedback.'
744                 $('#navbar-feedback-modal').modal('hide')
745                 $('#navbar-message-modal').modal('show')
746               }
747             )
748           }
749         )
750       }
751
752       await scripts(_out)
753     }
754   )
755 }