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