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