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