PageSourceSearch

https://www.filmpreservation.org/assets/adva_cms-9156d16605c63061a…74a62bc490725908951efa20d1d58e6462e37c8b.js

js filmpreservation.org collected 2026-09-24 17:21:00 UTC 98,773 bytes, 3,073 lines download raw bytes

1/*
2Unobtrusive JavaScript
3https://github.com/rails/rails/blob/main/actionview/app/javascript
4Released under the MIT license
5 */
6(function(global, factory) {
7  typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, 
8  global.Rails = factory());
9})(this, (function() {
10  "use strict";
11  const linkClickSelector = "a[data-confirm], a[data-method], a[data-remote]:not([disabled]), a[data-disable-with], a[data-disable]";
12  const buttonClickSelector = {
13    selector: "button[data-remote]:not([form]), button[data-confirm]:not([form])",
14    exclude: "form button"
15  };
16  const inputChangeSelector = "select[data-remote], input[data-remote], textarea[data-remote]";
17  const formSubmitSelector = "form:not([data-turbo=true])";
18  const formInputClickSelector = "form:not([data-turbo=true]) input[type=submit], form:not([data-turbo=true]) input[type=image], form:not([data-turbo=true]) button[type=submit], form:not([data-turbo=true]) button:not([type]), input[type=submit][form], input[type=image][form], button[type=submit][form], button[form]:not([type])";
19  const formDisableSelector = "input[data-disable-with]:enabled, button[data-disable-with]:enabled, textarea[data-disable-with]:enabled, input[data-disable]:enabled, button[data-disable]:enabled, textarea[data-disable]:enabled";
20  const formEnableSelector = "input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled, input[data-disable]:disabled, button[data-disable]:disabled, textarea[data-disable]:disabled";
21  const fileInputSelector = "input[name][type=file]:not([disabled])";
22  const linkDisableSelector = "a[data-disable-with], a[data-disable]";
23  const buttonDisableSelector = "button[data-remote][data-disable-with], button[data-remote][data-disable]";
24  let nonce = null;
25  const loadCSPNonce = () => {
26    const metaTag = document.querySelector("meta[name=csp-nonce]");
27    return nonce = metaTag && metaTag.content;
28  };
29  const cspNonce = () => nonce || loadCSPNonce();
30  const m = Element.prototype.matches || Element.prototype.matchesSelector || Element.prototype.mozMatchesSelector || Element.prototype.msMatchesSelector || Element.prototype.oMatchesSelector || Element.prototype.webkitMatchesSelector;
31  const matches = function(element, selector) {
32    if (selector.exclude) {
33      return m.call(element, selector.selector) && !m.call(element, selector.exclude);
34    } else {
35      return m.call(element, selector);
36    }
37  };
38  const EXPANDO = "_ujsData";
39  const getData = (element, key) => element[EXPANDO] ? element[EXPANDO][key] : undefined;
40  const setData = function(element, key, value) {
41    if (!element[EXPANDO]) {
42      element[EXPANDO] = {};
43    }
44    return element[EXPANDO][key] = value;
45  };
46  const $ = selector => Array.prototype.slice.call(document.querySelectorAll(selector));
47  const isContentEditable = function(element) {
48    var isEditable = false;
49    do {
50      if (element.isContentEditable) {
51        isEditable = true;
52        break;
53      }
54      element = element.parentElement;
55    } while (element);
56    return isEditable;
57  };
58  const csrfToken = () => {
59    const meta = document.querySelector("meta[name=csrf-token]");
60    return meta && meta.content;
61  };
62  const csrfParam = () => {
63    const meta = document.querySelector("meta[name=csrf-param]");
64    return meta && meta.content;
65  };
66  const CSRFProtection = xhr => {
67    const token = csrfToken();
68    if (token) {
69      return xhr.setRequestHeader("X-CSRF-Token", token);
70    }
71  };
72  const refreshCSRFTokens = () => {
73    const token = csrfToken();
74    const param = csrfParam();
75    if (token && param) {
76      return $('form input[name="' + param + '"]').forEach((input => input.value = token));
77    }
78  };
79  const AcceptHeaders = {
80    "*": "*/*",
81    text: "text/plain",
82    html: "text/html",
83    xml: "application/xml, text/xml",
84    json: "application/json, text/javascript",
85    script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"
86  };
87  const ajax = options => {
88    options = prepareOptions(options);
89    var xhr = createXHR(options, (function() {
90      const response = processResponse(xhr.response != null ? xhr.response : xhr.responseText, xhr.getResponseHeader("Content-Type"));
91      if (Math.floor(xhr.status / 100) === 2) {
92        if (typeof options.success === "function") {
93          options.success(response, xhr.statusText, xhr);
94        }
95      } else {
96        if (typeof options.error === "function") {
97          options.error(response, xhr.statusText, xhr);
98        }
99      }
100      return typeof options.complete === "function" ? options.complete(xhr, xhr.statusText) : undefined;
101    }));
102    if (options.beforeSend && !options.beforeSend(xhr, options)) {
103      return false;
104    }
105    if (xhr.readyState === XMLHttpRequest.OPENED) {
106      return xhr.send(options.data);
107    }
108  };
109  var prepareOptions = function(options) {
110    options.url = options.url || location.href;
111    options.type = options.type.toUpperCase();
112    if (options.type === "GET" && options.data) {
113      if (options.url.indexOf("?") < 0) {
114        options.url += "?" + options.data;
115      } else {
116        options.url += "&" + options.data;
117      }
118    }
119    if (!(options.dataType in AcceptHeaders)) {
120      options.dataType = "*";
121    }
122    options.accept = AcceptHeaders[options.dataType];
123    if (options.dataType !== "*") {
124      options.accept += ", */*; q=0.01";
125    }
126    return options;
127  };
128  var createXHR = function(options, done) {
129    const xhr = new XMLHttpRequest;
130    xhr.open(options.type, options.url, true);
131    xhr.setRequestHeader("Accept", options.accept);
132    if (typeof options.data === "string") {
133      xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
134    }
135    if (!options.crossDomain) {
136      xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
137      CSRFProtection(xhr);
138    }
139    xhr.withCredentials = !!options.withCredentials;
140    xhr.onreadystatechange = function() {
141      if (xhr.readyState === XMLHttpRequest.DONE) {
142        return done(xhr);
143      }
144    };
145    return xhr;
146  };
147  var processResponse = function(response, type) {
148    if (typeof response === "string" && typeof type === "string") {
149      if (type.match(/\bjson\b/)) {
150        try {
151          response = JSON.parse(response);
152        } catch (error) {}
153      } else if (type.match(/\b(?:java|ecma)script\b/)) {
154        const script = document.createElement("script");
155        script.setAttribute("nonce", cspNonce());
156        script.text = response;
157        document.head.appendChild(script).parentNode.removeChild(script);
158      } else if (type.match(/\b(xml|html|svg)\b/)) {
159        const parser = new DOMParser;
160        type = type.replace(/;.+/, "");
161        try {
162          response = parser.parseFromString(response, type);
163        } catch (error1) {}
164      }
165    }
166    return response;
167  };
168  const href = element => element.href;
169  const isCrossDomain = function(url) {
170    const originAnchor = document.createElement("a");
171    originAnchor.href = location.href;
172    const urlAnchor = document.createElement("a");
173    try {
174      urlAnchor.href = url;
175      return !((!urlAnchor.protocol || urlAnchor.protocol === ":") && !urlAnchor.host || originAnchor.protocol + "//" + originAnchor.host === urlAnchor.protocol + "//" + urlAnchor.host);
176    } catch (e) {
177      return true;
178    }
179  };
180  let preventDefault;
181  let {CustomEvent: CustomEvent} = window;
182  if (typeof CustomEvent !== "function") {
183    CustomEvent = function(event, params) {
184      const evt = document.createEvent("CustomEvent");
185      evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
186      return evt;
187    };
188    CustomEvent.prototype = window.Event.prototype;
189    ({preventDefault: preventDefault} = CustomEvent.prototype);
190    CustomEvent.prototype.preventDefault = function() {
191      const result = preventDefault.call(this);
192      if (this.cancelable && !this.defaultPrevented) {
193        Object.defineProperty(this, "defaultPrevented", {
194          get() {
195            return true;
196          }
197        });
198      }
199      return result;
200    };
201  }
202  const fire = (obj, name, data) => {
203    const event = new CustomEvent(name, {
204      bubbles: true,
205      cancelable: true,
206      detail: data
207    });
208    obj.dispatchEvent(event);
209    return !event.defaultPrevented;
210  };
211  const stopEverything = e => {
212    fire(e.target, "ujs:everythingStopped");
213    e.preventDefault();
214    e.stopPropagation();
215    e.stopImmediatePropagation();
216  };
217  const delegate = (element, selector, eventType, handler) => element.addEventListener(eventType, (function(e) {
218    let {target: target} = e;
219    while (!!(target instanceof Element) && !matches(target, selector)) {
220      target = target.parentNode;
221    }
222    if (target instanceof Element && handler.call(target, e) === false) {
223      e.preventDefault();
224      e.stopPropagation();
225    }
226  }));
227  const toArray = e => Array.prototype.slice.call(e);
228  const serializeElement = (element, additionalParam) => {
229    let inputs = [ element ];
230    if (matches(element, "form")) {
231      inputs = toArray(element.elements);
232    }
233    const params = [];
234    inputs.forEach((function(input) {
235      if (!input.name || input.disabled) {
236        return;
237      }
238      if (matches(input, "fieldset[disabled] *")) {
239        return;
240      }
241      if (matches(input, "select")) {
242        toArray(input.options).forEach((function(option) {
243          if (option.selected) {
244            params.push({
245              name: input.name,
246              value: option.value
247            });
248          }
249        }));
250      } else if (input.checked || [ "radio", "checkbox", "submit" ].indexOf(input.type) === -1) {
251        params.push({
252          name: input.name,
253          value: input.value
254        });
255      }
256    }));
257    if (additionalParam) {
258      params.push(additionalParam);
259    }
260    return params.map((function(param) {
261      if (param.name) {
262        return `${encodeURIComponent(param.name)}=${encodeURIComponent(param.value)}`;
263      } else {
264        return param;
265      }
266    })).join("&");
267  };
268  const formElements = (form, selector) => {
269    if (matches(form, "form")) {
270      return toArray(form.elements).filter((el => matches(el, selector)));
271    } else {
272      return toArray(form.querySelectorAll(selector));
273    }
274  };
275  const handleConfirmWithRails = rails => function(e) {
276    if (!allowAction(this, rails)) {
277      stopEverything(e);
278    }
279  };
280  const confirm = (message, element) => window.confirm(message);
281  var allowAction = function(element, rails) {
282    let callback;
283    const message = element.getAttribute("data-confirm");
284    if (!message) {
285      return true;
286    }
287    let answer = false;
288    if (fire(element, "confirm")) {
289      try {
290        answer = rails.confirm(message, element);
291      } catch (error) {}
292      callback = fire(element, "confirm:complete", [ answer ]);
293    }
294    return answer && callback;
295  };
296  const handleDisabledElement = function(e) {
297    const element = this;
298    if (element.disabled) {
299      stopEverything(e);
300    }
301  };
302  const enableElement = e => {
303    let element;
304    if (e instanceof Event) {
305      if (isXhrRedirect(e)) {
306        return;
307      }
308      element = e.target;
309    } else {
310      element = e;
311    }
312    if (isContentEditable(element)) {
313      return;
314    }
315    if (matches(element, linkDisableSelector)) {
316      return enableLinkElement(element);
317    } else if (matches(element, buttonDisableSelector) || matches(element, formEnableSelector)) {
318      return enableFormElement(element);
319    } else if (matches(element, formSubmitSelector)) {
320      return enableFormElements(element);
321    }
322  };
323  const disableElement = e => {
324    const element = e instanceof Event ? e.target : e;
325    if (isContentEditable(element)) {
326      return;
327    }
328    if (matches(element, linkDisableSelector)) {
329      return disableLinkElement(element);
330    } else if (matches(element, buttonDisableSelector) || matches(element, formDisableSelector)) {
331      return disableFormElement(element);
332    } else if (matches(element, formSubmitSelector)) {
333      return disableFormElements(element);
334    }
335  };
336  var disableLinkElement = function(element) {
337    if (getData(element, "ujs:disabled")) {
338      return;
339    }
340    const replacement = element.getAttribute("data-disable-with");
341    if (replacement != null) {
342      setData(element, "ujs:enable-with", element.innerHTML);
343      element.innerHTML = replacement;
344    }
345    element.addEventListener("click", stopEverything);
346    return setData(element, "ujs:disabled", true);
347  };
348  var enableLinkElement = function(element) {
349    const originalText = getData(element, "ujs:enable-with");
350    if (originalText != null) {
351      element.innerHTML = originalText;
352      setData(element, "ujs:enable-with", null);
353    }
354    element.removeEventListener("click", stopEverything);
355    return setData(element, "ujs:disabled", null);
356  };
357  var disableFormElements = form => formElements(form, formDisableSelector).forEach(disableFormElement);
358  var disableFormElement = function(element) {
359    if (getData(element, "ujs:disabled")) {
360      return;
361    }
362    const replacement = element.getAttribute("data-disable-with");
363    if (replacement != null) {
364      if (matches(element, "button")) {
365        setData(element, "ujs:enable-with", element.innerHTML);
366        element.innerHTML = replacement;
367      } else {
368        setData(element, "ujs:enable-with", element.value);
369        element.value = replacement;
370      }
371    }
372    element.disabled = true;
373    return setData(element, "ujs:disabled", true);
374  };
375  var enableFormElements = form => formElements(form, formEnableSelector).forEach((element => enableFormElement(element)));
376  var enableFormElement = function(element) {
377    const originalText = getData(element, "ujs:enable-with");
378    if (originalText != null) {
379      if (matches(element, "button")) {
380        element.innerHTML = originalText;
381      } else {
382        element.value = originalText;
383      }
384      setData(element, "ujs:enable-with", null);
385    }
386    element.disabled = false;
387    return setData(element, "ujs:disabled", null);
388  };
389  var isXhrRedirect = function(event) {
390    const xhr = event.detail ? event.detail[0] : undefined;
391    return xhr && xhr.getResponseHeader("X-Xhr-Redirect");
392  };
393  const handleMethodWithRails = rails => function(e) {
394    const link = this;
395    const method = link.getAttribute("data-method");
396    if (!method) {
397      return;
398    }
399    if (isContentEditable(this)) {
400      return;
401    }
402    const href = rails.href(link);
403    const csrfToken$1 = csrfToken();
404    const csrfParam$1 = csrfParam();
405    const form = document.createElement("form");
406    let formContent = `<input name='_method' value='${method}' type='hidden' />`;
407    if (csrfParam$1 && csrfToken$1 && !isCrossDomain(href)) {
408      formContent += `<input name='${csrfParam$1}' value='${csrfToken$1}' type='hidden' />`;
409    }
410    formContent += '<input type="submit" />';
411    form.method = "post";
412    form.action = href;
413    form.target = link.target;
414    form.innerHTML = formContent;
415    form.style.display = "none";
416    document.body.appendChild(form);
417    form.querySelector('[type="submit"]').click();
418    stopEverything(e);
419  };
420  const isRemote = function(element) {
421    const value = element.getAttribute("data-remote");
422    return value != null && value !== "false";
423  };
424  const handleRemoteWithRails = rails => function(e) {
425    let data, method, url;
426    const element = this;
427    if (!isRemote(element)) {
428      return true;
429    }
430    if (!fire(element, "ajax:before")) {
431      fire(element, "ajax:stopped");
432      return false;
433    }
434    if (isContentEditable(element)) {
435      fire(element, "ajax:stopped");
436      return false;
437    }
438    const withCredentials = element.getAttribute("data-with-credentials");
439    const dataType = element.getAttribute("data-type") || "script";
440    if (matches(element, formSubmitSelector)) {
441      const button = getData(element, "ujs:submit-button");
442      method = getData(element, "ujs:submit-button-formmethod") || element.getAttribute("method") || "get";
443      url = getData(element, "ujs:submit-button-formaction") || element.getAttribute("action") || location.href;
444      if (method.toUpperCase() === "GET") {
445        url = url.replace(/\?.*$/, "");
446      }
447      if (element.enctype === "multipart/form-data") {
448        data = new FormData(element);
449        if (button != null) {
450          data.append(button.name, button.value);
451        }
452      } else {
453        data = serializeElement(element, button);
454      }
455      setData(element, "ujs:submit-button", null);
456      setData(element, "ujs:submit-button-formmethod", null);
457      setData(element, "ujs:submit-button-formaction", null);
458    } else if (matches(element, buttonClickSelector) || matches(element, inputChangeSelector)) {
459      method = element.getAttribute("data-method");
460      url = element.getAttribute("data-url");
461      data = serializeElement(element, element.getAttribute("data-params"));
462    } else {
463      method = element.getAttribute("data-method");
464      url = rails.href(element);
465      data = element.getAttribute("data-params");
466    }
467    ajax({
468      type: method || "GET",
469      url: url,
470      data: data,
471      dataType: dataType,
472      beforeSend(xhr, options) {
473        if (fire(element, "ajax:beforeSend", [ xhr, options ])) {
474          return fire(element, "ajax:send", [ xhr ]);
475        } else {
476          fire(element, "ajax:stopped");
477          return false;
478        }
479      },
480      success(...args) {
481        return fire(element, "ajax:success", args);
482      },
483      error(...args) {
484        return fire(element, "ajax:error", args);
485      },
486      complete(...args) {
487        return fire(element, "ajax:complete", args);
488      },
489      crossDomain: isCrossDomain(url),
490      withCredentials: withCredentials != null && withCredentials !== "false"
491    });
492    stopEverything(e);
493  };
494  const formSubmitButtonClick = function(e) {
495    const button = this;
496    const {form: form} = button;
497    if (!form) {
498      return;
499    }
500    if (button.name) {
501      setData(form, "ujs:submit-button", {
502        name: button.name,
503        value: button.value
504      });
505    }
506    setData(form, "ujs:formnovalidate-button", button.formNoValidate);
507    setData(form, "ujs:submit-button-formaction", button.getAttribute("formaction"));
508    return setData(form, "ujs:submit-button-formmethod", button.getAttribute("formmethod"));
509  };
510  const preventInsignificantClick = function(e) {
511    const link = this;
512    const method = (link.getAttribute("data-method") || "GET").toUpperCase();
513    const data = link.getAttribute("data-params");
514    const metaClick = e.metaKey || e.ctrlKey;
515    const insignificantMetaClick = metaClick && method === "GET" && !data;
516    const nonPrimaryMouseClick = e.button != null && e.button !== 0;
517    if (nonPrimaryMouseClick || insignificantMetaClick) {
518      e.stopImmediatePropagation();
519    }
520  };
521  const Rails = {
522    $: $,
523    ajax: ajax,
524    buttonClickSelector: buttonClickSelector,
525    buttonDisableSelector: buttonDisableSelector,
526    confirm: confirm,
527    cspNonce: cspNonce,
528    csrfToken: csrfToken,
529    csrfParam: csrfParam,
530    CSRFProtection: CSRFProtection,
531    delegate: delegate,
532    disableElement: disableElement,
533    enableElement: enableElement,
534    fileInputSelector: fileInputSelector,
535    fire: fire,
536    formElements: formElements,
537    formEnableSelector: formEnableSelector,
538    formDisableSelector: formDisableSelector,
539    formInputClickSelector: formInputClickSelector,
540    formSubmitButtonClick: formSubmitButtonClick,
541    formSubmitSelector: formSubmitSelector,
542    getData: getData,
543    handleDisabledElement: handleDisabledElement,
544    href: href,
545    inputChangeSelector: inputChangeSelector,
546    isCrossDomain: isCrossDomain,
547    linkClickSelector: linkClickSelector,
548    linkDisableSelector: linkDisableSelector,
549    loadCSPNonce: loadCSPNonce,
550    matches: matches,
551    preventInsignificantClick: preventInsignificantClick,
552    refreshCSRFTokens: refreshCSRFTokens,
553    serializeElement: serializeElement,
554    setData: setData,
555    stopEverything: stopEverything
556  };
557  const handleConfirm = handleConfirmWithRails(Rails);
558  Rails.handleConfirm = handleConfirm;
559  const handleMethod = handleMethodWithRails(Rails);
560  Rails.handleMethod = handleMethod;
561  const handleRemote = handleRemoteWithRails(Rails);
562  Rails.handleRemote = handleRemote;
563  const start = function() {
564    if (window._rails_loaded) {
565      throw new Error("rails-ujs has already been loaded!");
566    }
567    window.addEventListener("pageshow", (function() {
568      $(formEnableSelector).forEach((function(el) {
569        if (getData(el, "ujs:disabled")) {
570          enableElement(el);
571        }
572      }));
573      $(linkDisableSelector).forEach((function(el) {
574        if (getData(el, "ujs:disabled")) {
575          enableElement(el);
576        }
577      }));
578    }));
579    delegate(document, linkDisableSelector, "ajax:complete", enableElement);
580    delegate(document, linkDisableSelector, "ajax:stopped", enableElement);
581    delegate(document, buttonDisableSelector, "ajax:complete", enableElement);
582    delegate(document, buttonDisableSelector, "ajax:stopped", enableElement);
583    delegate(document, linkClickSelector, "click", preventInsignificantClick);
584    delegate(document, linkClickSelector, "click", handleDisabledElement);
585    delegate(document, linkClickSelector, "click", handleConfirm);
586    delegate(document, linkClickSelector, "click", disableElement);
587    delegate(document, linkClickSelector, "click", handleRemote);
588    delegate(document, linkClickSelector, "click", handleMethod);
589    delegate(document, buttonClickSelector, "click", preventInsignificantClick);
590    delegate(document, buttonClickSelector, "click", handleDisabledElement);
591    delegate(document, buttonClickSelector, "click", handleConfirm);
592    delegate(document, buttonClickSelector, "click", disableElement);
593    delegate(document, buttonClickSelector, "click", handleRemote);
594    delegate(document, inputChangeSelector, "change", handleDisabledElement);
595    delegate(document, inputChangeSelector, "change", handleConfirm);
596    delegate(document, inputChangeSelector, "change", handleRemote);
597    delegate(document, formSubmitSelector, "submit", handleDisabledElement);
598    delegate(document, formSubmitSelector, "submit", handleConfirm);
599    delegate(document, formSubmitSelector, "submit", handleRemote);
600    delegate(document, formSubmitSelector, "submit", (e => setTimeout((() => disableElement(e)), 13)));
601    delegate(document, formSubmitSelector, "ajax:send", disableElement);
602    delegate(document, formSubmitSelector, "ajax:complete", enableElement);
603    delegate(document, formInputClickSelector, "click", preventInsignificantClick);
604    delegate(document, formInputClickSelector, "click", handleDisabledElement);
605    delegate(document, formInputClickSelector, "click", handleConfirm);
606    delegate(document, formInputClickSelector, "click", formSubmitButtonClick);
607    document.addEventListener("DOMContentLoaded", refreshCSRFTokens);
608    document.addEventListener("DOMContentLoaded", loadCSPNonce);
609    return window._rails_loaded = true;
610  };
611  Rails.start = start;
612  if (typeof jQuery !== "undefined" && jQuery && jQuery.ajax) {
613    if (jQuery.rails) {
614      throw new Error("If you load both jquery_ujs and rails-ujs, use rails-ujs only.");
615    }
616    jQuery.rails = Rails;
617    jQuery.ajaxPrefilter((function(options, originalOptions, xhr) {
618      if (!options.crossDomain) {
619        return CSRFProtection(xhr);
620      }
621    }));
622  }
623  if (typeof exports !== "object" && typeof module === "undefined") {
624    window.Rails = Rails;
625    if (fire(document, "rails:attachBindings")) {
626      start();
627    }
628  }
629  return Rails;
630}));
631/**
632 * Taggle - dependency-less tagging library
633 * @author Sean Coker <[email protected]>
634 * @version 1.15.0 (modified)
635 * @license MIT
636 */
637
638/////////////////////
639// Default options //
640/////////////////////
641
642const BACKSPACE = 8;
643const DELETE = 46;
644const COMMA = 188;
645const TAB = 9;
646const ENTER = 13;
647
648const DEFAULTS = {
649  /**
650   * Class added to the container div when focused
651   * @type {String}
652   */
653  containerFocusClass: 'active',
654
655  /**
656   * Spaces will be removed from the tags by default
657   * @type {Boolean}
658   */
659  trimTags: true,
660
661  /**
662   * Limit the number of tags that can be added
663   * @type {Number}
664   */
665  maxTags: null,
666
667  /**
668   * Placeholder string to be placed in an empty taggle field
669   * @type {String}
670   */
671  placeholder: 'Enter tags...',
672
673  /**
674   * Keycodes that will add a tag
675   * @type {Array}
676   */
677  submitKeys: [COMMA, TAB, ENTER],
678
679  /**
680   * Preserve case of tags being added ie
681   * "tag" is different than "Tag"
682   * @type {Boolean}
683   */
684  preserveCase: false,
685
686  /**
687   * Function hook called when a tag is added
688   * @param  {Event} event Event triggered when tag was added
689   * @param  {String} tag The tag added
690   */
691  onTagAdd: () => {},
692
693  /**
694   * Function hook called when a tag is removed
695   * @param  {Event} event Event triggered when tag was removed
696   * @param  {String} tag The tag removed
697   */
698  onTagRemove: () => {}
699};
700
701//////////////////////
702// Helper functions //
703//////////////////////
704function _clamp(val, min, max) {
705  return Math.min(Math.max(val, min), max);
706}
707
708/**
709 * Taggle ES6 Class - Modern tagging library
710 */
711class Taggle {
712  /**
713   * Constructor
714   * @param {Mixed} el ID of an element or the actual element
715   * @param {Object} options
716   */
717  constructor(el, options) {
718    this.settings = Object.assign({}, DEFAULTS, options);
719    this.measurements = {
720      container: {
721        rect: null,
722        style: null,
723        padding: null
724      }
725    };
726    this.container = el;
727    this.tag = {
728      values: [],
729      elements: []
730    };
731    this.inputContainer = options.inputContainer;
732    this.input = document.createElement('input');
733    this.sizer = document.createElement('div');
734    this.pasting = false;
735    this.placeholder = null;
736
737    if (this.settings.placeholder) {
738      this.placeholder = document.createElement('span');
739    }
740
741    this._backspacePressed = false;
742    this._inputPosition = 0;
743    this._setMeasurements();
744    this._setupTextarea();
745    this._attachEvents();
746  }
747
748  /**
749   * Gets all the layout measurements up front
750   */
751  _setMeasurements() {
752    this.measurements.container.rect = this.container.getBoundingClientRect();
753    const style = window.getComputedStyle(this.container);
754    this.measurements.container.style = style;
755
756    const lpad = parseInt(style.paddingLeft, 10);
757    const rpad = parseInt(style.paddingRight, 10);
758    const lborder = parseInt(style.borderLeftWidth, 10);
759    const rborder = parseInt(style.borderRightWidth, 10);
760
761    this.measurements.container.padding = lpad + rpad + lborder + rborder;
762  }
763
764  /**
765   * Setup the div container for tags to be entered
766   */
767  _setupTextarea() {
768    this.input.type = 'text';
769    // Make sure no left/right padding messes with the input sizing
770    this.input.style.paddingLeft = 0;
771    this.input.style.paddingRight = 0;
772    this.input.className = 'taggle_input';
773    this.input.tabIndex = 1;
774    this.sizer.className = 'taggle_sizer';
775
776    [...this.container.children].filter(child => child.tagName === 'TAG-OPTION').forEach(tagOption => {
777      this.tag.values.push(tagOption.value);
778      this.tag.elements.push(tagOption);
779      this._inputPosition = _clamp(this._inputPosition + 1, 0, this.tag.values.length);
780    });
781
782
783    if (this.placeholder) {
784      this._hidePlaceholder();
785      this.placeholder.classList.add('taggle_placeholder');
786      this.container.appendChild(this.placeholder);
787      this.placeholder.textContent = this.settings.placeholder;
788
789      if (!this.tag.values.length) {
790        this._showPlaceholder();
791      }
792    }
793
794
795    const div = document.createElement('div');
796    div.appendChild(this.input);
797    div.appendChild(this.sizer);
798    this.inputContainer.appendChild(div);
799    const fontSize = window.getComputedStyle(this.input).fontSize;
800    this.sizer.style.fontSize = fontSize;
801  }
802
803  /**
804   * Attaches neccessary events
805   */
806  _attachEvents() {
807    if (this._eventsAttached) {
808      return false;
809    }
810    this._eventsAttached = true;
811
812    this._handleContainerClick = () => this.input.focus();
813    this.container.addEventListener('click', this._handleContainerClick);
814
815    this._handleFocus = this._setFocusStateForContainer.bind(this);
816    this._handleBlur = this._blurEvent.bind(this);
817    this._handleKeydown = this._keydownEvents.bind(this);
818    this._handleKeyup = this._keyupEvents.bind(this);
819
820    this.input.addEventListener('focus', this._handleFocus);
821    this.input.addEventListener('blur', this._handleBlur);
822    this.input.addEventListener('keydown', this._handleKeydown);
823    this.input.addEventListener('keyup', this._handleKeyup);
824
825    return true;
826  }
827
828  _detachEvents() {
829    if (!this._eventsAttached) {
830      return false;
831    }
832    this._eventsAttached = false;
833
834    this.container.removeEventListener('click', this._handleContainerClick);
835    this.input.removeEventListener('focus', this._handleFocus);
836    this.input.removeEventListener('blur', this._handleBlur);
837    this.input.removeEventListener('keydown', this._handleKeydown);
838    this.input.removeEventListener('keyup', this._handleKeyup);
839
840    return true;
841  }
842
843  /**
844   * Returns whether or not the specified tag text can be added
845   * @param {Event} e event causing the potentially added tag
846   * @param {String} text tag value
847   * @return {Boolean}
848   */
849  _canAdd(e, text) {
850    if (!text) {
851      return false;
852    }
853    const limit = this.settings.maxTags;
854    if (limit !== null && limit <= this.getTagValues().length) {
855      return false;
856    }
857
858    // Check for duplicates
859    return this.tag.values.indexOf(text) === -1;
860  }
861
862  /**
863   * Appends tag with its corresponding input to the list
864   * @param {Event} e
865   * @param {String} text
866   * @param {Number} index
867   */
868  _add(e, text, index) {
869    let values = text || '';
870    const delimiter = ',';
871
872    if (typeof text !== 'string') {
873      values = this.input.value;
874
875      if (this.settings.trimTags) {
876        if (values[0] === delimiter) {
877          values = values.replace(delimiter, '');
878        }
879        values = values.trim();
880      }
881    }
882
883    values.split(delimiter).map(val => {
884      if (this.settings.trimTags) {
885        val = val.trim();
886      }
887      return this._formatTag(val);
888    }
888).forEach(val => {
889      if (!this._canAdd(e, val)) {
890        return;
891      }
892
893      const currentTagLength = this.tag.values.length;
894      const tagIndex = _clamp(index || currentTagLength, 0, currentTagLength);
895      const tagOption = this._createTag(val, tagIndex);
896      this.container.append(tagOption);
897
898      val = this.tag.values[tagIndex];
899
900      this.settings.onTagAdd(e, val);
901
902      this.input.value = '';
903      this._setMeasurements();
904      this._setInputWidth();
905      this._setFocusStateForContainer();
906    });
907  }
908
909  /**
910   * Removes last tag if it has already been probed
911   * @param {Event} e
912   */
913  _checkPrevOrNextTag(e) {
914    const taggles = this.container.querySelectorAll('tag-option');
915    const prevTagIndex = _clamp(this._inputPosition - 1, 0, taggles.length - 1);
916    const nextTagIndex = _clamp(this._inputPosition, 0, taggles.length - 1);
917    let index = prevTagIndex;
918
919    if (e.keyCode === DELETE) {
920      index = nextTagIndex;
921    }
922
923    const targetTaggle = taggles[index];
924    const hotClass = 'taggle_hot';
925    const isDeleteOrBackspace = [BACKSPACE, DELETE].includes(e.keyCode);
926
927    // prevent holding backspace from deleting all tags
928    if (this.input.value === '' && isDeleteOrBackspace && !this._backspacePressed) {
929      if (targetTaggle.classList.contains(hotClass)) {
930        this._backspacePressed = true;
931        this._remove(targetTaggle, e);
932        this._setMeasurements();
933        this._setInputWidth();
934        this._setFocusStateForContainer();
935      }
936      else {
937        targetTaggle.classList.add(hotClass);
938      }
939    }
940    else if (targetTaggle.classList.contains(hotClass)) {
941      targetTaggle.classList.remove(hotClass);
942    }
943  }
944
945  /**
946   * Setter for the hidden input.
947   * @param {Number} width
948   */
949  _setInputWidth() {
950    const width = this.sizer.getBoundingClientRect().width;
951    const max = this.measurements.container.rect.width - this.measurements.container.padding;
952    const size = parseInt(this.sizer.style.fontSize, 10);
953
954    // 1.5 just seems to be a good multiplier here
955    const newWidth = Math.round(_clamp(width + (size * 1.5), 10, max));
956
957    this.input.style.width = `${newWidth}px`;
958  }
959
960  /**
961   * Handles focus state of div container.
962   */
963  _setFocusStateForContainer() {
964    this._setMeasurements();
965    this._setInputWidth();
966
967    if (!this.container.classList.contains(this.settings.containerFocusClass)) {
968      this.container.classList.add(this.settings.containerFocusClass);
969    }
970
971    this._hidePlaceholder();
972  }
973
974  /**
975   * Runs all the events that need to happen on a blur
976   * @param {Event} e
977   */
978  _blurEvent(e) {
979    if (this.container.classList.contains(this.settings.containerFocusClass)) {
980      this.container.classList.remove(this.settings.containerFocusClass);
981    }
982
983    if (!this.tag.values.length && !this.input.value) {
984      this._showPlaceholder();
985    }
986  }
987
988  /**
989   * Runs all the events that need to run on keydown
990   * @param {Event} e
991   */
992  _keydownEvents(e) {
993    const key = e.keyCode;
994    this.pasting = false;
995
996    this._setInputWidth();
997
998    if (key === 86 && e.metaKey) {
999      this.pasting = true;
1000    }
1001
1002    if (this.settings.submitKeys.includes(key) && this.input.value !== '') {
1003      this._confirmValidTagEvent(e);
1004      return;
1005    }
1006
1007    if (this.tag.values.length) {
1008      this._checkPrevOrNextTag(e);
1009    }
1010  }
1011
1012  /**
1013   * Runs all the events that need to run on keyup
1014   * @param {Event} e
1015   */
1016  _keyupEvents(e) {
1017    this._backspacePressed = false;
1018
1019    this.sizer.textContent = this.input.value;
1020
1021    // If we break to a new line because the text is too long
1022    // and decide to delete everything, we should resize the input
1023    // so it falls back inline
1024    if (!this.input.value) {
1025      this._setInputWidth();
1026    }
1027
1028    if (this.pasting && this.input.value !== '') {
1029      this._add(e);
1030      this.pasting = false;
1031    }
1032  }
1033
1034  /**
1035   * Confirms the inputted value to be converted to a tag
1036   * @param {Event} e
1037   */
1038  _confirmValidTagEvent(e) {
1039    // prevents from jumping out of textarea
1040    e.preventDefault();
1041
1042    this._add(e, null, this._inputPosition);
1043  }
1044
1045  _createTag(text, index) {
1046    const tagOption = document.createElement('tag-option');
1047
1048    text = this._formatTag(text);
1049    tagOption.textContent = text;
1050    tagOption.setAttribute('value', text);
1051
1052    this.tag.values.splice(index, 0, text);
1053    this.tag.elements.splice(index, 0, tagOption);
1054    this._inputPosition = _clamp(this._inputPosition + 1, 0, this.tag.values.length);
1055
1056    return tagOption;
1057  }
1058
1059  _showPlaceholder() {
1060    if (this.placeholder) {
1061      this.placeholder.style.opacity = 1;
1062      this.placeholder.setAttribute('aria-hidden', 'false');
1063    }
1064  }
1065
1066  _hidePlaceholder() {
1067    if (this.placeholder) {
1068      this.placeholder.style.opacity = 0;
1069      this.placeholder.setAttribute('aria-hidden', 'true');
1070    }
1071  }
1072
1073  /**
1074   * Removes tag from the tags collection
1075   * @param {HTMLElement} tagOption List item to remove
1076   * @param {Event} e
1077   */
1078  _remove(tagOption, e) {
1079    const index = this.tag.elements.indexOf(tagOption);
1080    if (index === -1) return;
1081
1082    const text = this.tag.values[index];
1083
1084    tagOption.remove();
1085    this.tag.elements.splice(index, 1);
1086    this.tag.values.splice(index, 1);
1087    this.settings.onTagRemove(e, text);
1088
1089    if (index < this._inputPosition) {
1090      this._inputPosition = _clamp(this._inputPosition - 1, 0, this.tag.values.length);
1091    }
1092
1093    this._setFocusStateForContainer();
1094  }
1095
1096  /**
1097   * Format the text for a tag
1098   * @param {String} text Tag text
1099   * @return {String}
1100   */
1101  _formatTag(text) {
1102    return this.settings.preserveCase ? text : text.toLowerCase();
1103  }
1104
1105  // @todo
1106  // @deprecated use getTags().values
1107  getTagValues() {
1108    return [...this.tag.values];
1109  }
1110
1111  getInput() {
1112    return this.input;
1113  }
1114
1115  add(text, index) {
1116    const isArr = Array.isArray(text);
1117
1118    if (isArr) {
1119      text.forEach((tag, i) => {
1120        if (typeof tag === 'string') {
1121          this._add(null, tag, index ? index + i : index);
1122        }
1123      });
1124    }
1125    else {
1126      this._add(null, text, index);
1127    }
1128
1129    return this;
1130  }
1131
1132  remove(text) {
1133    const index = this.tag.values.indexOf(text);
1134    if (index > -1) {
1135      this._remove(this.tag.elements[index]);
1136    }
1137    return this;
1138  }
1139
1140  removeAll() {
1141    [...this.tag.elements].forEach(element => this._remove(element));
1142    this._showPlaceholder();
1143    return this;
1144  }
1145
1146  _setDisabledState(disabled) {
1147    const elements = [
1148      ...this.container.querySelectorAll('button'),
1149      ...this.container.querySelectorAll('input')
1150    ];
1151
1152    elements.forEach((el) => {
1153      if (disabled) {
1154        el.setAttribute('disabled', '');
1155      } else {
1156        el.removeAttribute('disabled');
1157      }
1158    });
1159
1160    return this;
1161  }
1162
1163  enable() {
1164    return this._setDisabledState(false);
1165  }
1166
1167  disable() {
1168    return this._setDisabledState(true);
1169  }
1170
1171  destroy() {
1172    this._detachEvents();
1173  }
1174}
1175
1176/**
1177 * Copyright (c) 2016 Denis Taran
1178 *
1179 * Homepage: https://smartscheduling.com/en/documentation/autocomplete
1180 * Source: https://github.com/denis-taran/autocomplete
1181 *
1182 * MIT License
1183 */
1184function autocomplete(settings) {
1185    // just an alias to minimize JS file size
1186    var doc = document;
1187    var container = settings.container || doc.createElement('div');
1188    var preventSubmit = settings.preventSubmit || 0 /* Never */;
1189    container.id = container.id || 'autocomplete-' + uid();
1190    var containerStyle = container.style;
1191    var debounceWaitMs = settings.debounceWaitMs || 0;
1192    var disableAutoSelect = settings.disableAutoSelect || false;
1193    var customContainerParent = container.parentElement;
1194    var items = [];
1195    var inputValue = '';
1196    var minLen = 2;
1197    var showOnFocus = settings.showOnFocus;
1198    var selected;
1199    var fetchCounter = 0;
1200    var debounceTimer;
1201    var destroyed = false;
1202    // Fixes #104: autocomplete selection is broken on Firefox for Android
1203    var suppressAutocomplete = false;
1204    if (settings.minLength !== undefined) {
1205        minLen = settings.minLength;
1206    }
1207    if (!settings.input) {
1208        throw new Error('input undefined');
1209    }
1210    var input = settings.input;
1211    container.className = [container.className, 'autocomplete', settings.className || ''].join(' ').trim();
1212    container.setAttribute('role', 'listbox');
1213    input.setAttribute('role', 'combobox');
1214    input.setAttribute('aria-expanded', 'false');
1215    input.setAttribute('aria-autocomplete', 'list');
1216    input.setAttribute('aria-controls', container.id);
1217    input.setAttribute('aria-owns', container.id);
1218    input.setAttribute('aria-activedescendant', '');
1219    input.setAttribute('aria-haspopup', 'listbox');
1220    // IOS implementation for fixed positioning has many bugs, so we will use absolute positioning
1221    containerStyle.position = 'absolute';
1222    /**
1223     * Generate a very complex textual ID that greatly reduces the chance of a collision with another ID or text.
1224     */
1225    function uid() {
1226        return Date.now().toString(36) + Math.random().toString(36).substring(2);
1227    }
1228    /**
1229     * Detach the container from DOM
1230     */
1231    function detach() {
1232        var parent = container.parentNode;
1233        if (parent) {
1234            parent.removeChild(container);
1235        }
1236    }
1237    /**
1238     * Clear debouncing timer if assigned
1239     */
1240    function clearDebounceTimer() {
1241        if (debounceTimer) {
1242            window.clearTimeout(debounceTimer);
1243        }
1244    }
1245    /**
1246     * Attach the container to DOM
1247     */
1248    function attach() {
1249        if (!container.parentNode) {
1250            (customContainerParent || doc.body).appendChild(container);
1251        }
1252    }
1253    /**
1254     * Check if container for autocomplete is displayed
1255     */
1256    function containerDisplayed() {
1257        return !!container.parentNode;
1258    }
1259    /**
1260     * Clear autocomplete state and hide container
1261     */
1262    function clear() {
1263        // prevent the update call if there are pending AJAX requests
1264        fetchCounter++;
1265        items = [];
1266        inputValue = '';
1267        selected = undefined;
1268        input.setAttribute('aria-activedescendant', '');
1269        input.setAttribute('aria-expanded', 'false');
1270        detach();
1271    }
1272    /**
1273     * Update autocomplete position
1274     */
1275    function updatePosition() {
1276        if (!containerDisplayed()) {
1277            return;
1278        }
1279        input.setAttribute('aria-expanded', 'true');
1280        containerStyle.height = 'auto';
1281        containerStyle.width = input.offsetWidth + 'px';
1282        var maxHeight = 0;
1283        var inputRect;
1284        function calc() {
1285            var docEl = doc.documentElement;
1286            var clientTop = docEl.clientTop || doc.body.clientTop || 0;
1287            var clientLeft = docEl.clientLeft || doc.body.clientLeft || 0;
1288            var scrollTop = window.pageYOffset || docEl.scrollTop;
1289            var scrollLeft = window.pageXOffset || docEl.scrollLeft;
1290            inputRect = input.getBoundingClientRect();
1291            var top = inputRect.top + input.offsetHeight + scrollTop - clientTop;
1292            var left = inputRect.left + scrollLeft - clientLeft;
1293            containerStyle.top = top + 'px';
1294            containerStyle.left = left + 'px';
1295            maxHeight = window.innerHeight - (inputRect.top + input.offsetHeight);
1296            if (maxHeight < 0) {
1297                maxHeight = 0;
1298            }
1299            containerStyle.top = top + 'px';
1300            containerStyle.bottom = '';
1301            containerStyle.left = left + 'px';
1302            containerStyle.maxHeight = maxHeight + 'px';
1303        }
1304        // the calc method must be called twice, otherwise the calculation may be wrong on resize event (chrome browser)
1305        calc();
1306        calc();
1307        if (settings.customize && inputRect) {
1308            settings.customize(input, inputRect, container, maxHeight);
1309        }
1310    }
1311    /**
1312     * Redraw the autocomplete div element with suggestions
1313     */
1314    function update() {
1315        container.textContent = '';
1316        input.setAttribute('aria-activedescendant', '');
1317        // function for rendering autocomplete suggestions
1318        var render = function (item, _, __) {
1319            var itemElement = doc.createElement('div');
1320            itemElement.textContent = item.label || '';
1321            return itemElement;
1322        };
1323        if (settings.render) {
1324            render = settings.render;
1325        }
1326        // function to render autocomplete groups
1327        var renderGroup = function (groupName, _) {
1328            var groupDiv = doc.createElement('div');
1329            groupDiv.textContent = groupName;
1330            return groupDiv;
1331        };
1332        if (settings.renderGroup) {
1333            renderGroup = settings.renderGroup;
1334        }
1335        var fragment = doc.createDocumentFragment();
1336        var prevGroup = uid();
1337        items.forEach(function (item, index) {
1338            if (item.group && item.group !== prevGroup) {
1339                prevGroup = item.group;
1340                var groupDiv = renderGroup(item.group, inputValue);
1341                if (groupDiv) {
1342                    groupDiv.className += ' group';
1343                    fragment.appendChild(groupDiv);
1344                }
1345            }
1346            var div = render(item, inputValue, index);
1347            if (div) {
1348                div.id = container.id + "_" + index;
1349                div.setAttribute('role', 'option');
1350                div.addEventListener('click', function (ev) {
1351                    suppressAutocomplete = true;
1352                    try {
1353                        settings.onSelect(item, input);
1354                    }
1355                    finally {
1356                        suppressAutocomplete = false;
1357                    }
1358                    clear();
1359                    ev.preventDefault();
1360                    ev.stopPropagation();
1361                });
1362                if (item === selected) {
1363                    div.className += ' selected';
1364                    div.setAttribute('aria-selected', 'true');
1365                    input.setAttribute('aria-activedescendant', div.id);
1366                }
1367                fragment.appendChild(div);
1368            }
1369        });
1370        container.appendChild(fragment);
1371        if (items.length < 1) {
1372            if (settings.emptyMsg) {
1373                var empty = doc.createElement('div');
1374                empty.id = container.id + "_" + uid();
1375                empty.className = 'empty';
1376                empty.textContent = settings.emptyMsg;
1377                container.appendChild(empty);
1378                input.setAttribute('aria-activedescendant', empty.id);
1379            }
1380            else {
1381                clear();
1382                return;
1383            }
1384        }
1385        attach();
1386        updatePosition();
1387        updateScroll();
1388    }
1389    function updateIfDisplayed() {
1390        if (containerDisplayed()) {
1391            update();
1392        }
1393    }
1394    function resizeEventHandler() {
1395        updateIfDisplayed();
1396    }
1397    function scrollEventHandler(e) {
1398        if (e.target !== container) {
1399            updateIfDisplayed();
1400        }
1401        else {
1402            e.preventDefault();
1403        }
1404    }
1405    function inputEventHandler() {
1406        if (!suppressAutocomplete) {
1407            fetch(0 /* Keyboard */);
1408        }
1409    }
1410    /**
1411     * Automatically move scroll bar if selected item is not visible
1412     */
1413    function updateScroll() {
1414        var elements = container.getElementsByClassName('selected');
1415        if (elements.length > 0) {
1416            var element = elements[0];
1417            // make group visible
1418            var previous = element.previousElementSibling;
1419            if (previous && previous.className.indexOf('group') !== -1 && !previous.previousElementSibling) {
1420                element = previous;
1421            }
1422            if (element.offsetTop < container.scrollTop) {
1423                container.scrollTop = element.offsetTop;
1424            }
1425            else {
1426                var selectBottom = element.offsetTop + element.offsetHeight;
1427                var containerBottom = container.scrollTop + container.offsetHeight;
1428                if (selectBottom > containerBottom) {
1429                    container.scrollTop += selectBottom - containerBottom;
1430                }
1431            }
1432        }
1433    }
1434    function selectPreviousSuggestion() {
1435        var index = items.indexOf(selected);
1436        selected = index === -1
1437            ? undefined
1438            : items[(index + items.length - 1) % items.length];
1439        updateSelectedSuggestion(index);
1440    }
1441    function selectNextSuggestion() {
1442        var index = items.indexOf(selected);
1443        selected = items.length < 1
1444            ? undefined
1445            : index === -1
1446                ? items[0]
1447                : items[(index + 1) % items.length];
1448        updateSelectedSuggestion(index);
1449    }
1450    function updateSelectedSuggestion(index) {
1451        if (items.length > 0) {
1452            unselectSuggestion(index);
1453            selectSuggestion(items.indexOf(selected));
1454            updateScroll();
1455        }
1456    }
1457    function selectSuggestion(index) {
1458        var element = doc.getElementById(container.id + "_" + index);
1459        if (element) {
1460            element.classList.add('selected');
1461            element.setAttribute('aria-selected', 'true');
1462            input.setAttribute('aria-activedescendant', element.i
1462d);
1463        }
1464    }
1465    function unselectSuggestion(index) {
1466        var element = doc.getElementById(container.id + "_" + index);
1467        if (element) {
1468            element.classList.remove('selected');
1469            element.removeAttribute('aria-selected');
1470            input.removeAttribute('aria-activedescendant');
1471        }
1472    }
1473    function handleArrowAndEscapeKeys(ev, key) {
1474        var containerIsDisplayed = containerDisplayed();
1475        if (key === 'Escape') {
1476            clear();
1477        }
1478        else {
1479            if (!containerIsDisplayed || items.length < 1) {
1480                return;
1481            }
1482            key === 'ArrowUp'
1483                ? selectPreviousSuggestion()
1484                : selectNextSuggestion();
1485        }
1486        ev.preventDefault();
1487        if (containerIsDisplayed) {
1488            ev.stopPropagation();
1489        }
1490    }
1491    function handleEnterKey(ev) {
1492        if (selected) {
1493            if (preventSubmit === 2 /* OnSelect */) {
1494                ev.preventDefault();
1495            }
1496            suppressAutocomplete = true;
1497            try {
1498                settings.onSelect(selected, input);
1499            }
1500            finally {
1501                suppressAutocomplete = false;
1502            }
1503            clear();
1504        }
1505        if (preventSubmit === 1 /* Always */) {
1506            ev.preventDefault();
1507        }
1508    }
1509    function keydownEventHandler(ev) {
1510        var key = ev.key;
1511        switch (key) {
1512            case 'ArrowUp':
1513            case 'ArrowDown':
1514            case 'Escape':
1515                handleArrowAndEscapeKeys(ev, key);
1516                break;
1517            case 'Enter':
1518                handleEnterKey(ev);
1519                break;
1520        }
1521    }
1522    function focusEventHandler() {
1523        if (showOnFocus) {
1524            fetch(1 /* Focus */);
1525        }
1526    }
1527    function fetch(trigger) {
1528        if (input.value.length >= minLen || trigger === 1 /* Focus */) {
1529            clearDebounceTimer();
1530            debounceTimer = window.setTimeout(function () { return startFetch(input.value, trigger, input.selectionStart || 0); }, trigger === 0 /* Keyboard */ || trigger === 2 /* Mouse */ ? debounceWaitMs : 0);
1531        }
1532        else {
1533            clear();
1534        }
1535    }
1536    function startFetch(inputText, trigger, cursorPos) {
1537        if (destroyed)
1538            return;
1539        var savedFetchCounter = ++fetchCounter;
1540        settings.fetch(inputText, function (elements) {
1541            if (fetchCounter === savedFetchCounter && elements) {
1542                items = elements;
1543                inputValue = inputText;
1544                selected = (items.length < 1 || disableAutoSelect) ? undefined : items[0];
1545                update();
1546            }
1547        }, trigger, cursorPos);
1548    }
1549    function keyupEventHandler(e) {
1550        if (settings.keyup) {
1551            settings.keyup({
1552                event: e,
1553                fetch: function () { return fetch(0 /* Keyboard */); }
1554            });
1555            return;
1556        }
1557        if (!containerDisplayed() && e.key === 'ArrowDown') {
1558            fetch(0 /* Keyboard */);
1559        }
1560    }
1561    function clickEventHandler(e) {
1562        settings.click && settings.click({
1563            event: e,
1564            fetch: function () { return fetch(2 /* Mouse */); }
1565        });
1566    }
1567    function blurEventHandler() {
1568        // when an item is selected by mouse click, the blur event will be initiated before the click event and remove DOM elements,
1569        // so that the click event will never be triggered. In order to avoid this issue, DOM removal should be delayed.
1570        setTimeout(function () {
1571            if (doc.activeElement !== input) {
1572                clear();
1573            }
1574        }, 200);
1575    }
1576    function manualFetch() {
1577        startFetch(input.value, 3 /* Manual */, input.selectionStart || 0);
1578    }
1579    /**
1580     * Fixes #26: on long clicks focus will be lost and onSelect method will not be called
1581     */
1582    container.addEventListener('mousedown', function (evt) {
1583        evt.stopPropagation();
1584        evt.preventDefault();
1585    });
1586    /**
1587     * Fixes #30: autocomplete closes when scrollbar is clicked in IE
1588     * See: https://stackoverflow.com/a/9210267/13172349
1589     */
1590    container.addEventListener('focus', function () { return input.focus(); });
1591    // If the custom autocomplete container is already appended to the DOM during widget initialization, detach it.
1592    detach();
1593    /**
1594     * This function will remove DOM elements and clear event handlers
1595     */
1596    function destroy() {
1597        input.removeEventListener('focus', focusEventHandler);
1598        input.removeEventListener('keyup', keyupEventHandler);
1599        input.removeEventListener('click', clickEventHandler);
1600        input.removeEventListener('keydown', keydownEventHandler);
1601        input.removeEventListener('input', inputEventHandler);
1602        input.removeEventListener('blur', blurEventHandler);
1603        window.removeEventListener('resize', resizeEventHandler);
1604        doc.removeEventListener('scroll', scrollEventHandler, true);
1605        input.removeAttribute('role');
1606        input.removeAttribute('aria-expanded');
1607        input.removeAttribute('aria-autocomplete');
1608        input.removeAttribute('aria-controls');
1609        input.removeAttribute('aria-activedescendant');
1610        input.removeAttribute('aria-owns');
1611        input.removeAttribute('aria-haspopup');
1612        clearDebounceTimer();
1613        clear();
1614        destroyed = true;
1615    }
1616    // setup event handlers
1617    input.addEventListener('keyup', keyupEventHandler);
1618    input.addEventListener('click', clickEventHandler);
1619    input.addEventListener('keydown', keydownEventHandler);
1620    input.addEventListener('input', inputEventHandler);
1621    input.addEventListener('blur', blurEventHandler);
1622    input.addEventListener('focus', focusEventHandler);
1623    window.addEventListener('resize', resizeEventHandler);
1624    doc.addEventListener('scroll', scrollEventHandler, true);
1625    return {
1626        destroy: destroy,
1627        fetch: manualFetch
1628    };
1629}
1630
1631const tagOptionStyleSheet = new CSSStyleSheet();
1632tagOptionStyleSheet.replaceSync(`
1633  :host {
1634    background: var(--tag-option-bg, #588a00);
1635    padding: 3px 10px 3px 10px !important;
1636    margin-right: 4px !important;
1637    margin-bottom: 2px !important;
1638    display: inline-flex;
1639    align-items: center;
1640    float: none;
1641    font-size: 14px;
1642    line-height: 1;
1643    min-height: 32px;
1644    color: var(--tag-option-color, #fff);
1645    text-transform: none;
1646    border-radius: 3px;
1647    position: relative;
1648    cursor: pointer;
1649  }
1650  button {
1651    z-index: 1;
1652    border: none;
1653    background: none;
1654    font-size: 20px;
1655    display: inline-block;
1656    color: var(--tag-option-button-color, rgba(255, 255, 255, 0.6));
1657    right: 10px;
1658    height: 100%;
1659    cursor: pointer;
1660  }
1661`);
1662
1663const inputTagStyleSheet = new CSSStyleSheet();
1664inputTagStyleSheet.replaceSync(`
1665  :host { display: block; }
1666  :host *{
1667    position: relative;
1668    box-sizing: border-box;
1669    margin: 0;
1670    padding: 0;
1671  }
1672  #container {
1673    background: var(--container-bg, rgba(255, 255, 255, 0.8));
1674    padding: 6px 6px 3px;
1675    max-height: none;
1676    display: flex;
1677    margin: 0;
1678    flex-wrap: wrap;
1679    align-items: flex-start;
1680    min-height: 48px;
1681    line-height: 48px;
1682    width: 100%;
1683    border: 1px solid var(--container-border, #d0d0d0);
1684    border-left-width: var(--container-border-left-width, 1px);
1685    border-left-color: var(--container-border-left-color, #d0d0d0);
1686    outline: 1px solid transparent;
1687    box-shadow: var(--container-shadow, #ccc) 0 1px 4px 0 inset;
1688    border-radius: 2px;
1689    cursor: text;
1690    color: #333;
1691    list-style: none;
1692    padding-right: 32px;
1693  }
1694  input {
1695    display: block;
1696    height: 38px;
1697    float: none;
1698    margin: 0;
1699    padding-left: 10px !important;
1700    padding-right: 30px !important;
1701    width: auto !important;
1702    min-width: 70px;
1703    font-size: 14px;
1704    width: 100%;
1705    line-height: 2;
1706    padding: 0 0 0 10px;
1707    border: 1px dashed var(--input-border, #d0d0d0);
1708    outline: 1px solid transparent;
1709    background: var(--input-bg, #fff);
1710    box-shadow: none;
1711    border-radius: 2px;
1712    cursor: text;
1713    color: var(--input-color, #333);
1714  }
1715  button {
1716    width: 38px;
1717    text-align: center;
1718    line-height: 36px;
1719    border: 1px solid var(--button-border, #e0e0e0);
1720    font-size: 20px;
1721    color: var(--button-color, #666);
1722    position: absolute !important;
1723    z-index: 10;
1724    right: 0px;
1725    top: 0;
1726    font-weight: 400;
1727    cursor: pointer;
1728    background: none;
1729  }
1730  .taggle_sizer{
1731    padding: 0;
1732    margin: 0;
1733    position: absolute;
1734    top: -500px;
1735    z-index: -1;
1736    visibility: hidden;
1737  }
1738  .ui-autocomplete{
1739    position: static !important;
1740    width: 100% !important;
1741    margin-top: 2px;
1742  }
1743  .ui-menu{
1744    margin: 0;
1745    padding: 6px;
1746    box-shadow: var(--menu-shadow, #ccc) 0 1px 6px;
1747    z-index: 2;
1748    display: flex;
1749    flex-wrap: wrap;
1750    background: var(--menu-bg, #fff);
1751    list-style: none;
1752    font-size: 14px;
1753    min-width: 200px;
1754  }
1755  .ui-menu .ui-menu-item{
1756    display: inline-block;
1757    margin: 0 0 2px;
1758    line-height: 30px;
1759    border: none;
1760    padding: 0 10px;
1761    text-indent: 0;
1762    border-radius: 2px;
1763    width: auto;
1764    cursor: pointer;
1765    color: var(--menu-color, #555);
1766  }
1767  .ui-menu .ui-menu-item::before{ display: none; }
1768  .ui-menu .ui-menu-item:hover{ background: var(--menu-hover, #e0e0e0); }
1769  .ui-state-active{
1770    padding: 0;
1771    border: none;
1772    background: none;
1773    color: inherit;
1774  }
1775`);
1776
1777
1778class TagOption extends HTMLElement {
1779  constructor() {
1780    super();
1781    this._shadowRoot = this.attachShadow({ mode: "open" });
1782    this._shadowRoot.adoptedStyleSheets = [tagOptionStyleSheet];
1783  }
1784
1785  connectedCallback() {
1786    this._shadowRoot.replaceChildren();
1787    this._shadowRoot.appendChild(document.createElement("slot"));
1788
1789    this.buttonTarget = document.createElement("button");
1790    this.buttonTarget.type = "button";
1791    this.buttonTarget.textContent = "×";
1792    this.buttonTarget.onclick = event => {
1793      this.parentNode._taggle._remove(this, event);
1794    };
1795    this._shadowRoot.appendChild(this.buttonTarget);
1796  }
1797
1798  get value() {
1799    return this.getAttribute("value") || this.innerText
1800  }
1801
1802  get label() {
1803    return this.innerText
1804  }
1805}
1806customElements.define("tag-option", TagOption);
1807
1808
1809class InputTag extends HTMLElement {
1810  static get formAssociated() {
1811    return true;
1812  }
1813
1814  static get observedAttributes() {
1815    return ['name', 'multiple', 'required', 'list'];
1816  }
1817
1818  constructor() {
1819    super();
1820    this._internals = this.attachInternals();
1821    this._shadowRoot = this.attachShadow({ mode: "open" });
1822    this._shadowRoot.adoptedStyleSheets = [inputTagStyleSheet];
1823
1824    this.observer = new MutationObserver(mutations => {
1825      let needsTagOptionsUpdate = false;
1826      let needsAutocompleteUpdate = false;
1827
1828      for (const mutation of mutations) {
1829        if (mutation.type === 'childList') {
1830          const addedRemovedNodes = [...mutation.addedNodes, ...mutation.removedNodes];
1831          if (addedRemovedNodes.some(node => node.tagName === 'TAG-OPTION')) {
1832            needsTagOptionsUpdate = true;
1833          }
1834          if (addedRemovedNodes.some(node => node.tagName === 'DATALIST')) {
1835            needsAutocompleteUpdate = true;
1836          }
1837        } else if (mutation.type === 'attributes') {
1838          // Handle attribute changes on tag-option elements
1839          if (mutation.target !== this && mutation.target.tagName === 'TAG-OPTION') {
1840            needsTagOptionsUpdate = true;
1841          }
1842        }
1843      }
1844
1845      if (needsTagOptionsUpdate || needsAutocompleteUpdate) {
1846        this.unobserve();
1847        if (needsTagOptionsUpdate) {
1848          this.processTagOptions();
1849        }
1850        if (needsAutocompleteUpdate && this.initialized) {
1851          this.setupAutocomplete();
1852        }
1853        this.observe();
1854      }
1855    });
1856  }
1857
1858  unobserve() {
1859    this.observer.disconnect();
1860  }
1861
1862  observe() {
1863    this.observer.observe(this, {
1864      childList: true,
1865      attributes: true,
1866      subtree: true,
1867      attributeFilter: ["value"],
1868    });
1869  }
1870
1871  processTagOptions() {
1872    if(!this._taggle || !this._taggle.tag) return
1873    let tagOptions = Array.from(this.children).filter(e => e.tagName === 'TAG-OPTION');
1874    let values = tagOptions.map(e => e.value).filter(value => value !== null && value !== undefined);
1875
1876    // Enforce maxTags constraint for single mode
1877    if (!this.multiple && values.length > 1) {
1878      // Remove excess tag-options from DOM (keep only the first one)
1879      tagOptions.slice(1).forEach(el => el.remove());
1880      tagOptions = tagOptions.slice(0, 1);
1881      values = values.slice(0, 1);
1882    }
1883
1884    this._taggle.tag.elements = tagOptions;
1885    this._taggle.tag.values = values;
1886    this._inputPosition = this._taggle.tag.values.length;
1887
1888    // Update the taggle display elements to match the current values
1889    const taggleElements = this._taggle.tag.elements;
1890    taggleElements.forEach((element, index) => {
1891      if (element && element.setAttribute) {
1892        element.setAttribute('data-value', values[index]);
1893      }
1894    });
1895
1896    // Update internal value to match
1897    this.updateValue();
1898
1899    // Ensure input visibility is updated when tags change via DOM
1900    this.updateInputVisibility();
1901  }
1902
1903  get form() {
1904    return this._internals.form;
1905  }
1906
1907  _setFormValue(values) {
1908    this._internals.value = values;
1909
1910    const formData = new FormData();
1911    values.forEach(value => formData.append(this.name, value));
1912    // Always append empty string when no values so server knows to clear the field
1913    // (like Rails multiple checkboxes which prepend an empty hidden field)
1914    if (values.length === 0) {
1915      formData.append(this.name, "");
1916    }
1917    this._internals.setFormValue(formData);
1918  }
1919
1920  get name() {
1921    return this.getAttribute("name");
1922  }
1923
1924  get multiple() {
1925    return this.hasAttribute('multiple');
1926  }
1927
1928  get value() {
1929    const internalValue = this._internals.value;
1930    if (this.multiple) {
1931      return internalValue; // Return array for multiple mode
1932    } else {
1933      return internalValue.length > 0 ? internalValue[0] : ''; // Return string for single mode
1934    }
1935  }
1936
1937  set value(input) {
1938    // Convert input to array format for internal storage
1939    let values;
1940    if (Array.isArray(input)) {
1941      values = input;
1942    } else if (typeof input === 'string') {
1943      values = input === '' ? [] : [input];
1944    } else {
1945      values = [];
1946    }
1947
1948    const oldValues = this._internals.value;
1949    this._setFormValue(values);
1950
1951    // Update taggle to match the new values
1952    if (this._taggle && this.initialized) {
1953      this.suppressEvents = true; // Prevent infinite loops
1954      this._taggle.removeAll();
1955      if (values.length > 0) {
1956        this._taggle.add(values);
1957      }
1958      this.suppressEvents = false;
1959    }
1960
1961    if(this.initialized && !this.suppressEvents && JSON.stringify(oldValues) !== JSON.stringify(values)) {
1962      this.dispatchEvent(new CustomEvent("change", {
1963        bubbles: true,
1964        composed: true,
1965      }));
1966    }
1967  }
1968
1969  reset() {
1970    this._taggle.removeAll();
1971    this._taggleInputTarget.value = '';
1972    this._updateButtonContent();
1973  }
1974
1975  get options() {
1976    const datalistId = this.getAttribute("list");
1977    if(datalistId) {
1978      const datalist = document.getElementById(datalistId);
1979      if(datalist) {
1980        return [...datalist.options].map(option => option.value).filter(value => value !== null && value !== undefined)
1981      }
1982    }
1983
1984    // Fall back to nested datalist
1985    const nestedDatalist = this.querySelector('datalist');
1986    if(nestedDatalist) {
1987      return [...nestedDatalist.options].map(option => option.hasAttribute('value') ? option.value : option.textContent).filter(value => value !== null && value !== undefined)
1988    }
1989
1990    return []
1991  }
1992
1993  _getOptionsWithLabels() {
1994    const datalistId = this.getAttribute("list");
1995    if(datalistId) {
1996      const datalist = document.getElementById(datalistId);
1997      if(datalist) {
1998        return [...datalist.options].map(option => ({
1999          value: option.value,
2000          label: option.textContent || option.value
2001        })).filter(item => item.value !== null && item.value !== undefined)
2002      }
2003    }
2004
2005    // Fall back to nested datalist
2006    const nestedDatalist = this.querySelector('datalist');
2007    if(nestedDatalist) {
2008      return [...nestedDatalist.options].map(option => ({
2009        value: option.hasAttribute('value') ? option.value : option.textContent,
2010        label: option.textContent || option.value
2011      })).filter(item => item.value !== null && item.value !== undefined)
2012    }
2013
2014    return []
2015  }
2016
2017  async connectedCallback() {
2018    this.setAttribute('tabindex', '0');
2019    this.addEventListener("focus", e => this.focus(e));
2020
2021    // Wait for child tag-option elements to be fully connected
2022    await new Promise(resolve => setTimeout(resolve, 0));
2023
2024    this._shadowRoot.replaceChildren();
2025
2026    this._wrapperTarget = document.createElement("div");
2027    this._wrapperTarget.style.position = "relative";
2028
2029    this.containerTarget = document.createElement("div");
2030    this.containerTarget.id = "container";
2031    this.containerTarget.appendChild(document.createElement("slot"));
2032
2033    this.inputTarget = document.createElement("input");
2034    this.inputTarget.id = "inputTarget";
2035    this.inputTarget.type = "hidden";
2036    this.inputTarget.name = this.name || "";
2037
2038    this._wrapperTarget.appendChild(this.containerTarget);
2039    this._wrapperTarget.appendChild(this.inputTarget);
2040    this._shadowRoot.appendChild(this._wrapperTarget);
2041
2042    this.form?.addEventListener("reset", this.reset.bind(this));
2043
2044    this.required = this.hasAttribute("required");
2045
2046    const maxTags = this.multiple ? undefined : 1;
2047    const placeholder = this.inputTarget.getAttribute("placeholder");
2048
2049    this.inputTarget.value = "";
2050    this.inputTarget.id = "";
2051
2052    this._taggle = new Taggle(this, {
2053      inputContainer: this.containerTarget,
2054      preserveCase: true,
2055      hiddenInputName: this.name,
2056      maxTags: maxTags,
2057      placeholder: placeholder,
2058      onTagAdd: (event, tag) => this.onTagAdd(event, tag),
2059      onTagRemove: (event, tag) => this.onTagRemove(event, tag),
2060    });
2061    this._taggleInputTarget = this._taggle.getInput();
2062    this._taggleInputTarget.id = this.id;
2063    this._taggleInputTarget.autocomplete = "off";
2064    this._taggleInputTarget.setAttribute("data-turbo-permanent", true);
2065    this._taggleInputTarget.addEventListener("keyup", e => this.keyup(e));
2066
2067    // Set initial value after taggle is initialized
2068    this.value = this._taggle.getTagValues();
2069
2070    this.checkRequired();
2071
2072    this.buttonTarget = document.createElement("button");
2073    this.buttonTarget.type = "button";
2074    this.buttonTarget.className = "add";
2075    this.buttonTarget.textContent = "+";
2076    this.buttonTarget.addEventListener("mousedown", e => e.preventDefault());
2077    this.buttonTarget.addEventListener("click", e => this._add(e));
2078    this._taggleInputTarget.insertAdjacentElement("afterend", this.buttonTarget);
2079
2080    this._taggleInputTarget.addEventListener("input", () => this._updateButtonContent());
2081
2082    this.autocompleteContainerTarget = document.createElement("ul");
2083    this._wrapperTarget.appendChild(this.autocompleteContainerTarget);
2084
2085    this.setupAutocomplete();
2086
2087    this.observe(); // Start observing after taggle is set up
2088    this.initialized = true;
2089
2090    // Update visibility based on current state
2091    this.updateInputVisibility();
2092    this._updateButtonContent();
2093  }
2094
2095  setupAutocomplete() {
2096    this._autocompleteResult = autocomplete({
2097      input: this._taggleInputTarget,
2098      container: this.autocompleteContainerTarget,
2099      className: "ui-menu ui-autocomplete",
2100      fetch: (text, update) => {
2101        const currentTags = this._taggle.getTagValues();
2102        const suggestions = this._getOptionsWithLabels().filter(option =>
2103          option.label.toLowerCase().includes(text.toLowerCase()) &&
2104          !currentTags.includes(option.value)
2105        );
2106        update(suggestions);
2107      },
2108      render: item => {
2109        const li = document.createElement("li");
2110        li.className = "ui-menu-item";
2111        li.dataset.value = item.value;
2112        li.textContent = item.label;
2113        return li
2114      },
2115      onSelect: item => {
2116        // Prevent adding multiple tags in single mode
2117        if (!this.multiple && this._taggle.getTagValues().length > 0) {
2118          this._taggleInputTarget.value = '';
2119          this._updateButtonContent();
2120          return
2121        }
2122
2123        // Create a tag-option element with proper value/label separation
2124        const tagOption = document.createElement('tag-option');
2125        tagOption.setAttribute('value', item.value);
2126        tagOption.textContent = item.label;
2127        this.appendChild(tagOption);
2128
2129        // Clear input
2130        this._taggleInputTarget.value = '';
2131        this._updateButtonContent();
2132      },
2133      minLength: 0,
2134      customize: (input, inputRect, container, maxHeight) => {
2135        // Position autocomplete below the input-tag container, accounting for dynamic height
2136        this._updateAutocompletePosition(container);
2137
2138        // Store reference to update positioning when container height changes
2139        this._autocompleteContainer = container;
2140      }
2141    });
2142  }
2143
2144  disconnectedCallback() {
2145    this.form?.removeEventListener("reset", this.reset.bind(this));
2146    this.unobserve();
2147  }
2148
2149  attributeChangedCallback(name, oldValue, newValue) {
2150    if (oldValue === newValue) return;
2151
2152    // Only handle changes after the component is connected and initialized
2153    if (!this._taggle) return;
2154
2155    switch (name) {
2156      case 'name':
2157        this.handleNameChange(newValue);
2158        break;
2159      case 'multiple':
2160        this.handleMultipleChange(newValue !== null);
2161        break;
2162      case 'required':
2163        this.handleRequiredChange(newValue !== null);
2164        break;
2165      case 'list':
2166        this.handleListChange(newValue);
2167        break;
2168    }
2169  }
2170
2171  checkRequired() {
2172    const flag = this.required && this._taggle.getTagValues().length == 0;
2173    this._taggleInputTarget.required = flag;
2174
2175    // Update ElementInternals validity to match internal input
2176    if (flag) {
2177      this._internals.setValidity({ valueMissing: true }, 'Please fill out this field.', this._taggleInputTarget);
2178    } else {
2179      this._internals.setValidity({});
2180    }
2181  }
2182
2183  // monkeypatch support for android comma
2184  keyup(event) {
2185    const key = event.which || event.keyCode;
2186    const normalKeyboard = key != 229;
2187    if(normalKeyboard) return
2188    const value = this._taggleInputTarget.value;
2189
2190    // backspace
2191    if(value.length == 0) {
2192      const values = this._taggle.tag.values;
2193      this._taggle.remove(values[values.length - 1]);
2194      return
2195    }
2196
2197    // comma
2198    if(/,$/.test(value)) {
2199      const tag = value.replace(',', '');
2200      this._taggle.add(tag);
2201      this._taggleInputTarget.value = '';
2202      return
2203    }
2204  }
2205
2206  _add(event) {
2207    event.preventDefault();
2208    const value = this._taggleInputTarget.value;
2209    if (value === '') {
2210      if (this._isAutocompleteOpen()) {
2211        this._closeAutocomplete();
2212      } else {
2213        this._taggleInputTarget.focus();
2214        this._autocompleteResult.fetch();
2215      }
2216      return
2217    }
2218    this._taggle.add(value);
2219    this._taggleInputTarget.value = '';
2220    this._updateButtonContent();
2221  }
2222
2223  _isAutocompleteOpen() {
2224    return this._taggleInputTarget.getAttribute("aria-expanded") === "true"
2225  }
2226
2227  _closeAutocomplete() {
2228    this._taggleInputTarget.setAttribute("aria-expanded", "false");
2229    this.autocompleteContainerTarget.remove();
2230  }
2231
2232  _updateButtonContent() {
2233    const isEmpty = !this._taggleInputTarget.value;
2234    const currentTags = this._taggle.getTagValues();
2235    const hasAvailableOption = this.options.some(value => !currentTags.includes(value));
2236    this.buttonTarget.textContent = isEmpty && hasAvailableOption ? "▾" : "+";
2237  }
2238
2239  onTagAdd(event, tag) {
2240    if (!this.suppressEvents) {
2241      const isNew = !this.options.includes(tag);
2242      this.dispatchEvent(new CustomEvent("update", {
2243        detail: { tag, isNew },
2244        bubbles: true,
2245        composed: true,
2246      }));
2247    }
2248    this.syncValue();
2249    this.checkRequired();
2250    this.updateInputVisibility();
2251    // Defer button update: taggle clears input.value after calling this callback
2252    setTimeout(() => this._updateButtonContent(), 0);
2253
2254    // Update autocomplete position if it's currently open
2255    if (this._autocompleteContainer) {
2256      // Use setTimeout to allow DOM to update first
2257      setTimeout(() => this._updateAutocompletePosition(this._autocompleteContainer), 0);
2258    }
2259  }
2260
2261  onTagRemove(event, tag) {
2262    if (!this.suppressEvents) {
2263      this.dispatchEvent(new CustomEvent("update", {
2264        detail: { tag },
2265        bubbles: true,
2266        composed: true,
2267      }));
2268    }
2269    this.syncValue();
2270    this.checkRequired();
2271    this.updateInputVisibility();
2272    this._updateButtonContent();
2273
2274    // Update autocomplete position if it's currently open
2275    if (this._autocompleteContainer) {
2276      // Use setTimeout to allow DOM to update first
2277      setTimeout(() => this._updateAutocompletePosition(this._autocompleteContainer), 0);
2278    }
2279  }
2280
2281  syncValue() {
2282    // Directly update internals without triggering the setter
2283    const values = this._taggle.getTagValues();
2284    const oldValues = this._internals.value;
2285    this._setFormValue(values);
2286
2287    if(this.initialized && !this.suppressEvents && JSON.stringify(oldValues) !== JSON.stringify(values)) {
2288      this.dispatchEvent(new CustomEvent("change", {
2289        bubbles: true,
2290        composed: true,
2291      }));
2292    }
2293  }
2294
2295  // Public API methods
2296  add(tags) {
2297    if (!this._taggle) return
2298    this._taggle.add(tags);
2299  }
2300
2301  remove(tag) {
2302    if (!this._taggle) return
2303    this._taggle.remove(tag);
2304  }
2305
2306  removeAll() {
2307    if (!this._taggle) return
2308    this._taggle.removeAll();
2309  }
2310
2311  has(tag) {
2312    if (!this._taggle) return false
2313    return this._taggle.getTagValues().includes(tag)
2314  }
2315
2316  get tags() {
2317    if (!this._taggle) return []
2318    return this._taggle.getTagValues()
2319  }
2320
2321  // Private getter for testing autocomplete suggestions
2322  get _autocompleteSuggestions() {
2323    if (!this.autocompleteContainerTarget) return []
2324    const items = this.autocompleteContainerTarget.querySelectorAll('.ui-menu-item');
2325    return Array.from(items).map(item => item.textContent.trim())
2326  }
2327
2328  // Update autocomplete position based on current container height
2329  _updateAutocompletePosition(container) {
2330    if (!container) return
2331
2332    const inputTagRect = this.containerTarget.getBoundingClientRect();
2333
2334    container.style.setProperty('position', 'absolute', 'important');
2335    container.style.setProperty('top', `${inputTagRect.height}px`, 'important');
2336    container.style.setProperty('left', '0', 'important');
2337    container.style.setProperty('right', '0', 'important');
2338    container.style.setProperty('width', '100%', 'important');
2339    container.style.setProperty('z-index', '1000', 'important');
2340  }
2341
2342  updateInputVisibility() {
2343    if (!this._taggleInputTarget || !this.buttonTarget) return;
2344
2345    const hasTags = this._taggle && this._taggle.getTagValues().length > 0;
2346
2347    if (this.multiple) {
2348      // Multiple mode: always show input and button
2349      this._taggleInputTarget.style.display = '';
2350      this.buttonTarget.style.display = '';
2351    } else {
2352      // Single mode: hide input and button when tag exists
2353      if (hasTags) {
2354        this._taggleInputTarget.style.display = 'none';
2355        this.buttonTarget.style.display = 'none';
2356      } else {
2357        this._taggleInputTarget.style.display = '';
2358        this.buttonTarget.style.display = '';
2359      }
2360    }
2361  }
2362
2363  addAt(tag, index) {
2364    if (!this._taggle) return
2365    this._taggle.add(tag, index);
2366  }
2367
2368  disable() {
2369    if (this._taggle) {
2370      this._taggle.disable();
2371    }
2372  }
2373
2374  enable() {
2375    if (this._taggle) {
2376      this._taggle.enable();
2377    }
2378  }
2379
2380  focus() {
2381    if (this._taggleInputTarget) {
2382      this._taggleInputTarget.focus();
2383    }
2384  }
2385
2386  checkValidity() {
2387    if (this._taggle) {
2388      this.checkRequired();
2389    }
2390    return this._internals.checkValidity()
2391  }
2392
2393  reportValidity() {
2394    if (this._taggle) {
2395      this.checkRequired();
2396    }
2397    return this._internals.reportValidity()
2398  }
2399
2400  handleNameChange(newName) {
2401    // Update the hidden input name to match
2402    const hiddenInput = this._shadowRoot.querySelector('input[type="hidden"]');
2403    if (hiddenInput) {
2404      hiddenInput.name = newName || '';
2405    }
2406
2407    // Update the form value with the new name
2408    if (this._internals.value) {
2409      this.value = this._internals.value; // This will recreate FormData with new name
2410    }
2411  }
2412
2413  handleMultipleChange(isMultiple) {
2414    if (!this._taggle) return;
2415
2416    // Get current tags
2417    const currentTags = this._taggle.getTagValues();
2418
2419    if (!isMultiple && currentTags.length > 1) {
2420      // Single mode: remove excess tag-option elements from DOM
2421      const tagOptions = Array.from(this.children);
2422      // Keep only the first tag-option element, remove the rest
2423      tagOptions.forEach((tagOption, i) => {
2424        if (i > 0 && tagOption) {
2425          this.removeChild(tagOption);
2426        }
2427      });
2428    }
2429
2430    // Reinitialize taggle with new multiple setting
2431    this.reinitializeTaggle();
2432
2433    // Restore tags, respecting the new multiple constraint
2434    if (isMultiple) {
2435      // Multiple mode: restore all remaining tags
2436      if (currentTags.length > 0) {
2437        this._taggle.add(currentTags);
2438      }
2439    } else {
2440      // Single mode: keep only the first tag
2441      if (currentTags.length > 0) {
2442        this._taggle.add(currentTags[0]);
2443      }
2444    }
2445
2446    this.updateValue();
2447    this.updateInputVisibility();
2448  }
2449
2450  handleRequiredChange(isRequired) {
2451    if (!this._taggle) return;
2452
2453    // Update the internal required state
2454    this.required = isRequired;
2455
2456    // Update validation
2457    this.checkRequired();
2458  }
2459
2460  handleListChange(newListId) {
2461    if (!this._taggle) return;
2462
2463    // Re-setup autocomplete with new datalist
2464    this.setupAutocomplete();
2465  }
2466
2467  reinitializeTaggle() {
2468    // Clean up existing taggle if it exists
2469    if (this._taggle && this._taggle.destroy) {
2470      this._taggle.destroy();
2471    }
2472
2473    // Get current configuration
2474    const maxTags = this.hasAttribute("multiple") ? undefined : 1;
2475    const placeholder = this.getAttribute("placeholder") || "";
2476
2477    // Create new taggle instance using original configuration pattern
2478    this._taggle = new Taggle(this, {
2479      inputContainer: this.containerTarget,
2480      preserveCase: true,
2481      hiddenInputName: this.name,
2482      maxTags: maxTags,
2483      placeholder: placeholder,
2484      onTagAdd: (event, tag) => this.onTagAdd(event, tag),
2485      onTagRemove: (event, tag) => this.onTagRemove(event, tag),
2486    });
2487
2488    // Re-get references since taggle was recreated
2489    this._taggleInputTarget = this._taggle.getInput();
2490    this._taggleInputTarget.id = this.id || "";
2491    this._taggleInputTarget.autocomplete = "off";
2492    this._taggleInputTarget.setAttribute("data-turbo-permanent", true);
2493    this._taggleInputTarget.addEventListener("keyup", e => this.keyup(e));
2494    this._taggleInputTarget.addEventListener("input", () => this._updateButtonContent());
2495
2496    // Re-setup autocomplete
2497    this.setupAutocomplete();
2498    this._updateButtonContent();
2499
2500    // Re-process existing tag options
2501    this.processTagOptions();
2502  }
2503
2504  updateValue() {
2505    if (!this._taggle) return;
2506
2507    // Update the internal value to match taggle state
2508    const values = this._taggle.getTagValues();
2509    const oldValues = this._internals.value;
2510    this._setFormValue(values);
2511
2512    // Check validity after updating
2513    this.checkRequired();
2514
2515    if(this.initialized && !this.suppressEvents && JSON.stringify(oldValues) !== JSON.stringify(values)) {
2516      this.dispatchEvent(new CustomEvent("change", {
2517        bubbles: true,
2518        composed: true,
2519      }));
2520    }
2521  }
2522}
2523customElements.define("input-tag", InputTag);
2524// concatenate successive sites main menus together. this allows for extension in other engines. pretty lame.
2525document.addEventListener("DOMContentLoaded", () => {
2526  let first = null
2527  Array.from(document.querySelectorAll("#top >
2527 .main")).forEach((element, index) => {
2528    if(index === 0) {
2529      first = element
2530    } else {
2531      first.insertAdjacentHTML("beforeend", element.innerHTML)
2532      element.remove()
2533    }
2534  })
2535})
2536;
2537document.addEventListener("DOMContentLoaded", () => {
2538  Array.from(document.querySelectorAll(".wysiwyg")).forEach(e => {
2539    if(!window.CKEDITOR) return
2540    CKEDITOR.timestamp = new Date().valueOf()
2541    e.ckeditor = CKEDITOR.replace(e.id, {
2542      width: e.getBoundingClientRect().width,
2543      height: e.getBoundingClientRect().height,
2544      customConfig: '/assets/ckeditor/config-d6288973a5d4491b97a4deeefd9eaa8bec60f0d05460200fa2352296e5fdbf63.js',
2545    })
2546  })
2547})
2548;
2549document.addEventListener("DOMContentLoaded", () => {
2550  const table = document.querySelector("table.list")
2551  const link = document.querySelector("a.reorder")
2552  if(table && link) new TableTree(table, link)
2553})
2554
2555/**
2556 * Configuration options:
2557 *
2558 * onDragClass
2559 *   This class is added for the duration of the drag and then removed when the row is dropped. It is more
2560 *   flexible than using onDragStyle since it can be inherited by the row cells and other content. The default
2561 *   is class is tDnD_whileDrag. So to use the default, simply customise this CSS class in your
2562 *   stylesheet.
2563 * onDrop
2564 *   Pass a function that will be called when the row is dropped. The function takes 2 parameters: the table
2565 *   and the row that was dropped. You can work out the new order of the rows by using
2566 *   table.rows.
2567 * onDragStart
2568 *   Pass a function that will be called when the user starts dragging. The function takes 2 parameters: the
2569 *   table and the row which the user has started to drag.
2570 * onAllowDrop
2571 *   Pass a function that will be called as a row is over another row. If the function returns true, allow
2572 *   dropping on that row, otherwise not. The function takes 2 parameters: the dragged row and the row under
2573 *   the cursor. It returns a boolean: true allows the drop, false doesn't allow it.
2574 * scrollAmount
2575 *   This is the number of pixels to scroll if the user moves the mouse 
2575cursor to the top or bottom of the
2576 *   window. The page should automatically scroll up or down as appropriate.
2577 */
2578
2579class TableDnD {
2580  constructor() {
2581    this.currentTable = null
2582    this.dragObject = null
2583    this.mouseOffset = null
2584    this.oldY = 0
2585  }
2586
2587  add(table, options) {
2588    table.tableDnDConfig = {
2589      // Add in the default class for whileDragging
2590      onDragClass: "tDnD_whileDrag",
2591      onDrop: null,
2592      onDrag: null, // ADDED
2593      onDragStart: null,
2594      scrollAmount: 5,
2595      ...(options || {}),
2596    }
2597    this.makeDraggable(table)
2598  }
2599
2600  makeDraggable(table) {
2601    table.querySelectorAll("tr").forEach(row => {
2602      row.addEventListener("mousedown", event => this.mousedown(table, row, event))
2603      row.style.cursor = "move"
2604    })
2605    // Now we need to capture the mouse up and mouse move event
2606    // We can use bind so that we don't interfere with other event handlers
2607    document.addEventListener('mousemove', event => this.mousemove(event))
2608    document.addEventListener('mouseup', event => this.mouseup(event))
2609  }
2610
2611  mousedown(table, row, event) {
2612    if(event.target.tagName == "TD") {
2613      event.preventDefault()
2614      this.dragObject = row
2615      this.currentTable = table
2616      this.mouseOffset = this.getMouseOffset(row, event)
2617      let config = table.tableDnDConfig
2618      if(config.onDragStart) config.onDragStart(table, row)
2619    }
2620  }
2621
2622  /** Given a target element and a mouse event, get the mouse offset from that element.
2623    To do this we need the element's position and the mouse position */
2624  getMouseOffset(target, event) {
2625    var docPos  = this.getPosition(target)
2626    return {
2627      x: event.pageX - docPos.x,
2628      y: event.pageY - docPos.y,
2629    }
2630  }
2631
2632  getPosition(element) {
2633    const rect = element.getBoundingClientRect()
2634    return {
2635        x: rect.left + window.pageXOffset,
2636        y: rect.top + window.pageYOffset,
2637    }
2638  }
2639
2640  mousemove(event) {
2641    if (this.dragObject == null) {
2642      return
2643    }
2644
2645    var config = this.currentTable.tableDnDConfig
2646    var y = event.pageY - this.mouseOffset.y
2647    var yOffset = window.pageYOffset
2648    if (event.pageY - yOffset < config.scrollAmount) {
2649      window.scrollBy(0, -config.scrollAmount)
2650    } else if(window.innerHeight - (event.pageY-yOffset) < config.scrollAmount) {
2651      window.scrollBy(0, config.scrollAmount)
2652    }
2653
2654    if (y != this.oldY) {
2655      // work out if we're going up or down...
2656      var movingDown = y > this.oldY
2657      this.oldY = y
2658      this.dragObject.classList.add(config.onDragClass)
2659      // If we're over a row then move the dragged row to there so that the user sees the
2660      // effect dynamically
2661      var currentRow = this.findDropTargetRow(this.dragObject, y, movingDown)
2662      if(currentRow) {
2663        if (movingDown && this.dragObject != currentRow) {
2664          this.dragObject.parentNode.insertBefore(this.dragObject, currentRow.nextSibling)
2665        } else if(!movingDown && this.dragObject != currentRow) {
2666          this.dragObject.parentNode.insertBefore(this.dragObject, currentRow)
2667        }
2668        if(config.onDrag) {
2669          config.onDrag(this.currentTable, this.dragObject)
2670        }
2671      }
2672    }
2673    return false
2674  }
2675
2676  /** We're only worried about the y position really, because we can only move rows up and down */
2677  findDropTargetRow(draggedRow, y, movingDown) {
2678    const rows = Array.from(this.currentTable.querySelectorAll("tr"))
2679    for (var i=0; i<rows.length; i++) {
2680      var row = rows[i]
2681      var rowY = this.getPosition(row).y
2682      var rowHeight = parseInt(row.offsetHeight)/2
2683      if (row.offsetHeight == 0) {
2684        rowY = this.getPosition(row.firstChild).y
2685        rowHeight = parseInt(row.firstChild.offsetHeight)/2
2686      }
2687      // Because we always have to insert before, we need to offset the height a bit
2688      if ((y > rowY - rowHeight) && (y < (rowY + rowHeight))) {
2689        // that's the row we're over
2690        // If it's the same as the current row, ignore it
2691        if(row == draggedRow) return
2692        var config = this.currentTable.tableDnDConfig
2693        if (config.onAllowDrop) {
2694          if(!config.onAllowDrop(draggedRow, row, movingDown)) return
2695        }
2696        return row
2697      }
2698    }
2699  }
2700
2701  mouseup(e) {
2702    if(this.currentTable && this.dragObject) {
2703      var droppedRow = this.dragObject
2704      var config = this.currentTable.tableDnDConfig
2705      // If we have a dragObject, then we need to release it,
2706      // The row will already have been moved to the right place so we just reset stuff
2707      droppedRow.classList.remove(config.onDragClass)
2708      this.dragObject = null
2709      if(config.onDrop) config.onDrop(this.currentTable, droppedRow)
2710      this.currentTable = null; // let go of the table too
2711    }
2712  }
2713
2714  teardown(table) {
2715    table.querySelectorAll("tr").forEach(row => {
2716      row.removeEventListener("mousedown", event => this.mousedown(table, row, event))
2717      row.style.cursor = "auto"
2718    })
2719    this.dragObject = null
2720    this.currentTable = null
2721    this.mouseOffset = null
2722  }
2723}
2724
2725class Base {
2726  find_node(element) {
2727    for (var i = 0; i < this.children.length; i++) {
2728      var child = this.children[i]
2729      if (this.children[i].element == element) {
2730        return this.children[i]
2731      } else {
2732        var result = this.children[i].find_node(element)
2733        if (result) return result
2734      }
2735    }
2736  }
2737
2738  ttnode(node) {
2739    var subject = node.push ? node[0] : node
2740    return (this.current_table || this.table_tree.current_table).find_node(subject)
2741  }
2742}
2743
2744class TableTree extends Base {
2745  constructor(table, link) {
2746    super()
2747    this.table = table
2748    this.type = link.id.replace('reorder_', '')
2749    this.collection_url = link.href
2750    this.tableDnD = new TableDnD()
2751
2752    link.addEventListener("click", event => {
2753      event.preventDefault()
2754      link.parentElement.classList.toggle('active')
2755      this.toggle()
2756    })
2757
2758    this.tableDnDOptions = {
2759      onDragClass: 'drag',
2760      onDragStart: (table, row) => {
2761        this.startOffset = this.tableDnD.mouseOffset.x
2762        row.addEventListener("mousemove", event => this.mousemove(event))
2763        this.ttnode(row)?.dragStart()
2764      },
2765      onDrag: (table, row) => {
2766        this.current_table.dirty = true
2767        this.ttnode(row)?.update_children()
2768      },
2769      onDrop: (table, row) => {
2770        row.removeEventListener("mousemove", event => this.mousemove(event))
2771        this.ttnode(row)?.drop()
2772        this.current_table.rebuild()
2773        this.current_table.update_remote(row)
2774      },
2775      onAllowDrop: (draggedRow, row, movingDown) => {
2776        const node = this.ttnode(row)
2777        const next = movingDown ? node.next_row_sibling() : node
2778        if(next && next.parent && this.ttnode(draggedRow)) {
2779          if(next.parent.level >= this.ttnode(draggedRow).level) return false
2780        }
2781        return node ? true : false
2782      }
2783    }
2784  }
2785
2786  toggle() {
2787    this.current_table ? this.teardown() : this.setup()
2788  }
2789
2790  setup() {
2791    const tbody = this.table.querySelector('tbody')
2792    this.tableDnD.add(tbody, this.tableDnDOptions)
2793    this.current_table = new Table(this, this.table, this.type, this.collection_url)
2794    this.current_table.setSortable()
2795  }
2796
2797  teardown() {
2798    // this.current_table.update_remote()
2799    this.tableDnD.teardown(this.table)
2800    this.current_table.setUnsortable()
2801    this.current_table = null
2802  }
2803
2804  level(element) {
2805    var match = element.className.match(/level_([\d]+)/)
2806    return match ? parseInt(match[1]) : 0
2807  }
2808
2809  mousemove(event) {
2810    if (!this.current_table.is_tree) return
2811
2812    const element = event.target.closest("tr")
2813    const offset = this.tableDnD.getMouseOffset(element, event).x - this.startOffset
2814    if(offset > 25) {
2815      this.current_table.dirty = true
2816      this.ttnode(element).increment_level(event)
2817    } else if(offset < -25) {
2818      this.current_table.dirty = true
2819      this.ttnode(element).decrement_level(event)
2820    }
2821  }
2822}
2823
2824class Table extends Base {
2825  constructor(table_tree, table, type, collection_url) {
2826    super()
2827    this.table_tree = table_tree
2828    this.is_tree = table.classList.contains('tree')
2829    this.table = table
2830    this.type = type
2831    this.level = -1
2832    this.collection_url = collection_url
2833    this.rebuild()
2834  }
2835
2836  rebuild() {
2837    this.trs = Array.from(this.table.querySelectorAll('tr'))
2838    this.children = this.trs.map(tr => {
2839      if(this.table_tree.level(tr) === 0) {
2840        return new TableNode(this.table_tree, this, tr, this.table_tree.level(tr))
2841      }
2842    }).filter(e => e)
2843  }
2844
2845  setSortable() {
2846    this.trs.forEach(tr => {
2847      // tr is in thead
2848      let cells = tr.querySelectorAll('th')
2849      cells.forEach((th, index) => {
2850        if(index === 0) {
2851          th.setAttribute('colspan', cells.length)
2852        } else {
2853          th.hidden = true
2854        }
2855      })
2856
2857      // tbody
2858      cells = tr.querySelectorAll('td')
2859      cells.forEach((td, index) => {
2860        if (index === 0) {
2861          td.setAttribute('colspan', cells.length)
2862          td.querySelectorAll('a').forEach((a, index) => {
2863            a.hidden = true
2864            if (index === 0) {
2865              td.appendChild(document.createTextNode(a.innerText))
2866            }
2867          })
2868        } else {
2869          td.hidden = true
2870        }
2871      })
2872    })
2873  }
2874
2875  setUnsortable() {
2876    this.trs.forEach(tr => {
2877      // tr is in thead
2878      let cells = tr.querySelectorAll('th')
2879      cells.forEach((th, index) => {
2880        if(index === 0) {
2881          this.setAttribute('colspan', 1)
2882        } else {
2883          th.hidden = false
2884        }
2885      })
2886
2887      // tbody
2888      cells = tr.querySelectorAll('td')
2889      cells.forEach((td, index) => {
2890        if (index === 0) {
2891          td.querySelectorAll('a').forEach((a, index) => {
2892            a.hidden = false
2893            if (index === 0) {
2894              td.removeChild(td.lastChild)
2895            }
2896          })
2897          td.querySelector('img.spinner')?.remove()
2898          td.setAttribute('colspan', 1)
2899        } else {
2900          td.hidden = false
2901        }
2902      })
2903    })
2904  }
2905
2906  update_remote(row) {
2907    if(!this.dirty) return
2908    this.dirty = false
2909    this.show_spinner(row)
2910
2911    fetch(this.collection_url, {
2912      method: "PUT",
2913      body: new URLSearchParams({
2914        ...this.serialize(row),
2915        authenticity_token: window._auth_token,
2916      }),
2917    })
2918    .then(() => this.hide_spinner(row))
2919    .catch(() => this.hide_spinner(row))
2920  }
2921
2922  serialize(row) {
2923    row = this.ttnode(row)
2924    let data = {}
2925    data[`${this.type}[${row.id()}][parent_id]`] = row.parent_id()
2926    data[`${this.type}[${row.id()}][left_id]`] = row.left_id()
2927    return data
2928  }
2929
2930  show_spinner(row) {
2931    let img = document.createElement('img')
2932    img.src = 'data:image/gif;
2932base64,R0lGODlhEAAQAPQAAP///wAAAPDw8IqKiuDg4EZGRnp6egAAAFhYWCQkJKysrL6+vhQUFJycnAQEBDY2NmhoaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAAFdyAgAgIJIeWoAkRCCMdBkKtIHIngyMKsErPBYbADpkSCwhDmQCBethRB6Vj4kFCkQPG4IlWDgrNRIwnO4UKBXDufzQvDMaoSDBgFb886MiQadgNABAokfCwzBA8LCg0Egl8jAggGAA1kBIA1BAYzlyILczULC2UhACH5BAkKAAAALAAAAAAQABAAAAV2ICACAmlAZTmOREEIyUEQjLKKxPHADhEvqxlgcGgkGI1DYSVAIAWMx+lwSKkICJ0QsHi9RgKBwnVTiRQQgwF4I4UFDQQEwi6/3YSGWRRmjhEETAJfIgMFCnAKM0KDV4EEEAQLiF18TAYNXDaSe3x6mjidN1s3IQAh+QQJCgAAACwAAAAAEAAQAAAFeCAgAgLZDGU5jgRECEUiCI+yioSDwDJyLKsXoHFQxBSHAoAAFBhqtMJg8DgQBgfrEsJAEAg4YhZIEiwgKtHiMBgtpg3wbUZXGO7kOb1MUKRFMysCChAoggJCIg0GC2aNe4gqQldfL4l/Ag1AXySJgn5LcoE3QXI3IQAh+QQJCgAAACwAAAAAEAAQAAAFdiAgAgLZNGU5joQhCEjxIssqEo8bC9BRjy9Ag7GILQ4QEoE0gBAEBcOpcBA0DoxSK/e8LRIHn+i1cK0IyKdg0VAoljYIg+GgnRrwVS/8IAkICyosBIQpBAMoKy9dImxPhS+GKkFrkX+TigtLlIyKXUF+NjagNiEAIfkECQoAAAAsAAAAABAAEAAABWwgIAICaRhlOY4EIgjH8R7LKhKHGwsMvb4AAy3WODBIBBKCsYA9TjuhDNDKEVSERezQEL0WrhXucRUQGuik7bFlngzqVW9LMl9XWvLdjFaJtDFqZ1cEZUB0dUgvL3dgP4WJZn4jkomWNpSTIyEAIfkECQoAAAAsAAAAABAAEAAABX4gIAICuSxlOY6CIgiD8RrEKgqGOwxwUrMlAoSwIzAGpJpgoSDAGifDY5kopBYDlEpAQBwevxfBtRIUGi8xwWkDNBCIwmC9Vq0aiQQDQuK+VgQPDXV9hCJjBwcFYU5pLwwHXQcMKSmNLQcIAExlbH8JBwttaX0ABAcNbWVbKyEAIfkECQoAAAAsAAAAABAAEAAABXkgIAICSRBlOY7CIghN8zbEKsKoIjdFzZaEgUBHKChMJtRwcWpAWoWnifm6ESAMhO8lQK0EEAV3rFopIBCEcGwDKAqPh4HUrY4ICHH1dSoTFgcHUiZjBhAJB2AHDykpKAwHAwdzf19KkASIPl9cDgcnDkdtNwiMJCshACH5BAkKAAAALAAAAAAQABAAAAV3ICACAkkQZTmOAiosiyAoxCq+KPxCNVsSMRgBsiClWrLTSWFoIQZHl6pleBh6suxKMIhlvzbAwkBWfFWrBQTxNLq2RG2yhSUkDs2b63AYDAoJXAcFRwADeAkJDX0AQCsEfAQMDAIPBz0rCgcxky0JRWE1AmwpKyEAIfkECQoAAAAsAAAAABAAEAAABXkgIAICKZzkqJ4nQZxLqZKv4NqNLKK2/Q4Ek4lFXChsg5ypJjs1II3gEDUSRInEGYAw6B6zM4JhrDAtEosVkLUtHA7RHaHAGJQEjsODcEg0FBAFVgkQJQ1pAwcDDw8KcFtSInwJAowCCA6RIwqZAgkPNgVpWndjdyohACH5BAkKAAAALAAAAAAQABAAAAV5ICACAimc5KieLEuUKvm2xAKLqDCfC2GaO9eL0LABWTiBYmA06W6kHgvCqEJiAIJiu3gcvgUsscHUERm+kaCxyxa+zRPk0SgJEgfIvbAdIAQLCAYlCj4DBw0IBQsMCjIqBAcPAooCBg9pKgsJLwUFOhCZKyQDA3YqIQAh+QQJCgAAACwAAAAAEAAQAAAFdSAgAgIpnOSonmxbqiThCrJKEHFbo8JxDDOZYFFb+A41E4H4OhkOipXwBElYITDAckFEOBgMQ3arkMkUBdxIUGZpEb7kaQBRlASPg0FQQHAbEEMGDSVEAA1QBhAED1E0NgwFAooCDWljaQIQCE5qMHcNhCkjIQAh+QQJCgAAACwAAAAAEAAQAAAFeSAgAgIpnOSoLgxxvqgKLEcCC65KEAByKK8cSpA4DAiHQ/DkKhGKh4ZCtCyZGo6F6iYYPAqFgYy02xkSaLEMV34tELyRYNEsCQyHlvWkGCzsPgMCEAY7Cg04Uk48LAsDhRA8MVQPEF0GAgqYYwSRlycNcWskCkApIyEAOwAAAAAAAAAAAA=='
2933    img.className = 'spinner'
2934    row.querySelector("td").appendChild(img)
2935  }
2936
2937  hide_spinner(row) {
2938    let cell = row.querySelector('td')
2939    cell.removeChild(cell.lastChild)
2940  }
2941}
2942
2943class TableNode extends Base {
2944  constructor(table_tree, parent, element, level) {
2945    super()
2946    this.table_tree = table_tree
2947    this.parent = parent
2948    this.element = element
2949    this.level = level
2950
2951    this.children = this.find_children().map(child => {
2952      var level = this.table_tree.level(child)
2953      if(level == this.level + 1) {
2954        return new TableNode(this.table_tree, this, child, level)
2955      }
2956    }).filter(e => e)
2957  }
2958
2959  find_children() {
2960    var stop = false
2961    return this.row_siblings().slice(this.row_index() + 1).filter(child => {
2962      var level = this.table_tree.level(child)
2963      if(this.level == level) stop = true // how to break from an iterator?
2964      return !stop && this.level + 1 == level
2965    })
2966  }
2967
2968  depth() {
2969    if (this.children.length > 0) {
2970      return Math.max.apply(Math, this.children.map(child => child.depth()))
2971    } else {
2972      return this.level
2973    }
2974  }
2975
2976  siblings() {
2977    return this.parent.children
2978  }
2979
2980  id() {
2981    return this.element ? this.to_int(this.element.id) : 'null'
2982  }
2983
2984  parent_id() {
2985    return this.parent.element ? this.to_int(this.parent.element.id) : 'null'
2986  }
2987
2988  left_id() {
2989    let left = this.left()
2990    return left ? this.to_int(left.element.id) : 'null'
2991  }
2992
2993  left() {
2994    let siblings = this.siblings()
2995    let ix = siblings.indexOf(this) - 1
2996    if(ix >= 0) return siblings[ix]
2997  }
2998
2999  to_int(str) {
3000    if(str) return str.replace(/[\D]+/, '')
3001  }
3002
3003  next_row_sibling() {
3004    return this.row_siblings()[this.row_index() + 1]
3005  }
3006
3007  row_siblings() {
3008    this._row_siblings ||= Array.from(this.element.parentElement.children)
3009    return this._row_siblings
3010  }
3011
3012  row_index() {
3013    return this.row_siblings().indexOf(this.element)
3014  }
3015
3016  dragStart() {
3017    this.element.classList.add('drag')
3018    this.children.forEach(child => child.dragStart())
3019  }
3020
3021  drop() {
3022    this.element.classList.remove('drag')
3023    this.children.forEach(child => child.drop())
3024    this.adjust_level()
3025  }
3026
3027  increment_level(event) {
3028    let prev = this.element.previousElementSibling
3029    if(prev) prev = this.ttnode(prev)
3030    if(!prev || prev.level < this.level || this.depth() >= 5) return
3031    this.update_level(event, this.level + 1)
3032  }
3033
3034  decrement_level(event) {
3035    if(this.level == 0) return
3036    this.update_level(event, this.level - 1)
3037  }
3038
3039  update_level(event, level) {
3040    if (event) this.table_tree.startOffset = this.table_tree.tableDnD.getMouseOffset(this.element, event).x
3041
3042    this.element.classList.remove('level_' + this.level)
3043    this.element.classList.add('level_' + level)
3044
3045    this.level = level
3046    this.children.forEach(child => child.update_level(event, level + 1))
3047  }
3048
3049  adjust_level() {
3050    var prev = this.element.previousElementSibling
3051    if(!prev) {
3052      this.update_level(null, 0)
3053    } else if(this.ttnode(prev).level + 1 < this.level) {
3054      this.update_level(null, this.ttnode(prev).level + 1)
3055    }
3056  }
3057
3058  update_children() {
3059    this.children.forEach(child => child.element.parentNode.removeChild(child.element))
3060    var next = this.element.nextSibling
3061    this.children.forEach(child => this.element.parentNode.insertBefore(child.element, next))
3062    this.children.forEach(child => child.update_children())
3063  }
3064}
3065;
3066
3067
3068
3069
3070
3071
3072
3073

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.