PageSourceSearch

https://irioncad.org/lib/mvcgrid/js/mvc-grid.js

js irioncad.org collected 2026-10-04 17:39:49 UTC 35,671 bytes, 1,062 lines download raw bytes

1/*!
2 * Mvc.Grid 7.2.1
3 *
4 * Copyright © NonFactors
5 *
6 * Licensed under the terms of the MIT License
7 * https://www.opensource.org/licenses/mit-license.php
8 */
9class MvcGrid {
10    constructor(container, options = {}) {
11        const grid = this;
12        const element = grid.findGrid(container);
13
14        if (element.dataset.id) {
15            return MvcGrid.instances[parseInt(element.dataset.id)].set(options);
16        }
17
18        grid.columns = [];
19        grid.element = element;
20        grid.loadingDelay = 300;
21        grid.loadingTimerId = 0;
22        grid.name = element.dataset.name;
23        grid.controller = new AbortController();
24        grid.isAjax = Boolean(element.dataset.url);
25        grid.prefix = grid.name ? `${grid.name}-` : "";
26        grid.filterMode = (element.dataset.filterMode || "").toLowerCase();
27        element.dataset.id = options.id || MvcGrid.instances.length.toString();
28        grid.url = element.dataset.url ? new URL(element.dataset.url, location.href) : new URL(location.href);
29        grid.url = options.url ? new URL(options.url.toString(), location.href) : grid.url;
30        grid.url = options.query ? new URL(`?${options.query}`, grid.url.href) : grid.url;
31        grid.sort = grid.buildSort();
32        grid.filters = {
33            default: MvcGridFilter,
34            date: MvcGridDateFilter,
35            guid: MvcGridGuidFilter,
36            text: MvcGridTextFilter,
37            number: MvcGridNumberFilter
38        };
39
40        const headers = element.querySelector(".mvc-grid-headers");
41        const rowFilters = element.querySelectorAll(".mvc-grid-row-filters th");
42
43        if (headers) {
44            for (const [i, header] of headers.querySelectorAll("th").entries()) {
45                grid.columns.push(new MvcGridColumn(grid, header, rowFilters[i]));
46            }
47        }
48
49        const pager = element.querySelector(".mvc-grid-pager");
50
51        if (pager) {
52            grid.pager = new MvcGridPager(grid, pager);
53        }
54
55        grid.set(options);
56        grid.cleanUp();
57        grid.bind();
58
59        if (options.id) {
60            MvcGrid.instances[parseInt(options.id)] = grid;
61        } else {
62            MvcGrid.instances.push(grid);
63        }
64
65        if (!element.children.length) {
66            grid.reload();
67        }
68
69        return this;
70    }
71
72    set(options) {
73        const grid = this;
74
75        grid.loadingDelay = typeof options.loadingDelay === "undefined" ? grid.loadingDelay : options.loadingDelay;
76        grid.url = options.url ? new URL(options.url.toString(), location.href) : grid.url;
77        grid.url = options.query ? new URL(`?${options.query}`, grid.url.href) : grid.url;
78        grid.isAjax = typeof options.isAjax === "boolean" ? options.isAjax : grid.isAjax;
79        grid.filters = Object.assign(grid.filters, options.filters);
80
81        for (const column of grid.columns) {
82            if (column.filter && grid.filters[column.filter.name]) {
83                column.filter.instance = new grid.filters[column.filter.name](column);
84                column.filter.instance.init();
85            }
86        }
87
88        return grid;
89    }
90    showConfiguration(anchor) {
91        MvcGridPopup.showConfiguration(this, anchor);
92    }
93    getConfiguration() {
94        return {
95            name: this.name,
96            columns: this.columns.map(column => ({
97                name: column.name,
98                hidden: column.isHidden,
99                width: column.header.style.width
100            }))
101        };
102    }
103    configure(configuration) {
104        configuration.columns.forEach((column, index) => {
105            const rows = this.element.querySelectorAll("tr");
106            const i = this.columns.findIndex(col => col.name.toLowerCase() === column.name.toLowerCase());
107
108            if (i >= 0) {
109                this.columns[i].isHidden = column.hidden;
110
111                if (column.width.trim()) {
112                    this.columns[i].header.style.width = column.width.split(";", 2)[0];
113                }
114
115                for (const tr of rows) {
116                    if (column.hidden) {
117                        tr.children[i].classList.add("mvc-grid-hidden");
118                    } else {
119                        tr.children[i].classList.remove("mvc-grid-hidden");
120                    }
121
122                    if (i !== index) {
123                        tr.insertBefore(tr.children[i], tr.children[index]);
124                    }
125                }
126
127                this.columns.splice(i - (index < i ? 1 : 0), 0, this.columns.splice(index, 1)[0]);
128            }
129        });
130    }
131
132    reload() {
133        const grid = this;
134
135        grid.element.dispatchEvent(new CustomEvent("reloadstart", {
136            detail: { grid },
137            bubbles: true
138        }));
139
140        if (grid.isAjax) {
141            const url = new URL(grid.url.href);
142
143            grid.controller.abort();
144            MvcGridPopup.lastActiveElement = null;
145            grid.controller = new AbortController();
146
147            if (grid.loadingDelay !== null) {
148                const loader = `<td colspan="${grid.columns.length}"><div class="mvc-grid-loader"><div class="mvc-grid-spinner"></div></div></td>`;
149
150                clearTimeout(grid.loadingTimerId);
151
152                grid.loadingTimerId = setTimeout(() => {
153                    for (const row of grid.element.querySelectorAll("tbody > tr")) {
154                        row.innerHTML = loader;
155                    }
156                }, grid.loadingDelay);
157            }
158
159            MvcGridPopup.hide();
160
161            fetch(url.href, {
162                signal: grid.controller.signal,
163                headers: { "X-Requested-With": "XMLHttpRequest" }
164            }).then(response => {
165                if (!response.ok) {
166                    throw new Error(`Invalid response status: ${response.status}`);
167                }
168
169                return response.text();
170            }).then(response => {
171                const parent = grid.element.parentElement;
172                const template = document.createElement("template");
173                const i = Array.from(parent.children).indexOf(grid.element);
174
175                template.innerHTML = response.trim();
176
177                if (template.content.firstElementChild.classList.contains("mvc-grid")) {
178                    grid.element.outerHTML = response;
179                } else {
180                    throw new Error("Grid partial should only include grid declaration.");
181                }
182
183                const newGrid = new MvcGrid(parent.children[i], {
184                    loadingDelay: grid.loadingDelay,
185                    id: grid.element.dataset.id,
186                    filters: grid.filters,
187                    isAjax: grid.isAjax,
188                    url: grid.url
189                });
190
191                newGrid.element.dispatchEvent(new CustomEvent("reloadend", {
192                    detail: { grid: newGrid },
193                    bubbles: true
194                }));
195            }).catch(reason => {
196                if (reason.name === "AbortError") {
197                    return Promise.resolve();
198                }
199
200                const cancelled = !grid.element.dispatchEvent(new CustomEvent("reloadfail", {
201                    detail: { grid, reason },
202                    cancelable: true,
203                    bubbles: true
204                }));
205
206                return cancelled ? Promise.resolve() : Promise.reject(reason);
207            });
208        } else {
209            location.href = grid.url.href;
210        }
211    }
212
213    buildSort() {
214        const map = new Map();
215        const definitions = /(^|,)(.*?) (asc|desc)(?=$|,)/g;
216        const sort = this.url.searchParams.get(`${this.prefix}sort`) || "";
217
218        let match = definitions.exec(sort);
219
220        while (match) {
221            map.set(match[2], match[3]);
222
223            match = definitions.exec(sort);
224        }
225
226        return map;
227    }
228    findGrid(element) {
229        const grid = element.closest(".mvc-grid");
230
231        if (!grid) {
232            throw new Error("Grid can only be created from within mvc-grid structure.");
233        }
234
235        return grid;
236    }
237    cleanUp() {
238        delete this.element.dataset.filterMode;
239        delete this.element.dataset.url;
240    }
241    bind() {
242        const grid = this;
243
244        for (const row of grid.element.querySelectorAll("tbody > tr:not(.mvc-grid-empty-row)")) {
245            row.addEventListener("click", function (e) {
246                const data = {};
247
248                for (const [i, column] of grid.columns.entries()) {
249                    if (row.cells.length <= i) {
250                        return;
251                    }
252
253                    data[column.name] = row.cells[i].innerText;
254                }
255
256                this.dispatchEvent(new CustomEvent("rowclick", {
257                    detail: { grid: grid, data: data, originalEvent: e },
258                    bubbles: true
259                }));
260            });
261        }
262    }
263}
264
265MvcGrid.instances = [];
266MvcGrid.lang = {
267    default: {
268        "equals": "Equals",
269        "not-equals": "Not equals"
270    },
271    text: {
272        "contains": "Contains",
273        "consists-of": "Consists of",
274        "equals": "Equals",
275        "not-equals": "Not equals",
276        "starts-with": "Starts with",
277        "ends-with": "Ends with"
278    },
279    number: {
280        "equals": "Equals",
281        "not-equals": "Not equals",
282        "less-than": "Less than",
283        "greater-than": "Greater than",
284        "less-than-or-equal": "Less than or equal",
285        "greater-than-or-equal": "Greater than or equal"
286    },
287    date: {
288        "equals": "Equals",
289        "not-equals": "Not equals",
290        "earlier-than": "Earlier than",
291        "later-than": "Later than",
292        "earlier-than-or-equal": "Earlier than or equal",
293        "later-than-or-equal": "Later than or equal"
294    },
295    guid: {
296        "equals": "Equals",
297        "not-equals": "Not equals"
298    },
299    filter: {
300        "apply": "&#10003;",
301        "remove": "&#10008;"
302    },
303    operator: {
304        "select": "",
305        "and": "and",
306        "or": "or"
307    }
308};
309
310class MvcGridColumn {
311    constructor(grid, header, rowFilter) {
312        const column = this;
313        const data = header.dataset;
314
315        column.grid = grid;
316        column.header = header;
317        column.name = data.name || "";
318        column.isHidden = header.classList.contains("mvc-grid-hidden");
319        column.filter = data.filter ? new MvcGridColumnFilter(column, rowFilter) : null;
320        column.sort = header.classList.contains("sortable") ? new MvcGridColumnSort(column) : null;
321
322        column.cleanUp();
323    }
324    cleanUp() {
325        const data = this.header.dataset;
326
327        delete data.filterDefaultMethod;
328        delete data.filterApplied;
329        delete data.filterType;
330        delete data.filter;
331
332        delete data.sortFirst;
333        delete data.sort;
334
335        delete data.name;
336    }
337}
338
339class MvcGridColumnSort {
340    constructor(column) {
341        const sort = this;
342
343        sort.column = column;
344        sort.button = column.header.querySelector(".mvc-grid-sort");
345        sort.order = (column.header.dataset.sort || "").toLowerCase();
346        sort.first = (column.header.dataset.sortFirst || "asc").toLowerCase();
347
348        sort.bind();
349    }
350
351    toggle(multi) {
352        const sort = this;
353        const grid = sort.column.grid;
354        const map = sort.column.grid.sort;
355        const query = grid.url.searchParams;
356
357        if (sort.order === sort.first) {
358            sort.order = sort.order === "asc" ? "desc" : "asc";
359        } else if (sort.order) {
360            sort.order = "";
361        } else {
362            sort.order = sort.first;
363        }
364
365        if (!multi) {
366            map.clear();
367        }
368
369        if (sort.order) {
370            map.set(sort.column.name, sort.order);
371        } else {
372            map.delete(sort.column.name);
373        }
374
375        const order = Array.from(map).map(value => value.join(" ")).join(",");
376
377        query.delete(`${grid.prefix}sort`);
378
379        if (order) {
380            query.set(`${grid.prefix}sort`, order);
381        }
382
383        grid.reload();
384    }
385    bind() {
386        const sort = this;
387        const column = sort.column;
388
389        column.header.addEventListener("click", e => {
390            if (!column.filter || column.grid.filterMode !== "header") {
391                if (!/mvc-grid-(sort|filter)/.test(e.target.className)) {
392                    sort.toggle(e.ctrlKey || e.shiftKey);
393                }
394            }
395        });
396
397        sort.button.addEventListener("click", e => {
398            sort.toggle(e.ctrlKey || e.shiftKey);
399        });
400    }
401}
402
403class MvcGridColumnFilter {
404    constructor(column, rowFilter) {
405        const values = [];
406        const methods = [];
407        const filter = this;
408        const data = column.header.dataset;
409        const query = column.grid.url.searchParams;
410        const name = `${column.grid.prefix + column.name}-`;
411        let options = column.header.querySelector(".mvc-grid-options");
412
413        if (column.grid.filterMode === "row") {
414            options = rowFilter.querySelector("select");
415        }
416
417        if (options && options.classList.contains("mvc-grid-options")) {
418            options.parentElement.removeChild(options);
419        }
420
421        for (const parameter of query.entries()) {
422            if (parameter[0] !== `${name}op` && parameter[0].startsWith(name)) {
423                methods.push(parameter[0].substring(name.length));
424                values.push(parameter[1]);
425            }
426        }
427
428        filter.column = column;
429        filter.rowFilter = rowFilter;
430        filter.name = data.filter || "default";
431        filter.isApplied = data.filterApplied === "True";
432        filter.defaultMethod = data.filterDefaultMethod || "";
433        filter.type = (data.filterType || "single").toLowerCase();
434        filter.options = options && options.children.length > 0 ? options : null;
435        filter.button = (rowFilter || column.header).querySelector(".mvc-grid-filter");
436        filter.inlineInput = rowFilter ? rowFilter.querySelector(".mvc-grid-value") : null;
437
438        filter.first = {
439            method: methods[0] || "",
440            values: filter.type === "multi" ? values : values.slice(0, 1)
441        };
442
443        filter.operator = filter.type === "double" ? query.get(`${name}op`) || "" : "";
444
445        filter.second = {
446            method: filter.type === "double" ? methods[1] || "" : "",
447            values: filter.type === "double" ? values.slice(1, 2) : []
448        };
449
450        this.bind();
451    }
452
453    apply() {
454        const grid = this.column.grid;
455        const query = grid.url.searchParams;
456        const prefix = this.column.grid.prefix;
457        const order = query.get(`${prefix}sort`);
458
459        for (const column of grid.columns) {
460            for (const key of [...query.keys()]) {
461                if (key.startsWith(`${prefix + column.name}-`)) {
462                    query.delete(key);
463                }
464            }
465        }
466
467        query.delete(`${prefix}sort`);
468        query.delete(`${prefix}page`);
469        query.delete(`${prefix}rows`);
470
471        for (const column of grid.columns.filter(col => col.filter && (col === this.column || col.filter.isApplied || col.filter.first.values[0]))) {
472            const filter = column.filter;
473
474            query.set(`${prefix + column.name}-${filter.first.method}`, filter.first.values[0] || "");
475
476            for (let i = 1;
476 filter.type === "multi" && i < filter.first.values.length; i++) {
477                query.append(`${prefix + column.name}-${filter.first.method}`, filter.first.values[i] || "");
478            }
479
480            if (grid.filterMode === "excel" && filter.type === "double") {
481                query.set(`${prefix + column.name}-op`, filter.operator || "");
482                query.append(`${prefix + column.name}-${filter.second.method}`, filter.second.values[0] || "");
483            }
484        }
485
486        if (order) {
487            query.set(`${prefix}sort`, order);
488        }
489
490        if (grid.pager && grid.pager.showPageSizes) {
491            query.set(`${prefix}rows`, grid.pager.rowsPerPage.value);
492        }
493
494        grid.reload();
495    }
496    cancel() {
497        const filter = this;
498        const column = filter.column;
499        const grid = filter.column.grid;
500        const query = grid.url.searchParams;
501
502        if (filter.isApplied) {
503            query.delete(`${grid.prefix}page`);
504            query.delete(`${grid.prefix}rows`);
505
506            for (const key of [...query.keys()]) {
507                if (key.startsWith(`${grid.prefix + column.name}-`)) {
508                    query.delete(key);
509                }
510            }
511
512            grid.reload();
513        } else {
514            filter.first.values = [];
515            filter.second.values = [];
516
517            if (column.grid.filterMode !== "excel") {
518                filter.inlineInput.value = "";
519            }
520
521            MvcGridPopup.hide();
522        }
523    }
524
525    bind() {
526        const filter = this;
527        const column = filter.column;
528        const mode = column.grid.filterMode;
529
530        filter.button.addEventListener("click", () => {
531            MvcGridPopup.show(filter);
532        });
533
534        if (filter.options) {
535            if (mode === "row" && filter.type !== "multi") {
536                filter.inlineInput.addEventListener("change", function () {
537                    filter.first.values = [this.value];
538                    column.filter.apply();
539                });
540            } else if (mode === "header" || mode === "row") {
541                filter.inlineInput.addEventListener("click", function () {
542                    if (this.selectionStart === this.selectionEnd) {
543                        MvcGridPopup.show(filter);
544                    }
545                });
546            }
547        } else if (mode !== "excel") {
548            filter.inlineInput.addEventListener("input", function () {
549                filter.first.values = [this.value];
550                filter.instance.validate(this);
551            });
552
553            filter.inlineInput.addEventListener("keyup", function (e) {
554                if (e.key === "Enter" && filter.instance.isValid(this.value)) {
555                    column.filter.apply();
556                }
557            });
558        }
559    }
560}
561
562class MvcGridPager {
563    constructor(grid, element) {
564        const pager = this;
565
566        pager.grid = grid;
567        pager.element = element;
568        pager.totalRows = parseInt(element.dataset.totalRows);
569        pager.pages = grid.element.querySelectorAll("[data-page]");
570        pager.showPageSizes = element.dataset.showPageSizes === "True";
571        pager.rowsPerPage = element.querySelector(".mvc-grid-pager-rows");
572        pager.currentPage = pager.pages.length ? parseInt(element.querySelector(".active").dataset.page) : 1;
573
574        pager.cleanUp();
575        pager.bind();
576    }
577
578    apply(page, rows) {
579        const grid = this.grid;
580        const query = grid.url.searchParams;
581
582        query.delete(`${grid.prefix}page`);
583        query.delete(`${grid.prefix}rows`);
584
585        query.set(`${grid.prefix}page`, page);
586
587        if (this.showPageSizes) {
588            query.set(`${grid.prefix}rows`, typeof rows === "string" ? rows : this.rowsPerPage.value);
589        }
590
591        grid.reload();
592    }
593
594    cleanUp() {
595        delete this.element.dataset.showPageSizes;
596        delete this.element.dataset.totalPages;
597        delete this.element.dataset.totalRows;
598    }
599    bind() {
600        const pager = this;
601
602        for (const page of pager.pages) {
603            page.addEventListener("click", function () {
604                pager.apply(this.dataset.page);
605            });
606        }
607
608        pager.grid.element.querySelectorAll(".mvc-grid-pager-rows").forEach(rowsPerPage => {
609            rowsPerPage.addEventListener("change", function () {
610                const rows = parseInt(this.value);
611
612                if (!isNaN(rows) && rows >= 0) {
613                    const totalPages = rows === 0 ? 1 : Math.ceil(pager.totalRows / rows);
614
615                    pager.apply(Math.min(pager.currentPage, totalPages).toString(), rows.toString());
616                }
617            });
618        });
619    }
620}
621
622class MvcGridPopup {
623    static showConfiguration(grid, anchor) {
624        const popup = this;
625
626        popup.lastActiveElement = document.activeElement;
627        popup.element.className = "mvc-grid-popup mvc-grid-configuration";
628        popup.element.innerHTML = `<div class="popup-arrow"></div><div class="popup-content"></div>`;
629
630        const content = popup.element.querySelector(".popup-content");
631
632        content.appendChild(popup.createDropzone());
633
634        for (const column of grid.columns) {
635            content.appendChild(popup.createPreference(column));
636            content.appendChild(popup.createDropzone());
637        }
638
639        if (grid.columns.length) {
640            document.body.appendChild(popup.element);
641        }
642
643        popup.reposition(grid, anchor);
644        popup.bind();
645    }
646    static show(filter) {
647        if (!filter.instance) {
648            return;
649        }
650
651        const popup = this;
652        const filterer = filter.instance;
653
654        popup.lastActiveElement = document.activeElement;
655        popup.element.className = `mvc-grid-popup ${filterer.cssClasses}`.trim();
656        popup.element.innerHTML = `<div class="popup-arrow"></div><div class="popup-content">${filterer.render()}</div>`;
657
658        document.body.appendChild(popup.element);
659
660        popup.bind();
661        popup.setValues(filter);
662        popup.reposition(filter.column.grid, filter.button);
663
664        filterer.bindOperator();
665        filterer.bindMethods();
666        filterer.bindValues();
667        filterer.bindActions();
668
669        popup.element.querySelector(".mvc-grid-value").focus();
670    }
671    static hide(e) {
672        const popup = MvcGridPopup;
673        const initiator = e && e.target;
674        const visible = popup.element.parentNode;
675        const outside = !(initiator && initiator.closest(".mvc-grid-popup,.mvc-grid-filter"));
676
677        if (visible && outside) {
678            document.body.removeChild(popup.element);
679
680            if (popup.lastActiveElement) {
681                popup.lastActiveElement.focus();
682                popup.lastActiveElement = null;
683            }
684        }
685    }
686
687    static setValues(filter) {
688        const popup = this;
689
690        popup.setValue(`.mvc-grid-operator`, [filter.operator]);
691        popup.setValue(`.mvc-grid-value[data-filter="first"]`, filter.first.values);
692        popup.setValue(`.mvc-grid-value[data-filter="second"]`, filter.second.values);
693        popup.setValue(`.mvc-grid-method[data-filter="first"]`, [filter.first.method]);
694        popup.setValue(`.mvc-grid-method[data-filter="second"]`, [filter.second.method]);
695    }
696    static setValue(selector, values) {
697        const input = this.element.querySelector(selector);
698
699        if (input) {
700            if (input.tagName === "SELECT" && input.multiple) {
701                for (const option of Array.from(input.options)) {
702                    option.selected = values.indexOf(option.value) >= 0;
703                }
704            } else {
705                input.value = values[0] || "";
706            }
707        }
708    }
709    static createPreference(column) {
710        const popup = this;
711        const name = document.createElement("span");
712        const checkbox = document.createElement("input");
713        const preference = document.createElement("label");
714
715        checkbox.type = "checkbox";
716        preference.draggable = true;
717        preference.className = "mvc-grid-column";
718
719        if (column.filter && column.filter.inlineInput) {
720            name.innerText = column.filter.inlineInput.placeholder;
721        } else {
722            name.innerText = column.header.innerText.trim();
723        }
724
725        checkbox.checked = !column.isHidden;
726
727        checkbox.addEventListener("change", () => {
728            const i = column.grid.columns.indexOf(column);
729
730            for (const tr of column.grid.element.querySelectorAll("tr")) {
731                if (checkbox.checked) {
732                    tr.children[i].classList.remove("mvc-grid-hidden");
733                } else {
734                    tr.children[i].classList.add("mvc-grid-hidden");
735                }
736            }
737
738            column.isHidden = !checkbox.checked;
739
740            column.grid.element.dispatchEvent(new CustomEvent("gridconfigure", {
741                detail: { grid: column.grid },
742                bubbles: true
743            }));
744        });
745
746        preference.addEventListener("dragstart", () => {
747            popup.draggedColumn = column;
748            popup.draggedElement = preference;
749            preference.style.opacity = "0.4";
750            preference.parentElement.classList.add("mvc-grid-dragging");
751        });
752
753        preference.addEventListener("dragend", () => {
754            popup.draggedColumn = null;
755            popup.draggedElement = null;
756            preference.style.opacity = "";
757            preference.parentElement.classList.remove("mvc-grid-dragging");
758        });
759
760        preference.appendChild(checkbox);
761        preference.appendChild(name);
762
763        return preference;
764    }
765    static createDropzone() {
766        const dropzone = document.createElement("div");
767
768        dropzone.className = "mvc-grid-dropzone";
769
770        dropzone.addEventListener("dragenter", () => {
771            dropzone.classList.add("hover");
772        });
773
774        dropzone.addEventListener("dragover", e => {
775            e.preventDefault();
776        });
777
778        dropzone.addEventListener("dragleave", () => {
779            dropzone.classList.remove("hover");
780        });
781
782        dropzone.addEventListener("drop", () => {
783            const popup = this;
784            const dragged = popup.draggedElement;
785            const grid = popup.draggedColumn.grid;
786
787            if (dropzone !== dragged.previousElementSibling && dropzone !== dragged.nextElementSibling) {
788                const index = Array.from(popup.element.querySelectorAll(".mvc-grid-dropzone")).indexOf(dropzone);
789                const i = grid.columns.indexOf(popup.draggedColumn);
790
791                dropzone.parentElement.insertBefore(dragged.previousElementSibling, dropzone);
792                dropzone.parentElement.insertBefore(dragged, dropzone);
793
794                for (const tr of grid.element.querySelectorAll("tr")) {
795                    tr.insertBefore(tr.children[i], tr.children[index]);
796                }
797
798                grid.columns.splice(index - (i < index ? 1 : 0), 0, grid.columns.splice(i, 1)[0]);
799
800                grid.element.dispatchEvent(new CustomEvent("gridconfigure", {
801                    detail: { grid },
802                    bubbles: true
803                }));
804            }
805
806            dropzone.classList.remove("hover");
807        });
808
809        return dropzone;
810    }
811    static reposition(grid, anchor) {
812        const element = this.element;
813        const style = getComputedStyle(element);
814        const arrow = element.querySelector(".popup-arrow");
815        let { top, left } = (anchor || grid.element).getBoundingClientRect();
816
817        top += window.scrollY - parseFloat(style.borderTopWidth);
818        left += window.scrollX - parseFloat(style.borderLeftWidth);
819
820        if (anchor) {
821            left -= parseFloat(style.marginLeft) - anchor.offsetWidth / 2 + 26;
822
823            const arrowLeft = 26 - parseFloat(getComputedStyle(arrow).borderLeftWidth);
824            const width = parseFloat(style.marginLeft) + element.offsetWidth + parseFloat(style.marginRight);
825            const offset = Math.max(0, left + width - window.scrollX - document.documentElement.clientWidth);
826
827            top += anchor.offsetHeight / 3 * 2 + arrow.offsetHeight - parseFloat(style.marginTop);
828            arrow.style.left = `${Math.max(0, arrowLeft + offset)}px`;
829            left -= offset;
830        }
831
832        element.style.left = `${Math.max(0, left)}px`;
833        element.style.top = `${Math.max(0, top)}px`;
834        arrow.style.display = anchor ? "" : "none";
835    }
836    static bind() {
837        const popup = this;
838
839        window.addEventListener("mousedown", popup.hide);
840        window.addEventListener("touchstart", popup.hide);
841    }
842}
843
844MvcGridPopup.element = document.createElement("div");
845
846class MvcGridFilter {
847    constructor(column) {
848        const filter = this;
849
850        filter.column = column;
851        filter.type = column.filter.type;
852        filter.mode = column.grid.filterMode;
853        filter.methods = ["equals", "not-equals"];
854        filter.cssClasses = "mvc-grid-default-filter";
855    }
856
857    init() {
858        const filter = this;
859        const column = filter.column;
860        const columnFilter = column.filter;
861
862        if (!columnFilter.options && filter.mode !== "excel") {
863            filter.validate(columnFilter.inlineInput);
864        }
865
866        if (!columnFilter.first.method) {
867            columnFilter.first.method = columnFilter.defaultMethod;
868        }
869
870        if (!columnFilter.second.method) {
871            columnFilter.second.method = columnFilter.defaultMethod;
872        }
873
874        if (filter.methods.indexOf(columnFilter.first.method) < 0) {
875            columnFilter.first.method = filter.methods[0];
876        }
877
878        if (filter.methods.indexOf(columnFilter.second.method) < 0) {
879            columnFilter.second.method = filter.methods[0];
880        }
881    }
882    isValid(value) {
883        return !value || true;
884    }
885    validate(input) {
886        if (this.isValid(input.value)) {
887            input.classList.remove("invalid");
888        } else {
889            input.classList.add("invalid");
890        }
891    }
892
893    render() {
894        const filter = this;
895
896        return `<div class="popup-filter">
897                    ${filter.renderFilter("first")}
898                </div>
899                ${filter.mode === "excel" && filter.type === "double"
900                    ? `${filter.renderOperator()}
901                    <div class="popup-filter">
902                        ${filter.renderFilter("second")}
903                    </div>`
904                    : ""}
905                ${filter.renderActions()}`;
906    }
907    renderFilter(name) {
908        const filter = this;
909        const options = filter.column.filter.options;
910        const lang = MvcGrid.lang[filter.column.filter.name] || {};
911        const multiple = filter.type === "multi" ? " multiple" : "";
912        const methods = filter.methods.map(method => `<option value="${method}">${lang[method] || ""}</option>`).join("");
913
914        return `<div class="popup-group">
915                    <select class="mvc-grid-method" data-filter="${name}">
916                        ${methods}
917                    </select>
918                </div>
919                <div class="popup-group">${options
920                    ? `<select class="mvc-grid-value" data-filter="${name}"${multiple}>
921                          ${options.innerHTML}
922                       </select>`
923                    : `<input class="mvc-grid-value" data-filter="${name}">`}
924                </div>`;
925    }
926    renderOperator() {
927        const lang = MvcGrid.lang.operator;
928
929        return `<div class="popup-operator">
930                    <div class="popup-group">
931                        <select class="mvc-grid-operator">
932                            <option value="">${lang.select}</option>
933                            <option value="and">${lang.and}</option>
934                            <option value="or">${lang.or}</option>
935                        </select>
936                    </div>
937                </div>`;
938    }
939    renderActions() {
940        const lang = MvcGrid.lang.filter;
941
942        return `<div class="popup-actions">
943                    <button type="button" class="mvc-grid-apply" type="button">${lang.apply}</button>
944                    <button type="button" class="mvc-grid-cancel" type="button">${lang.remove}</button>
945                </div>`;
946    }
947    bindOperator() {
948        const filter = this.column.filter;
949        const operator = MvcGridPopup.element.querySelector(".mvc-grid-operator");
950
951        if (operator) {
952            operator.addEventListener("change", function () {
953                filter.operator = this.value;
954            });
955        }
956    }
957    bindMethods() {
958        const filter = this.column.filter;
959
960        for (const method of MvcGridPopup.element.querySelectorAll(".mvc-grid-method")) {
961            method.addEventListener("change", function () {
962                filter[this.dataset.filter].method = this.value;
963            });
964        }
965    }
966    bindValues() {
967        const filter = this;
968
969        for (const input of MvcGridPopup.element.querySelectorAll(".mvc-grid-value")) {
970            if (input.tagName === "SELECT") {
971                input.addEventListener("change", () => {
972                    const options = Array.from(input.options).filter(option => option.selected);
973
974                    filter.column.filter[input.dataset.filter].values = options.map(option => option.value);
975
976                    if (filter.mode !== "excel") {
977                        const inlineInput = filter.column.filter.inlineInput;
978
979                        if (filter.mode === "header" || filter.type === "multi") {
980                            inlineInput.value = options.map(option => option.text).join(", ");
981                        } else {
982                            inlineInput.value = input.value;
983                        }
984
985                        filter.validate(inlineInput);
986                    }
987                });
988            } else {
989                input.addEventListener("input", () => {
990                    filter.column.filter[input.dataset.filter].values = [input.value];
991
992                    if (filter.mode !== "excel") {
993                        const inlineInput = filter.column.filter.inlineInput;
994
995                        inlineInput.value = filter.column.filter[input.dataset.filter].values.join(", ");
996                        filter.validate(inlineInput);
997                    }
998
999                    filter.validate(input);
1000                });
1001
1002                input.addEventListener("keyup", function (e) {
1003                    if (e.key === "Enter" && filter.isValid(this.value)) {
1004                        filter.column.filter.apply();
1005                    }
1006                });
1007
1008                filter.validate(input);
1009            }
1010        }
1011    }
1012    bindActions() {
1013        const filter = this.column.filter;
1014        const popup = MvcGridPopup.element;
1015
1016        popup.querySelector(".mvc-grid-apply").addEventListener("click", filter.apply.bind(filter));
1017        popup.querySelector(".mvc-grid-cancel").addEventListener("click", filter.cancel.bind(filter));
1018    }
1019}
1020
1021class MvcGridTextFilter extends MvcGridFilter {
1022    constructor(column) {
1023        super(column);
1024
1025        this.cssClasses = "mvc-grid-text-filter";
1026        this.methods = ["contains", "consists-of", "equals", "not-equals", "starts-with", "ends-with"];
1027    }
1028}
1029
1030class MvcGridNumberFilter extends MvcGridFilter {
1031    constructor(column) {
1032        super(column);
1033
1034        this.cssClasses = "mvc-grid-number-filter";
1035        this.methods = ["equals", "not-equals", "less-than", "greater-than", "less-than-or-equal", "greater-than-or-equal"];
1036    }
1037
1038    isValid(value) {
1039        return !value || /^(?=.*\d+.*)[-+]?\d*[.,]?\d*$/.test(value);
1040    }
1041}
1042
1043class MvcGridDateFilter extends MvcGridFilter {
1044    constructor(column) {
1045        super(column);
1046
1047        this.cssClasses = "mvc-grid-date-filter";
1048        this.methods = ["equals", "not-equals", "earlier-than", "later-than", "earlier-than-or-equal", "later-than-or-equal"];
1049    }
1050}
1051
1052class MvcGridGuidFilter extends MvcGridFilter {
1053    constructor(column) {
1054        super(column);
1055
1056        this.cssClasses = "mvc-grid-guid-filter";
1057    }
1058
1059    isValid(value) {
1060        return !value || /^[0-9A-F]{8}-?([0-9A-F]{4}-?){3}[0-9A-F]{12}$/i.test(value);
1061    }
1062}

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.