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": "✓", 301 "remove": "✘" 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.