├── LICENSE ├── assets ├── ajax │ ├── index.ts │ └── naja.ts ├── css │ ├── datagrid-full.css │ ├── datagrid.css │ └── happy.css ├── datagrid-full.ts ├── datagrid.ts ├── index.ts ├── integrations │ ├── happy.ts │ ├── index.ts │ ├── sortable-js.ts │ ├── tom-select.ts │ └── vanilla-datepicker.ts ├── plugins │ ├── features │ │ ├── autosubmit.ts │ │ ├── checkboxes.ts │ │ ├── confirm.ts │ │ ├── editable.ts │ │ ├── inline.ts │ │ ├── item-detail.ts │ │ └── treeView.ts │ ├── index.ts │ └── integrations │ │ ├── datepicker.ts │ │ ├── happy.ts │ │ ├── nette-forms.ts │ │ ├── selectpicker.ts │ │ └── sortable.ts ├── types │ ├── ajax.d.ts │ ├── datagrid.d.ts │ ├── index.d.ts │ └── integrations.d.ts └── utils.ts ├── composer.json └── src ├── AggregationFunction ├── FunctionSum.php ├── IAggregatable.php ├── IAggregationFunction.php ├── IMultipleAggregationFunction.php ├── ISingleColumnAggregationFunction.php └── TDatagridAggregationFunction.php ├── Column ├── Action.php ├── Action │ └── Confirmation │ │ ├── CallbackConfirmation.php │ │ ├── IConfirmation.php │ │ └── StringConfirmation.php ├── ActionCallback.php ├── Column.php ├── ColumnDateTime.php ├── ColumnLink.php ├── ColumnNumber.php ├── ColumnStatus.php ├── ColumnText.php ├── FilterableColumn.php ├── ItemDetail.php ├── MultiAction.php └── Renderer.php ├── ColumnsSummary.php ├── Components └── DatagridPaginator │ ├── DatagridPaginator.php │ └── templates │ └── data_grid_paginator.latte ├── CsvDataModel.php ├── DataModel.php ├── DataSource ├── ApiDataSource.php ├── ArrayDataSource.php ├── DibiFluentDataSource.php ├── DibiFluentMssqlDataSource.php ├── DibiFluentPostgreDataSource.php ├── DoctrineCollectionDataSource.php ├── DoctrineDataSource.php ├── ElasticsearchDataSource.php ├── FilterableDataSource.php ├── IDataSource.php ├── NetteDatabaseTableDataSource.php ├── NetteDatabaseTableMssqlDataSource.php ├── NextrasDataSource.php └── SearchParamsBuilder.php ├── Datagrid.php ├── Exception ├── DatagridActionCallbackException.php ├── DatagridArrayDataSourceException.php ├── DatagridColumnException.php ├── DatagridColumnNotFoundException.php ├── DatagridColumnRendererException.php ├── DatagridColumnStatusException.php ├── DatagridDateTimeHelperException.php ├── DatagridException.php ├── DatagridFilterNotFoundException.php ├── DatagridFilterRangeException.php ├── DatagridGroupActionException.php ├── DatagridHasToBeAttachedToPresenterComponentException.php ├── DatagridItemDetailException.php ├── DatagridLinkCreationException.php └── DatagridWrongDataSourceException.php ├── Export ├── Export.php └── ExportCsv.php ├── Filter ├── Filter.php ├── FilterDate.php ├── FilterDateRange.php ├── FilterMultiSelect.php ├── FilterRange.php ├── FilterSelect.php ├── FilterText.php ├── IFilterDate.php ├── OneColumnFilter.php └── SubmitButton.php ├── GroupAction ├── GroupAction.php ├── GroupActionCollection.php ├── GroupButtonAction.php ├── GroupMultiSelectAction.php ├── GroupSelectAction.php ├── GroupTextAction.php └── GroupTextareaAction.php ├── InlineEdit ├── InlineAdd.php └── InlineEdit.php ├── Localization └── SimpleTranslator.php ├── Response └── CsvResponse.php ├── Row.php ├── Status └── Option.php ├── Toolbar └── ToolbarButton.php ├── Traits ├── TButtonCaret.php ├── TButtonClass.php ├── TButtonIcon.php ├── TButtonRenderer.php ├── TButtonText.php ├── TButtonTitle.php ├── TButtonTryAddIcon.php ├── TLink.php └── TRenderCondition.php ├── Utils ├── ArraysHelper.php ├── DateTimeHelper.php ├── ItemDetailForm.php ├── NetteDatabaseSelectionHelper.php ├── PropertyAccessHelper.php └── Sorting.php └── templates ├── column_multi_action.latte ├── column_status.latte ├── datagrid.latte ├── datagrid_filter_date.latte ├── datagrid_filter_daterange.latte ├── datagrid_filter_range.latte ├── datagrid_filter_select.latte ├── datagrid_filter_text.latte └── datagrid_tree.latte /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /assets/ajax/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./naja"; 2 | -------------------------------------------------------------------------------- /assets/ajax/naja.ts: -------------------------------------------------------------------------------- 1 | import type { Naja } from "naja"; 2 | import type { 3 | Ajax, 4 | AjaxEventMap as BaseAjaxEventMap, 5 | BaseRequestParams as AjaxBaseRequestParams, 6 | BeforeEventDetail as BaseBeforeEventDetail, 7 | DatagridPayload, 8 | ErrorEventDetail as BaseErrorEventDetail, 9 | EventDetail, 10 | EventListener, 11 | InteractEventDetail as BaseInteractEventDetail, 12 | Payload, 13 | RequestParams, 14 | Response as AjaxResponse, 15 | SuccessEventDetail as BaseSuccessEventDetail, 16 | } from "../types"; 17 | import { Datagrid } from "../datagrid"; 18 | import { BeforeEvent, ErrorEvent, Payload as NajaPayload, SuccessEvent } from "naja/dist/Naja"; 19 | import { InteractionEvent } from "naja/dist/core/UIHandler"; 20 | 21 | export interface BaseRequestParams extends AjaxBaseRequestParams, Request { 22 | url: string; 23 | method: string; 24 | } 25 | 26 | export interface BeforeEventDetail extends BaseBeforeEventDetail { 27 | params: EventDetail & RequestParams; 28 | } 29 | 30 | export interface InteractEventDetail< 31 | E extends HTMLElement = HTMLElement 32 | > extends BaseInteractEventDetail, EventDetail { 33 | element: E; 34 | } 35 | 36 | export interface SuccessEventDetail< 37 | P = DatagridPayload 38 | > extends BaseSuccessEventDetail, EventDetail { 39 | params: BaseRequestParams; 40 | payload: Payload

& NajaPayload; 41 | response: AjaxResponse & Response; 42 | } 43 | 44 | export interface ErrorEventDetail< 45 | E extends Error = Error, 46 | > extends BaseErrorEventDetail, EventDetail { 47 | params: BaseRequestParams; 48 | response: (AjaxResponse & Response) | undefined; 49 | error: E; 50 | } 51 | 52 | export interface AjaxEventMap extends BaseAjaxEventMap { 53 | before: CustomEvent; 54 | interact: CustomEvent; 55 | snippetUpdate: CustomEvent; 56 | success: CustomEvent; 57 | error: CustomEvent; 58 | } 59 | 60 | export class NajaAjax extends EventTarget implements Ajax { 61 | constructor(public client: C) { 62 | if (!client.VERSION || client.VERSION < 2) { 63 | throw new Error("NajaAjax supports Naja 2 and higher" + (client.VERSION ? `(version ${client.VERSION} provided)` : '')) 64 | } 65 | super(); 66 | } 67 | 68 | onInit() { 69 | this.client.addEventListener('before', (e) => { 70 | return this.dispatch('before', { 71 | params: e.detail 72 | }); 73 | }) 74 | 75 | this.client.uiHandler.addEventListener('interaction', (e) => { 76 | if (!(e.detail.element instanceof HTMLElement)) { 77 | throw new Error("Element is not an instanceof HTMLElement"); 78 | } 79 | 80 | const dispatchResult = this.dispatch('interact', { 81 | ...e.detail, 82 | element: e.detail.element as HTMLElement // Naja's event has a type of HTMLElement 83 | }); 84 | 85 | if (!dispatchResult) { 86 | e.stopPropagation(); 87 | e.preventDefault(); 88 | } 89 | 90 | return dispatchResult; 91 | }) 92 | 93 | 94 | this.client.addEventListener('success', (e) => { 95 | return this.dispatch('success', { 96 | ...e.detail, 97 | params: e.detail.request, 98 | payload: e.detail.payload as Payload 99 | }); 100 | }) 101 | 102 | this.client.addEventListener('error', (e) => { 103 | return this.dispatch('error', { 104 | ...e.detail, 105 | params: e.detail.request, 106 | response: e.detail.response, 107 | }); 108 | }) 109 | 110 | this.client.addEventListener('complete', (e) => { 111 | return this.dispatch('complete', { 112 | ...e.detail, 113 | params: e.detail.request, 114 | response: e.detail.response, 115 | }); 116 | }) 117 | 118 | return this; 119 | } 120 | 121 | async request(args: RequestParams): Promise

{ 122 | return await this.client.makeRequest(args.method, args.url, args.data) as P; 123 | } 124 | 125 | async submitForm(element: E): Promise

{ 126 | return await this.client.uiHandler.submitForm(element) as P; 127 | } 128 | 129 | dispatch< 130 | K extends string, M extends BaseAjaxEventMap = AjaxEventMap 131 | >(type: K, detail: K extends keyof M ? EventDetail : any, options?: boolean): boolean { 132 | return this.dispatchEvent(new CustomEvent(type, { 133 | detail: detail, 134 | cancelable: true, 135 | })); 136 | 137 | } 138 | 139 | declare addEventListener: ( 140 | type: K, 141 | listener: EventListener, 142 | options?: boolean | AddEventListenerOptions 143 | ) => void; 144 | 145 | declare removeEventListener: ( 146 | type: K, 147 | listener: EventListener, 148 | options?: boolean | AddEventListenerOptions 149 | ) => void; 150 | 151 | declare dispatchEvent: ( 152 | event: K extends keyof M ? M[K] : CustomEvent 153 | ) => boolean; 154 | } 155 | -------------------------------------------------------------------------------- /assets/css/datagrid-full.css: -------------------------------------------------------------------------------- 1 | @import 'vanillajs-datepicker/dist/css/datepicker-bs5.css'; 2 | @import "tom-select/dist/css/tom-select.css"; 3 | @import './happy.css'; 4 | @import './datagrid.css'; 5 | -------------------------------------------------------------------------------- /assets/datagrid-full.ts: -------------------------------------------------------------------------------- 1 | import naja from "naja"; 2 | import { default as netteForms } from "nette-forms"; 3 | import { 4 | AutosubmitPlugin, 5 | CheckboxPlugin, 6 | ConfirmPlugin, 7 | createDatagrids, 8 | DatepickerPlugin, 9 | Happy, 10 | HappyPlugin, 11 | InlinePlugin, 12 | ItemDetailPlugin, 13 | NetteFormsPlugin, 14 | SelectpickerPlugin, 15 | SortableJS, 16 | SortablePlugin, 17 | TomSelect, 18 | TreeViewPlugin, 19 | VanillaDatepicker, 20 | } from "." 21 | import { NajaAjax } from "./ajax"; 22 | import Select from "tom-select"; 23 | import { Dropdown } from "bootstrap"; 24 | 25 | // Datagrid + UI 26 | document.addEventListener("DOMContentLoaded", () => { 27 | // Initialize dropdowns 28 | Array.from(document.querySelectorAll('.dropdown')) 29 | .forEach(el => new Dropdown(el)) 30 | 31 | // Initialize Naja (nette ajax) 32 | naja.formsHandler.netteForms = netteForms; 33 | naja.initialize(); 34 | 35 | // Initialize datagrids 36 | createDatagrids(new NajaAjax(naja), { 37 | datagrid: { 38 | plugins: [ 39 | new AutosubmitPlugin(), 40 | new CheckboxPlugin(), 41 | new ConfirmPlugin(), 42 | new InlinePlugin(), 43 | new ItemDetailPlugin(), 44 | new NetteFormsPlugin(netteForms), 45 | new HappyPlugin(new Happy()), 46 | new SortablePlugin(new SortableJS()), 47 | new DatepickerPlugin(new VanillaDatepicker({ buttonClass: 'btn' })), 48 | new SelectpickerPlugin(new TomSelect(Select)), 49 | new TreeViewPlugin(), 50 | ], 51 | }, 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /assets/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./datagrid"; 2 | export * from "./plugins"; 3 | export * from "./integrations"; 4 | export * from "./datagrid"; 5 | -------------------------------------------------------------------------------- /assets/integrations/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./happy"; 2 | export * from "./sortable-js"; 3 | export * from "./tom-select"; 4 | export * from "./vanilla-datepicker"; 5 | -------------------------------------------------------------------------------- /assets/integrations/sortable-js.ts: -------------------------------------------------------------------------------- 1 | import { Datagrid } from ".."; 2 | import { Sortable as SortableInterface } from "../types"; 3 | import Sortable from "sortablejs" 4 | 5 | export class SortableJS implements SortableInterface { 6 | initSortable(datagrid: Datagrid): void { 7 | const sortable = datagrid.el.querySelector("[data-sortable]"); 8 | if (sortable) { 9 | new Sortable(sortable, { 10 | handle: '.handle-sort', 11 | draggable: 'tr', 12 | sort: true, 13 | direction: 'vertical', 14 | async onEnd({item}) { 15 | const itemId = item.getAttribute("data-id"); 16 | if (itemId) { 17 | const prevId = item.previousElementSibling?.getAttribute("data-id") ?? null; 18 | const nextId = item.nextElementSibling?.getAttribute("data-id") ?? null; 19 | 20 | const tbody = datagrid.el.querySelector("tbody"); 21 | 22 | if (tbody) { 23 | let componentPrefix = tbody.getAttribute("data-sortable-parent-path") ?? ''; 24 | if (componentPrefix.length) componentPrefix = `${componentPrefix}-`; 25 | 26 | const url = tbody.getAttribute("data-sortable-url") ?? "?do=sort"; 27 | 28 | const data = { 29 | [`${componentPrefix}item_id`]: itemId, 30 | ...(prevId ? {[`${componentPrefix}prev_id`]: prevId} : {}), 31 | ...(nextId ? {[`${componentPrefix}next_id`]: nextId} : {}), 32 | }; 33 | 34 | return await datagrid.ajax.request({ 35 | method: "GET", 36 | url, 37 | data, 38 | }) 39 | } 40 | } 41 | }, 42 | }) 43 | } 44 | } 45 | 46 | initSortableTree(datagrid: Datagrid): void { 47 | datagrid.el.querySelectorAll(".datagrid-tree-item-children").forEach((el) => { 48 | new Sortable(el, { 49 | handle: '.handle-sort', 50 | draggable: '.datagrid-tree-item:not(.datagrid-tree-header)', 51 | async onEnd({item}) { 52 | // TODO 53 | }, 54 | }) 55 | }) 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /assets/integrations/tom-select.ts: -------------------------------------------------------------------------------- 1 | import { Constructor, Selectpicker } from "../types"; 2 | import { RecursivePartial, TomInput, TomSettings } from "tom-select/dist/types/types"; 3 | import type TomSelectType from "tom-select"; 4 | import { window } from "../utils"; 5 | 6 | export class TomSelect implements Selectpicker { 7 | constructor( 8 | private select?: Constructor, 9 | private opts: RecursivePartial | ((input: HTMLElement | TomInput) => RecursivePartial) = {} 10 | ) { 11 | } 12 | 13 | initSelectpickers(elements: HTMLElement[]): void { 14 | const Select = this.select ?? window()?.TomSelect ?? null; 15 | 16 | if (Select) { 17 | for (const element of elements) { 18 | // Check if TomSelect is already initialized on the element 19 | if (element.tomselect) { 20 | continue; 21 | } 22 | 23 | new Select( 24 | element as TomInput, 25 | typeof this.opts === "function" ? this.opts(element) : this.opts 26 | ) 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /assets/integrations/vanilla-datepicker.ts: -------------------------------------------------------------------------------- 1 | import { Datepicker as DatepickerInterface } from "../types"; 2 | import { Datepicker } from "vanillajs-datepicker"; 3 | import { DatepickerOptions } from "vanillajs-datepicker/Datepicker"; 4 | 5 | export class VanillaDatepicker implements DatepickerInterface { 6 | constructor(private opts: DatepickerOptions | ((input: HTMLInputElement) => DatepickerOptions) = {}) { 7 | } 8 | 9 | initDatepickers(elements: HTMLInputElement[]): void { 10 | elements.forEach((element) => { 11 | const options = typeof this.opts === "function" ? this.opts(element) : this.opts; 12 | const picker = new Datepicker(element, { 13 | ...options, 14 | updateOnBlur: false 15 | }); 16 | 17 | element.addEventListener('changeDate', () => { 18 | const form = element.closest('form'); 19 | if (form) { 20 | form.submit(); 21 | } 22 | }); 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /assets/plugins/features/autosubmit.ts: -------------------------------------------------------------------------------- 1 | import { DatagridPlugin } from "../../types"; 2 | import { debounce, isEnter, isFunctionKey, isInKeyRange } from "../../utils"; 3 | import { Datagrid } from "../.."; 4 | 5 | export const AutosubmitAttribute = "data-autosubmit"; 6 | 7 | export const AutosubmitPerPageAttribute = "data-autosubmit-per-page"; 8 | 9 | export const AutosubmitChangeAttribute = "data-autosubmit-change"; 10 | 11 | export class AutosubmitPlugin implements DatagridPlugin { 12 | onDatagridInit(datagrid: Datagrid): boolean { 13 | datagrid.ajax.addEventListener('complete', (event) => { 14 | this.initPerPage(datagrid); 15 | this.initChange(datagrid); 16 | }); 17 | 18 | this.initPerPage(datagrid); 19 | this.initChange(datagrid); 20 | 21 | return true; 22 | }; 23 | 24 | // Auto-submit perPage 25 | initPerPage(datagrid: Datagrid) { 26 | datagrid.el.querySelectorAll(`select[${AutosubmitPerPageAttribute}]`) 27 | .forEach(pageSelectEl => { 28 | pageSelectEl.addEventListener("change", () => { 29 | let inputEl = pageSelectEl.parentElement?.querySelector("input[type=submit]"); 30 | if (!inputEl) { 31 | inputEl = pageSelectEl.parentElement?.querySelector("button[type=submit]"); 32 | } 33 | console.log({ inputEl }); 34 | if (!(inputEl instanceof HTMLElement)) return; 35 | const form = inputEl.closest('form'); 36 | console.log({ form }); 37 | form && datagrid.ajax.submitForm(form); 38 | }); 39 | }); 40 | } 41 | 42 | // Auto-submit change 43 | initChange(datagrid: Datagrid) { 44 | datagrid.el.querySelectorAll(`[${AutosubmitAttribute}]`) 45 | .forEach(submitEl => { 46 | const form = submitEl.closest("form"); 47 | if (!form) return; 48 | 49 | if (submitEl.dataset.listenersAttached === "true") { 50 | return; // Skip if listeners are already attached 51 | } 52 | submitEl.dataset.listenersAttached = "true"; 53 | 54 | // Select auto-submit 55 | if (submitEl instanceof HTMLSelectElement) { 56 | submitEl.addEventListener("change", () => datagrid.ajax.submitForm(form)); 57 | 58 | return; 59 | } 60 | 61 | // Input auto-submit 62 | if (submitEl instanceof HTMLInputElement) { 63 | // Handle change events 64 | if (submitEl.hasAttribute(AutosubmitChangeAttribute)) { 65 | submitEl.addEventListener( 66 | "change", 67 | debounce(() => datagrid.ajax.submitForm(form)) 68 | ); 69 | } 70 | 71 | submitEl.addEventListener( 72 | "keyup", 73 | debounce(e => { 74 | // Ignore keys such as alt, ctrl, etc, F-keys... (when enter is not pressed) 75 | if (!isEnter(e) && (isInKeyRange(e, 9, 40) || isFunctionKey(e))) { 76 | return; 77 | } 78 | 79 | return datagrid.ajax.submitForm(form); 80 | }) 81 | ); 82 | } 83 | }); 84 | } 85 | } 86 | 87 | 88 | -------------------------------------------------------------------------------- /assets/plugins/features/checkboxes.ts: -------------------------------------------------------------------------------- 1 | import { DatagridPlugin } from "../../types"; 2 | import { Datagrid } from "../.."; 3 | 4 | export const CheckboxAttribute = "data-check"; 5 | 6 | export class CheckboxPlugin implements DatagridPlugin { 7 | onDatagridInit(datagrid: Datagrid): boolean { 8 | let lastCheckbox = null; 9 | 10 | datagrid.el.addEventListener("click", e => { 11 | if (!(e.target instanceof HTMLElement)) return; 12 | 13 | if (e.target.classList.contains("col-checkbox")) { 14 | lastCheckbox = e.target; 15 | if (e.shiftKey && lastCheckbox) { 16 | const currentCheckboxRow = lastCheckbox.closest("tr"); 17 | if (!currentCheckboxRow) return; 18 | 19 | const lastCheckboxRow = lastCheckbox.closest("tr"); 20 | if (!lastCheckboxRow) return; 21 | 22 | const lastCheckboxTbody = lastCheckboxRow.closest("tbody"); 23 | if (!lastCheckboxTbody) return; 24 | 25 | const checkboxesRows = Array.from(lastCheckboxTbody.querySelectorAll("tr")); 26 | const [start, end] = [lastCheckboxRow.rowIndex, currentCheckboxRow.rowIndex].sort(); 27 | const rows = checkboxesRows.slice(start, end + 1); 28 | 29 | rows.forEach(row => { 30 | const input = row.querySelector('.col-checkbox input[type="checkbox"]'); 31 | if (input) { 32 | input.checked = true; 33 | } 34 | }); 35 | } 36 | } 37 | }); 38 | 39 | let checkboxes = datagrid.el.querySelectorAll(`input[data-check='${datagrid.name}']`); 40 | const select = datagrid.el.querySelector("select[name='group_action[group_action]']"); 41 | const actionButtons = document.querySelectorAll( 42 | ".row-group-actions *[type='submit']" 43 | ); 44 | const counter = document.querySelector(".datagrid-selected-rows-count"); 45 | 46 | // Handling a checkbox click + select all checkbox 47 | checkboxes.forEach(checkEl => { 48 | checkEl.addEventListener("change", () => { 49 | // Select all 50 | const isSelectAll = checkEl.hasAttribute("data-check-all"); 51 | if (isSelectAll) { 52 | if (datagrid.name !== checkEl.getAttribute("data-check-all")) return; 53 | 54 | checkboxes.forEach(checkbox => (checkbox.checked = checkEl.checked)); 55 | 56 | // todo: refactor not to repeat this code twice 57 | actionButtons.forEach(button => (button.disabled = !checkEl.checked)); 58 | 59 | if (select) { 60 | select.disabled = !checkEl.checked; 61 | } 62 | 63 | if (counter) { 64 | const total = Array.from(checkboxes).filter(c => !c.hasAttribute("data-check-all")).length; 65 | counter.innerText = `${checkEl.checked ? total : 0}/${total}`; 66 | } 67 | return; 68 | } else { 69 | const selectAll = datagrid.el.querySelector(`input[data-check='${datagrid.name}'][data-check-all]`); 70 | if (selectAll) { 71 | selectAll.checked = Array.from(checkboxes).filter(c => !c.hasAttribute("data-check-all")).every(c => c.checked); 72 | } 73 | } 74 | 75 | const checkedBoxes = Array.from(checkboxes).filter(checkbox => checkbox.checked && !checkEl.hasAttribute("data-check-all")); 76 | const hasChecked = checkedBoxes.length >= 1; 77 | 78 | actionButtons.forEach(button => (button.disabled = !hasChecked)); 79 | 80 | if (select) { 81 | select.disabled = !hasChecked; 82 | } 83 | 84 | if (counter) { 85 | counter.innerText = `${checkedBoxes.length}/${checkboxes.length}`; 86 | } 87 | }); 88 | }); 89 | 90 | return true; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /assets/plugins/features/confirm.ts: -------------------------------------------------------------------------------- 1 | import { Datagrid } from "../../datagrid"; 2 | import { DatagridPlugin } from "../../types"; 3 | 4 | export const ConfirmAttribute = "data-datagrid-confirm"; 5 | 6 | export class ConfirmPlugin implements DatagridPlugin { 7 | onDatagridInit(datagrid: Datagrid): boolean { 8 | datagrid.el 9 | .querySelectorAll(`[${ConfirmAttribute}]:not(.ajax)`) 10 | .forEach(confirmEl => 11 | confirmEl.addEventListener("click", e => this.confirmEventHandler.bind(datagrid)(e.target as HTMLElement, e)) 12 | ); 13 | 14 | datagrid.ajax.addEventListener("interact", e => this.confirmEventHandler.bind(datagrid)(e.detail.element, e)); 15 | 16 | return true; 17 | } 18 | 19 | confirmEventHandler(this: Datagrid, el: HTMLElement, e: Event) { 20 | const message = el.closest('a')?.getAttribute(ConfirmAttribute)!; 21 | if (!message) return; 22 | 23 | if (!window.confirm.bind(window)(message)) { 24 | e.stopPropagation(); 25 | e.preventDefault(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /assets/plugins/features/editable.ts: -------------------------------------------------------------------------------- 1 | import { DatagridPlugin } from "../../types"; 2 | import { calculateCellLines, isEnter, isEsc } from "../../utils"; 3 | import { Datagrid } from "../.."; 4 | 5 | export const EditableUrlAttribute = "data-datagrid-editable-url"; 6 | 7 | export const EditableTypeAttribute = "data-datagrid-editable-type"; 8 | 9 | export const EditableElementAttribute = "data-datagrid-editable-element"; 10 | 11 | export const EditableValueAttribute = "data-datagrid-editable-value"; 12 | 13 | export const EditableAttrsAttribute = "datagrid-editable-attrs"; 14 | 15 | export class EditablePlugin implements DatagridPlugin { 16 | onDatagridInit(datagrid: Datagrid): boolean { 17 | datagrid.el.querySelectorAll(`[${EditableUrlAttribute}]`).forEach(cell => { 18 | if (cell instanceof HTMLAnchorElement || cell.classList.contains("datagrid-inline-edit")) return; 19 | 20 | if (!cell.classList.contains("editing")) { 21 | cell.classList.add("editing"); 22 | const originalValue = cell.innerHTML.replace(/<\/?br>/g, "\n"); 23 | const valueToEdit = cell.getAttribute(EditableValueAttribute) ?? originalValue; 24 | 25 | cell.setAttribute("originalValue", originalValue); 26 | cell.setAttribute("valueToEdit", valueToEdit); 27 | 28 | const type = cell.getAttribute(EditableTypeAttribute) ?? "text"; 29 | 30 | let input: HTMLElement; 31 | 32 | switch (type) { 33 | case "textarea": 34 | cell.innerHTML = ``; 35 | input = cell.querySelector("textarea")!; 36 | break; 37 | case "select": 38 | input = cell.querySelector(cell.getAttribute(EditableElementAttribute) ?? "")!; 39 | input 40 | .querySelectorAll(`option[value='${valueToEdit}']`) 41 | .forEach(input => input.setAttribute("selected", "true")); 42 | break; 43 | default: 44 | cell.innerHTML = ``; 45 | input = cell.querySelector("input")!; 46 | } 47 | 48 | const attributes = JSON.parse(cell.getAttribute(EditableAttrsAttribute) ?? "{}"); 49 | for (const key in attributes) { 50 | const value = attributes[key]; 51 | input.setAttribute(key, value); 52 | } 53 | 54 | cell.classList.remove("edited"); 55 | 56 | const submitCell = async (el: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement) => { 57 | let value = el.value; 58 | if (value !== valueToEdit) { 59 | try { 60 | const response = await datagrid.ajax.request({ 61 | url: cell.getAttribute(EditableUrlAttribute) ?? "", 62 | method: "POST", 63 | data: { 64 | value, 65 | }, 66 | }) as any; 67 | 68 | if (type === "select") { 69 | cell.innerHTML = cell.querySelector(`option[value='${value}']`)?.innerHTML ?? ""; 70 | } else { 71 | if (response._datagrid_editable_new_value) { 72 | value = response._datagrid_editable_new_value; 73 | } 74 | cell.innerHTML = value; 75 | } 76 | cell.classList.add("edited"); 77 | } catch { 78 | cell.innerHTML = originalValue; 79 | cell.classList.add("edited-error"); 80 | } 81 | } else { 82 | cell.innerHTML = originalValue; 83 | } 84 | 85 | cell.classList.remove("editing"); 86 | }; 87 | 88 | cell 89 | .querySelectorAll( 90 | "input, textarea, select" 91 | ) 92 | .forEach(el => { 93 | el.addEventListener("blur", () => submitCell(el)); 94 | el.addEventListener("keydown", e => { 95 | if (isEnter(e as KeyboardEvent)) { 96 | e.stopPropagation(); 97 | e.preventDefault(); 98 | return submitCell(el); 99 | } 100 | 101 | if (isEsc(e as KeyboardEvent)) { 102 | e.stopPropagation(); 103 | e.preventDefault(); 104 | cell.classList.remove("editing"); 105 | cell.innerHTML = originalValue; 106 | } 107 | }); 108 | 109 | if (el instanceof HTMLSelectElement) { 110 | el.addEventListener("change", () => submitCell(el)); 111 | } 112 | }); 113 | } 114 | }); 115 | 116 | return true; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /assets/plugins/features/inline.ts: -------------------------------------------------------------------------------- 1 | import { DatagridPlugin } from "../../types"; 2 | import { isEnter } from "../../utils"; 3 | import { Datagrid, Datagrids } from "../.."; 4 | 5 | export class InlinePlugin implements DatagridPlugin { 6 | onInit(datagrids: Datagrids) { 7 | } 8 | 9 | onDatagridInit(datagrid: Datagrid): boolean { 10 | datagrid.ajax.addEventListener('success', ({ detail: { payload } }) => { 11 | if (!payload._datagrid_name || payload._datagrid_name !== datagrid.name) return; 12 | 13 | if (payload._datagrid_inline_edited || payload._datagrid_inline_edit_cancel) { 14 | const trigger = datagrid.el.querySelector('.datagrid-inline-edit-trigger'); 15 | 16 | if (payload._datagrid_inline_edited) { 17 | let rows = datagrid.el.querySelectorAll( 18 | `tr[data-id='${payload._datagrid_inline_edited}'] > td` 19 | ); 20 | 21 | rows.forEach(row => { 22 | row.classList.add("edited"); 23 | }) 24 | } 25 | 26 | trigger?.classList.remove("hidden"); 27 | return; 28 | } 29 | 30 | if (payload._datagrid_inline_adding) { 31 | const row = datagrid.el.querySelector(".datagrid-row-inline-add"); 32 | if (row) { 33 | row.classList.remove("datagrid-row-inline-add-hidden"); 34 | row.querySelector( 35 | "input:not([readonly]), textarea:not([readonly])" 36 | )?.focus(); 37 | } 38 | } 39 | 40 | if (payload._datagrid_inline_editing) { 41 | datagrid.el.querySelector(".datagrid-inline-edit-trigger") 42 | ?.classList.add("hidden"); 43 | } 44 | 45 | datagrid.el.querySelectorAll(".datagrid-inline-edit input").forEach(inputEl => { 46 | inputEl.addEventListener("keydown", e => { 47 | if (!isEnter(e)) return; 48 | 49 | e.stopPropagation(); 50 | e.preventDefault(); 51 | 52 | return inputEl 53 | .closest("tr") 54 | ?.querySelector(".col-action-inline-edit [name='inline_edit[submit]']") 55 | ?.click(); 56 | }); 57 | }); 58 | 59 | datagrid.el.querySelectorAll(".datagrid-inline-add input").forEach(inputEl => { 60 | inputEl.addEventListener("keydown", e => { 61 | if (!isEnter(e)) return; 62 | 63 | e.stopPropagation(); 64 | e.preventDefault(); 65 | 66 | return inputEl 67 | .closest("tr") 68 | ?.querySelector(".col-action-inline-edit [name='inline_add[submit]']") 69 | ?.click(); 70 | }); 71 | }); 72 | 73 | datagrid.el.querySelectorAll("[data-datagrid-cancel-inline-add]").forEach(cancel => { 74 | cancel.addEventListener("mouseup", e => { 75 | if (e.button === 0) { 76 | e.stopPropagation(); 77 | e.preventDefault(); 78 | const inlineAdd = cancel.closest(".datagrid-row-inline-add"); 79 | if (inlineAdd) { 80 | inlineAdd.classList.add("datagrid-row-inline-add-hidden"); 81 | } 82 | } 83 | }); 84 | }); 85 | }) 86 | 87 | return true; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /assets/plugins/features/item-detail.ts: -------------------------------------------------------------------------------- 1 | import { Datagrid } from "../.."; 2 | import { DatagridPlugin } from "../../types"; 3 | 4 | export class ItemDetailPlugin implements DatagridPlugin { 5 | onDatagridInit(datagrid: Datagrid): boolean { 6 | datagrid.el.querySelectorAll("[data-toggle-detail-grid]") 7 | .forEach((el) => { 8 | if (el.getAttribute("data-toggle-detail-grid") !== datagrid.name) return; 9 | const toggleId = el.getAttribute("data-toggle-detail")!; 10 | 11 | 12 | el.addEventListener("click", (e) => { 13 | const contentRow = datagrid.el.querySelector( 14 | `.item-detail-${datagrid.name}-id-${toggleId}` 15 | ); 16 | 17 | const gridRow = el.closest('tr'); 18 | 19 | if (contentRow) { 20 | // const div = contentRow.querySelector("td > div"); 21 | // if (div && !el.classList.contains("datagrid--slide-toggle")) { 22 | // attachSlideToggle(div, el); 23 | // } TODO: fix 24 | contentRow.classList.add("datagrid--content-row") 25 | contentRow.classList.toggle("is-active"); 26 | 27 | } 28 | 29 | datagrid.ajax.addEventListener("before", (e) => { 30 | if (e.detail.params.url.includes(`do=${datagrid.name}-getItemDetail`) && e.detail.params.url.includes(`grid-id=${toggleId}`)) { 31 | e.stopPropagation(); 32 | e.preventDefault(); 33 | } 34 | }) 35 | }) 36 | }); 37 | 38 | datagrid.ajax.addEventListener("success", ({detail: {payload}}) => { 39 | if (payload._datagrid_redraw_item_id && payload._datagrid_redraw_item_class) { 40 | datagrid.el.querySelector( 41 | `tr[data-id='${payload._datagrid_redraw_item_id}']` 42 | )?.setAttribute("class", payload._datagrid_redraw_item_class) 43 | } 44 | }) 45 | 46 | return true; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /assets/plugins/features/treeView.ts: -------------------------------------------------------------------------------- 1 | import { DatagridPlugin } from "../../types"; 2 | import { Datagrid } from "../.."; 3 | 4 | export class TreeViewPlugin implements DatagridPlugin { 5 | onDatagridInit(datagrid: Datagrid): boolean { 6 | datagrid.ajax.addEventListener("before", (e) => { 7 | }) 8 | 9 | datagrid.ajax.addEventListener("success", ({detail: {payload}}) => { 10 | if (payload._datagrid_tree) { 11 | const id = payload._datagrid_tree; 12 | const rowBlock = document.querySelector(`.datagrid-tree-item[data-id="${id}"]`); 13 | const childrenBlock = document.querySelector(`.datagrid-tree-item[data-id="${id}"] .datagrid-tree-item-children`); 14 | if (childrenBlock) { 15 | if (childrenBlock.classList.contains('showed')) { 16 | childrenBlock.innerHTML = ''; 17 | childrenBlock.classList.remove('showed'); 18 | rowBlock.querySelector(`a.chevron`).style.transform = "rotate(0deg)"; 19 | return; 20 | } 21 | 22 | childrenBlock.classList.add('showed'); 23 | rowBlock.querySelector(`a.chevron`).style.transform = "rotate(90deg)"; 24 | const snippets = payload.snippets; 25 | for (const snippetName in snippets) { 26 | const snippet = snippets[snippetName]; 27 | const snippetDocEl = new DOMParser().parseFromString(snippet, "text/html") 28 | .querySelector("[data-id]"); 29 | 30 | const id = snippetDocEl?.getAttribute("data-id") ?? ''; 31 | const hasChildren = snippetDocEl?.hasAttribute("data-has-children") ?? false; 32 | 33 | const template = `\n

${snippet}
`; 34 | 35 | childrenBlock.innerHTML = template; 36 | } 37 | //children_block.addClass('loaded'); 38 | //children_block.slideToggle('fast'); 39 | } 40 | } 41 | }) 42 | return true; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /assets/plugins/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./integrations/datepicker"; 2 | export * from "./integrations/happy"; 3 | export * from "./integrations/nette-forms" 4 | export * from "./integrations/selectpicker"; 5 | export * from "./integrations/sortable"; 6 | 7 | export * from "./features/autosubmit"; 8 | export * from "./features/checkboxes"; 9 | export * from "./features/confirm"; 10 | export * from "./features/editable"; 11 | export * from "./features/inline"; 12 | export * from "./features/item-detail"; 13 | export * from "./features/treeView"; 14 | -------------------------------------------------------------------------------- /assets/plugins/integrations/datepicker.ts: -------------------------------------------------------------------------------- 1 | import { Datagrid } from "../.."; 2 | import { DatagridPlugin, Datepicker } from "../../types"; 3 | 4 | export class DatepickerPlugin implements DatagridPlugin { 5 | constructor(private datepicker: Datepicker) { 6 | } 7 | 8 | onDatagridInit(datagrid: Datagrid): boolean { 9 | datagrid.ajax.addEventListener('complete', (event) => { 10 | this.initDatepicker(datagrid); 11 | }); 12 | 13 | this.initDatepicker(datagrid); 14 | 15 | return true; 16 | } 17 | 18 | private initDatepicker(datagrid: Datagrid): void { 19 | const elements = datagrid.el.querySelectorAll("input[data-provide='datepicker']"); 20 | 21 | if (elements.length >= 1) { 22 | this.datepicker.initDatepickers(Array.from(elements), datagrid); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /assets/plugins/integrations/happy.ts: -------------------------------------------------------------------------------- 1 | import { Datagrid } from "../.."; 2 | import { DatagridPlugin } from "../../types"; 3 | import { window } from "../../utils"; 4 | import type { Happy } from "../../integrations"; 5 | 6 | export class HappyPlugin implements DatagridPlugin { 7 | constructor(private happy?: Happy) { 8 | } 9 | 10 | onDatagridInit(datagrid: Datagrid): boolean { 11 | const happy = this.happy ?? window().happy ?? null; 12 | 13 | if (happy) { 14 | happy.init(); 15 | } 16 | 17 | return true; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /assets/plugins/integrations/nette-forms.ts: -------------------------------------------------------------------------------- 1 | import { DatagridPlugin, Nette } from "../../types"; 2 | import { Datagrid } from "../.."; 3 | import { window } from "../../utils"; 4 | 5 | export class NetteFormsPlugin implements DatagridPlugin { 6 | constructor(private nette?: Nette) { 7 | } 8 | 9 | onDatagridInit(datagrid: Datagrid): boolean { 10 | const nette = this.nette ?? window().Nette ?? null; 11 | 12 | if (nette) { 13 | datagrid.el.querySelectorAll("form").forEach(form => nette.initForm(form)); 14 | } 15 | 16 | return true; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /assets/plugins/integrations/selectpicker.ts: -------------------------------------------------------------------------------- 1 | import { DatagridPlugin, Selectpicker } from "../../types"; 2 | import { Datagrid } from "../.."; 3 | 4 | export class SelectpickerPlugin implements DatagridPlugin { 5 | constructor(private selectpicker: Selectpicker) { 6 | } 7 | 8 | onDatagridInit(datagrid: Datagrid): boolean { 9 | datagrid.ajax.addEventListener('complete', (event) => { 10 | this.initSelectpicker(datagrid); 11 | }); 12 | 13 | this.initSelectpicker(datagrid); 14 | 15 | return true; 16 | } 17 | 18 | private initSelectpicker(datagrid: Datagrid): void { 19 | const elements = datagrid.el.querySelectorAll("select.selectpicker"); 20 | 21 | if (elements.length >= 1) { 22 | this.selectpicker.initSelectpickers(Array.from(elements), datagrid); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /assets/plugins/integrations/sortable.ts: -------------------------------------------------------------------------------- 1 | import { Datagrid } from "../../datagrid"; 2 | import { DatagridPlugin, Sortable } from "../../types"; 3 | 4 | export class SortablePlugin implements DatagridPlugin { 5 | constructor(private sortable: Sortable) { 6 | } 7 | 8 | onDatagridInit(datagrid: Datagrid): boolean { 9 | datagrid.ajax.addEventListener('before', (event) => { 10 | // TODO old ln 694... wtf? 11 | }) 12 | 13 | this.sortable.initSortable(datagrid); 14 | 15 | datagrid.ajax.addEventListener('success', ({detail: {payload}}) => { 16 | if (payload._datagrid_sort) { 17 | for (const key in payload._datagrid_sort) { 18 | const href = payload._datagrid_sort[key]; 19 | const element = datagrid.el.querySelector(`#datagrid-sort-${key}`); 20 | 21 | if (element) { 22 | // TODO: Only for BC support, to be removed 23 | element.setAttribute("href", href); 24 | 25 | element.setAttribute("data-href", href); 26 | } 27 | } 28 | this.sortable.initSortable(datagrid); 29 | } 30 | 31 | if (payload._datagrid_tree) { 32 | const childrenContainer = datagrid.el.querySelector( 33 | `.datagrid-tree-item[data-id='${payload._datagrid_tree}'] .datagrid-tree-item-children` 34 | ); 35 | if (childrenContainer && payload.snippets) { 36 | childrenContainer.classList.add("loaded"); 37 | for (const key in payload.snippets) { 38 | const snippet = payload.snippets[key]; 39 | 40 | const doc = new DOMParser().parseFromString(snippet, 'text/html'); 41 | const element = doc.firstElementChild; 42 | if (element) { 43 | const treeItem = document.createElement("div"); 44 | treeItem.id = key; 45 | treeItem.classList.add("datagrid-tree-item") 46 | treeItem.setAttribute("data-id", key); 47 | if (element.hasAttribute("has-children")) { 48 | treeItem.classList.add("has-children"); 49 | } 50 | 51 | childrenContainer.append(treeItem); 52 | // attachSlideToggle(childrenContainer); 53 | } 54 | } 55 | } 56 | this.sortable.initSortableTree(datagrid); 57 | } 58 | }) 59 | return true; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /assets/types/ajax.d.ts: -------------------------------------------------------------------------------- 1 | import { EventDetail, EventListener, EventMap } from "."; 2 | import { Datagrid } from ".."; 3 | 4 | export interface BaseRequestParams { 5 | method: "GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "CONNECT" | "OPTIONS" | "TRACE" | "PATCH" | string; 6 | url: string; 7 | } 8 | 9 | export interface RequestParams extends BaseRequestParams { 10 | data: D; 11 | } 12 | 13 | export interface DatagridPayload { 14 | _datagrid_name?: string; 15 | _datagrid_toggle_detail?: string 16 | _datagrid_inline_editing?: boolean; 17 | _datagrid_inline_adding?: boolean; 18 | _datagrid_inline_edited?: boolean; 19 | _datagrid_inline_edit_cancel?: boolean; 20 | _datagrid_url?: boolean; 21 | _datagrid_sort?: Record; 22 | _datagrid_tree?: string; 23 | _datagrid_editable_new_value?: string; 24 | _datagrid_redraw_item_id?: string; 25 | _datagrid_redraw_item_class?: string; 26 | _datagrid_init?: boolean; 27 | non_empty_filters?: string[]; 28 | } 29 | 30 | export interface DatagridState { 31 | "grid-page": number | null, 32 | "grid-perPage": number, 33 | // TODO 34 | "grid-sort": any | null, 35 | "grid-filter": any | null 36 | } 37 | 38 | export type Payload

= P & { 39 | snippets?: Record; 40 | redirect?: string; 41 | state: S; 42 | }; 43 | 44 | export interface Response { 45 | headers: Record | Headers; 46 | status: number; 47 | } 48 | 49 | export interface BeforeEventDetail { 50 | params: RequestParams; 51 | } 52 | 53 | export interface InteractEventDetail { 54 | element: E; 55 | } 56 | 57 | export interface SuccessEventDetail

{ 58 | params: BaseRequestParams; 59 | payload: Payload

; 60 | response: Response; 61 | } 62 | 63 | export interface CompleteEventDetail

{ 64 | params: BaseRequestParams; 65 | payload: Payload

; 66 | response: Response; 67 | } 68 | 69 | export interface ErrorEventDetail { 70 | params: BaseRequestParams; 71 | response?: Response; 72 | error?: E; 73 | } 74 | 75 | export interface AjaxEventMap extends EventMap { 76 | before: CustomEvent; 77 | interact: CustomEvent; 78 | snippetUpdate: CustomEvent; 79 | success: CustomEvent; 80 | complete: CustomEvent; 81 | error: CustomEvent; 82 | } 83 | 84 | export interface Ajax extends EventTarget { 85 | client: C; 86 | 87 | /** 88 | * Initialization of the Ajax instance, called in createDatagrids(). 89 | * @return this 90 | */ 91 | onInit(): this; 92 | 93 | /** 94 | * Initializes a Datagrid instance. 95 | * @param grid The Datagrid instance 96 | */ 97 | onDatagridInit?(grid: G): void; 98 | 99 | /** 100 | * Sends a request to the server. 101 | */ 102 | request(args: RequestParams): Promise

; 103 | 104 | /** 105 | * Submits a form 106 | */ 107 | submitForm(element: E): Promise

; 108 | 109 | /** 110 | * Shortcut for dispatchEvent 111 | * @internal 112 | */ 113 | dispatch( 114 | type: K, 115 | detail: K extends keyof M ? EventDetail : any, 116 | options?: boolean 117 | ): boolean; 118 | 119 | /** 120 | * Note: For events dispatched directly from the underlying client, {@see Ajax.client}} 121 | **/ 122 | addEventListener( 123 | type: K, 124 | listener: EventListener, 125 | options?: boolean | AddEventListenerOptions 126 | ): void; 127 | 128 | /** 129 | * Note: For events dispatched directly from the underlying client, {@see Ajax.client}} 130 | **/ 131 | removeEventListener( 132 | type: K, 133 | listener: EventListener, 134 | options?: boolean | AddEventListenerOptions 135 | ): void; 136 | 137 | /** 138 | * @internal 139 | */ 140 | dispatchEvent( 141 | event: K extends keyof M ? M[K] : CustomEvent 142 | ): boolean; 143 | } 144 | -------------------------------------------------------------------------------- /assets/types/datagrid.d.ts: -------------------------------------------------------------------------------- 1 | import { Datagrid, Datagrids } from ".."; 2 | import { EventMap } from "."; 3 | 4 | export interface DatagridEventDetail { 5 | datagrid: Datagrid; 6 | } 7 | 8 | export interface DatagridEventMap extends EventMap { 9 | beforeInit: CustomEvent; 10 | afterInit: CustomEvent; 11 | } 12 | 13 | export interface DatagridPlugin { 14 | onInit?(datagrids: Datagrids): void; 15 | 16 | onDatagridInit?(datagrid: Datagrid): boolean; 17 | } 18 | 19 | export interface DatagridOptions { 20 | confirm(this: Datagrid, message: string): boolean; 21 | 22 | // Returning null will skip this datagrid 23 | resolveDatagridName: (this: Datagrid, datagrid: HTMLElement) => string | null; 24 | plugins: DatagridPlugin[]; 25 | } 26 | 27 | export interface DatagridsOptions { 28 | datagrid: Partial; 29 | selector: string; 30 | root: HTMLElement | string; 31 | } 32 | -------------------------------------------------------------------------------- /assets/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Happy } from "../integrations"; 2 | import TomSelect from "tom-select"; 3 | 4 | export interface Nette { 5 | initForm: (form: HTMLFormElement) => void; 6 | } 7 | 8 | export type Constructor = new (...args: any[]) => T; 9 | 10 | export type KeysOf = { [P in keyof T]: TVal; } 11 | 12 | export interface ExtendedWindow extends Window { 13 | jQuery?: any; 14 | Nette?: Nette; 15 | TomSelect?: Constructor; 16 | happy?: Happy; 17 | } 18 | 19 | // https://github.com/naja-js/naja/blob/384d298a9199bf778985d1bcf5747fe8de305b22/src/utils.ts 20 | type EventListenerFunction = ( 21 | this: ET, 22 | event: E 23 | ) => boolean | void | Promise; 24 | 25 | interface EventListenerObject { 26 | handleEvent(event: E): void | Promise; 27 | } 28 | 29 | export type EventListener = 30 | | EventListenerFunction 31 | | EventListenerObject 32 | | null; 33 | 34 | export type EventDetail = E extends CustomEvent ? D : never; 35 | 36 | export interface EventMap extends Record { 37 | } 38 | 39 | export * from "./datagrid"; 40 | export * from "./integrations"; 41 | export * from "./ajax"; 42 | -------------------------------------------------------------------------------- /assets/types/integrations.d.ts: -------------------------------------------------------------------------------- 1 | import { Datagrid } from ".."; 2 | 3 | export interface Sortable { 4 | initSortable(datagrid: Datagrid): void; 5 | 6 | initSortableTree(datagrid: Datagrid): void; 7 | } 8 | 9 | export interface Selectpicker { 10 | initSelectpickers(elements: HTMLElement[], datagrid: Datagrid): void; 11 | } 12 | 13 | export interface Datepicker { 14 | initDatepickers(elements: HTMLInputElement[], datagrid: Datagrid): void; 15 | } 16 | -------------------------------------------------------------------------------- /assets/utils.ts: -------------------------------------------------------------------------------- 1 | import { Datagrid } from "./datagrid"; 2 | import { ExtendedWindow } from "./types"; 3 | 4 | export function isPromise(p: any): p is Promise { 5 | return typeof p === "object" && typeof p.then === "function"; 6 | } 7 | 8 | export function isInKeyRange(e: KeyboardEvent, min: number, max: number): boolean { 9 | const code = e.key.length === 1 ? e.key.charCodeAt(0) : 0; 10 | return code >= min && code <= max; 11 | } 12 | 13 | export function isEnter(e: KeyboardEvent): boolean { 14 | return e.key === "Enter"; 15 | } 16 | 17 | export function isEsc(e: KeyboardEvent): boolean { 18 | return e.key === "Escape"; 19 | } 20 | 21 | export function isFunctionKey(e: KeyboardEvent): boolean { 22 | return e.key.length === 2 && e.key.startsWith("F"); 23 | } 24 | 25 | export function window(): ExtendedWindow { 26 | return (window ?? {}) as unknown as ExtendedWindow; 27 | } 28 | 29 | export function slideDown(element: HTMLElement, cb?: (nextStateShown: boolean) => unknown) { 30 | element.style.height = 'auto'; 31 | 32 | let height = element.clientHeight + "px"; 33 | 34 | element.style.height = '0px'; 35 | 36 | setTimeout(function () { 37 | element.style.height = height; 38 | cb?.(true); 39 | }, 0); 40 | } 41 | 42 | export function slideUp(element: HTMLElement, cb?: (nextStateShown: boolean) => unknown) { 43 | element.style.height = '0px'; 44 | 45 | setTimeout(() => { 46 | cb?.(false); 47 | }, 250); // TODO 48 | } 49 | 50 | export function slideToggle(element: HTMLElement, isVisible: boolean, cb?: (nextStateShown: boolean) => unknown) { 51 | if (!isVisible) { 52 | slideDown(element, cb); 53 | } else { 54 | slideUp(element, cb); 55 | } 56 | } 57 | 58 | export function attachSlideToggle(element: HTMLElement, control: HTMLElement, cb?: (nextStateShown: boolean) => unknown) { 59 | if (!control.classList.contains("datagrid--slide-toggle")) { 60 | let sliding = false; 61 | control.classList.add("datagrid--slide-toggle"); 62 | 63 | slideDown(element, cb); 64 | 65 | control.addEventListener('click', () => { 66 | if (sliding) return; 67 | sliding = true; 68 | slideToggle(element, control.classList.contains('is-active'), (active) => { 69 | sliding = false 70 | if (active) { 71 | control.classList.add("is-active"); 72 | } else { 73 | control.classList.remove("is-active"); 74 | } 75 | }); 76 | }); 77 | } 78 | } 79 | 80 | export function qs(params: Record, prefix: string = ""): string { 81 | const encodedParams = []; 82 | 83 | for (const _key in params) { 84 | const value = params[_key]; 85 | // Cannot do !value as that would also exclude valid negative values such as 0 or false 86 | if (value === null || value === undefined) continue; 87 | 88 | const key = prefix ? `${prefix}[${_key}]` : _key; 89 | 90 | // Skip empty strings 91 | if (typeof value === "string" && value.trim().length < 1) continue; 92 | 93 | if (typeof value === "object") { 94 | const nestedParams = qs(value, key); 95 | // Don't include if object is empty 96 | if (nestedParams.length >= 1) { 97 | encodedParams.push(nestedParams); 98 | } 99 | 100 | continue; 101 | } 102 | 103 | encodedParams.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`) 104 | } 105 | 106 | return encodedParams.join("&").replace(/&+$/gm, "").replace(/&*$/, ""); 107 | } 108 | 109 | export function calculateCellLines(el: HTMLElement) { 110 | const cellPadding = el.style.padding ? parseInt(el.style.padding.replace(/[^-\d\.]/g, ""), 10) : 0; 111 | const cellHeight = el.getBoundingClientRect().height; 112 | const lineHeight = Math.round(parseFloat(el.style.lineHeight ?? "0")); 113 | const cellLines = Math.round((cellHeight - 2 * cellPadding) / lineHeight); 114 | 115 | return cellLines; 116 | } 117 | 118 | // A little better debounce ;) 119 | export function debounce unknown | Promise>( 120 | fn: TFun, 121 | slowdown: number = 200 122 | ): (...args: TArgs[]) => void { 123 | let timeout: ReturnType | null = null; 124 | let blockedByPromise: boolean = false; 125 | 126 | return (...args) => { 127 | if (blockedByPromise) return; 128 | 129 | timeout && clearTimeout(timeout); 130 | timeout = setTimeout(() => { 131 | const result = fn(...args); 132 | 133 | if (isPromise(result)) { 134 | blockedByPromise = true; 135 | result.finally(() => { 136 | blockedByPromise = false; 137 | }); 138 | } 139 | }, slowdown); 140 | }; 141 | } 142 | 143 | export function defaultDatagridNameResolver(this: Datagrid, datagrid: HTMLElement) { 144 | // This attribute is not present by default, though if you're going to use this library 145 | // it's recommended to add it, because when not present, the fallback way is to parse the datagrid- class, 146 | // which is definitely far from reliable. Alternatively (mainly in case of a custom datagrid class), 147 | // you can pass your own resolveDatagridName function to the option. 148 | const attrName = datagrid.getAttribute("data-datagrid-name"); 149 | if (attrName) return attrName; 150 | 151 | console.warn( 152 | "Deprecated name resolution for datagrid", 153 | datagrid, 154 | ": Please add a data-datagrid-name attribute instead!\n" + 155 | "Currently, the Datagrid library relies on matching the name from the 'datagrid-[name]' class, which is unreliable " + 156 | "and may cause bugs if the default class names are not used (eg. if you add a datagrid-xx class, or change the name class completely!)\n" + 157 | "Alternatively, you can customize the name resolution with the `resolveDatagridName` option. See TBD for more info." // TODO 158 | ); 159 | 160 | const classes = datagrid.classList.value.split(" "); 161 | 162 | // Returns the first datagrid-XXX match 163 | for (const className of classes) { 164 | if (!className.startsWith("datagrid-")) continue; 165 | 166 | const [, ...split] = className.split("-"); 167 | const name = split.join("-"); 168 | 169 | // In case nothing actually follows the prefix (className = "datagrid-") 170 | if (name.length < 1) { 171 | console.error(`Failed to resolve datagrid name - ambigious class name '${className}'`); 172 | return null; 173 | } 174 | 175 | return name; 176 | } 177 | 178 | return null; 179 | } 180 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ublaboo/datagrid", 3 | "type": "library", 4 | "description": "DataGrid for Nette Framework: filtering, sorting, pagination, tree view, table view, translator, etc", 5 | "keywords": [ 6 | "datagrid", 7 | "grid", 8 | "nette", 9 | "contributte", 10 | "table", 11 | "data" 12 | ], 13 | "license": [ 14 | "MIT" 15 | ], 16 | "authors": [ 17 | { 18 | "name": "Pavel Janda", 19 | "homepage": "https://paveljanda.com" 20 | }, 21 | { 22 | "name": "Milan Felix Šulc", 23 | "homepage": "https://f3l1x.io" 24 | } 25 | ], 26 | "require": { 27 | "php": ">=8.2", 28 | "nette/application": "^3.2.0", 29 | "nette/di": "^3.0.0", 30 | "nette/forms": "^3.2.0", 31 | "nette/utils": "^4.0.0", 32 | "symfony/property-access": "^6.4.0 || ^7.2.0" 33 | }, 34 | "require-dev": { 35 | "contributte/qa": "^0.3.0", 36 | "dibi/dibi": "^5.0.2", 37 | "doctrine/annotations": "^1.14.4", 38 | "doctrine/cache": "^1.13.0", 39 | "doctrine/orm": "^2.20.2", 40 | "elasticsearch/elasticsearch": "^8.6", 41 | "mockery/mockery": "^1.6.12", 42 | "nette/database": "^3.0.2", 43 | "nette/tester": "^2.3.4", 44 | "nextras/dbal": "^4.0 || ^5.0", 45 | "nextras/orm": "^4.0 || ^5.0", 46 | "phpstan/phpstan-deprecation-rules": "^1.1", 47 | "phpstan/phpstan-mockery": "^1.1", 48 | "phpstan/phpstan-nette": "^1.0.0", 49 | "phpstan/phpstan-strict-rules": "^1.4", 50 | "tharos/leanmapper": "^3.4.2 || ^4.0.0", 51 | "tracy/tracy": "^2.6.3" 52 | }, 53 | "conflict": { 54 | "doctrine/collections": "<2.2.0" 55 | }, 56 | "autoload": { 57 | "psr-4": { 58 | "Contributte\\Datagrid\\": "src/" 59 | } 60 | }, 61 | "autoload-dev": { 62 | "psr-4": { 63 | "Contributte\\Datagrid\\Tests\\": "tests" 64 | } 65 | }, 66 | "prefer-stable": true, 67 | "minimum-stability": "dev", 68 | "config": { 69 | "sort-packages": true, 70 | "allow-plugins": { 71 | "composer/package-versions-deprecated": true, 72 | "dealerdirect/phpcodesniffer-composer-installer": true, 73 | "php-http/discovery": true 74 | } 75 | }, 76 | "extra": { 77 | "branch-alias": { 78 | "dev-master": "7.1.x-dev" 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/AggregationFunction/FunctionSum.php: -------------------------------------------------------------------------------- 1 | dataType; 28 | } 29 | 30 | public function processDataSource(Fluent|QueryBuilder|Collection|Selection|ICollection $dataSource): void 31 | { 32 | if ($dataSource instanceof Fluent) { 33 | $connection = $dataSource->getConnection(); 34 | $this->result = (int) $connection->select('SUM(%n)', $this->column) 35 | ->from($dataSource, 's') 36 | ->fetchSingle(); 37 | } 38 | 39 | if ($dataSource instanceof QueryBuilder) { 40 | $column = str_contains($this->column, '.') 41 | ? $this->column 42 | : current($dataSource->getRootAliases()) . '.' . $this->column; 43 | 44 | $this->result = (int) $dataSource 45 | ->select(sprintf('SUM(%s)', $column)) 46 | ->setMaxResults(1) 47 | ->setFirstResult(0) 48 | ->getQuery() 49 | ->getSingleScalarResult(); 50 | } 51 | 52 | if ($dataSource instanceof Collection) { 53 | $dataSource->forAll(function ($key, $value): bool { 54 | $this->result += PropertyAccessHelper::getValue($value, $this->column); 55 | 56 | return true; 57 | }); 58 | } 59 | 60 | if ($dataSource instanceof DbalCollection) { 61 | foreach ($dataSource->fetchAll() as $item) 62 | $this->result += $item->getValue($this->column); 63 | } 64 | } 65 | 66 | public function renderResult(): mixed 67 | { 68 | $result = $this->result; 69 | 70 | if (isset($this->renderer)) { 71 | $result = call_user_func($this->renderer, $result); 72 | } 73 | 74 | return $result; 75 | } 76 | 77 | /** 78 | * @return static 79 | */ 80 | public function setRenderer(?callable $callback = null): self 81 | { 82 | $this->renderer = $callback; 83 | 84 | return $this; 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/AggregationFunction/IAggregatable.php: -------------------------------------------------------------------------------- 1 | hasColumnsSummary()) { 27 | throw new DatagridException('You can use either ColumnsSummary or AggregationFunctions'); 28 | } 29 | 30 | if (!$this->dataModel instanceof DataModel) { 31 | throw new DatagridException('You have to set a data source first.'); 32 | } 33 | 34 | if (isset($this->aggregationFunctions[$key])) { 35 | throw new DatagridException('There is already a AggregationFunction defined on column ' . $key); 36 | } 37 | 38 | if ($this->multipleAggregationFunction instanceof IMultipleAggregationFunction) { 39 | throw new DatagridException('You can not use both AggregationFunctions and MultipleAggregationFunction'); 40 | } 41 | 42 | $this->aggregationFunctions[$key] = $aggregationFunction; 43 | 44 | return $this; 45 | } 46 | 47 | /** 48 | * @return static 49 | * @throws DatagridException 50 | */ 51 | public function setMultipleAggregationFunction( 52 | IMultipleAggregationFunction $multipleAggregationFunction 53 | ): self 54 | { 55 | if ($this->hasColumnsSummary()) { 56 | throw new DatagridException('You can use either ColumnsSummary or AggregationFunctions'); 57 | } 58 | 59 | if ($this->aggregationFunctions !== []) { 60 | throw new DatagridException('You can not use both AggregationFunctions and MultipleAggregationFunction'); 61 | } 62 | 63 | $this->multipleAggregationFunction = $multipleAggregationFunction; 64 | 65 | return $this; 66 | } 67 | 68 | /** 69 | * @throws DatagridException 70 | */ 71 | public function beforeDataModelFilter(IDataSource $dataSource): void 72 | { 73 | if (!$this->hasSomeAggregationFunction()) { 74 | return; 75 | } 76 | 77 | if (!$dataSource instanceof IAggregatable) { 78 | throw new DatagridException('Used DataSource has to implement IAggregatable for aggegations to work'); 79 | } 80 | 81 | if ($this->multipleAggregationFunction !== null) { 82 | $type = $this->multipleAggregationFunction->getFilterDataType(); 83 | 84 | if ($type === IAggregationFunction::DATA_TYPE_ALL) { 85 | $dataSource->processAggregation($this->multipleAggregationFunction); 86 | } 87 | 88 | return; 89 | } 90 | 91 | foreach ($this->aggregationFunctions as $aggregationFunction) { 92 | if ($aggregationFunction->getFilterDataType() === IAggregationFunction::DATA_TYPE_ALL) { 93 | $dataSource->processAggregation($aggregationFunction); 94 | } 95 | } 96 | } 97 | 98 | /** 99 | * @throws DatagridException 100 | */ 101 | public function afterDataModelFilter(IDataSource $dataSource): void 102 | { 103 | if (!$this->hasSomeAggregationFunction()) { 104 | return; 105 | } 106 | 107 | if (!$dataSource instanceof IAggregatable) { 108 | throw new DatagridException('Used DataSource has to implement IAggregatable for aggegations to work'); 109 | } 110 | 111 | if ($this->multipleAggregationFunction !== null) { 112 | if ($this->multipleAggregationFunction->getFilterDataType() === IAggregationFunction::DATA_TYPE_FILTERED) { 113 | $dataSource->processAggregation($this->multipleAggregationFunction); 114 | } 115 | 116 | return; 117 | } 118 | 119 | foreach ($this->aggregationFunctions as $aggregationFunction) { 120 | if ($aggregationFunction->getFilterDataType() === IAggregationFunction::DATA_TYPE_FILTERED) { 121 | $dataSource->processAggregation($aggregationFunction); 122 | } 123 | } 124 | } 125 | 126 | /** 127 | * @throws DatagridException 128 | */ 129 | public function afterDataModelPaginated(IDataSource $dataSource): void 130 | { 131 | if (!$this->hasSomeAggregationFunction()) { 132 | return; 133 | } 134 | 135 | if (!$dataSource instanceof IAggregatable) { 136 | throw new DatagridException('Used DataSource has to implement IAggregatable for aggegations to work'); 137 | } 138 | 139 | if ($this->multipleAggregationFunction !== null) { 140 | if ($this->multipleAggregationFunction->getFilterDataType() === IAggregationFunction::DATA_TYPE_PAGINATED) { 141 | $dataSource->processAggregation($this->multipleAggregationFunction); 142 | } 143 | 144 | return; 145 | } 146 | 147 | foreach ($this->aggregationFunctions as $aggregationFunction) { 148 | if ($aggregationFunction->getFilterDataType() === IAggregationFunction::DATA_TYPE_PAGINATED) { 149 | $dataSource->processAggregation($aggregationFunction); 150 | } 151 | } 152 | } 153 | 154 | public function hasSomeAggregationFunction(): bool 155 | { 156 | return $this->aggregationFunctions !== [] || $this->multipleAggregationFunction !== null; 157 | } 158 | 159 | /** 160 | * @return array 161 | */ 162 | public function getAggregationFunctions(): array 163 | { 164 | return $this->aggregationFunctions; 165 | } 166 | 167 | public function getMultipleAggregationFunction(): ?IMultipleAggregationFunction 168 | { 169 | return $this->multipleAggregationFunction; 170 | } 171 | 172 | } 173 | -------------------------------------------------------------------------------- /src/Column/Action/Confirmation/CallbackConfirmation.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 14 | } 15 | 16 | public function getCallback(): callable 17 | { 18 | return $this->callback; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/Column/Action/Confirmation/IConfirmation.php: -------------------------------------------------------------------------------- 1 | question; 15 | } 16 | 17 | public function getPlaceholderName(): ?string 18 | { 19 | return $this->placeholderName; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/Column/ActionCallback.php: -------------------------------------------------------------------------------- 1 | href is a identifier of user callback 26 | */ 27 | $params += ['__key' => $this->href]; 28 | 29 | return $this->grid->link('actionCallback!', $params); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/Column/ColumnDateTime.php: -------------------------------------------------------------------------------- 1 | format($this->format); 29 | } catch (DatagridDateTimeHelperException) { 30 | /** 31 | * Otherwise just return raw string 32 | */ 33 | return (string) $value; 34 | } 35 | } 36 | 37 | return $value->format($this->format); 38 | } 39 | 40 | /** 41 | * @return static 42 | */ 43 | public function setFormat(string $format): self 44 | { 45 | $this->format = $format; 46 | 47 | return $this; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/Column/ColumnLink.php: -------------------------------------------------------------------------------- 1 | useRenderer($row); 44 | } catch (DatagridColumnRendererException) { 45 | /** 46 | * Do not use renderer 47 | */ 48 | } 49 | 50 | $value = parent::render($row); 51 | 52 | if (! (bool) $value && $this->icon !== null) { 53 | return null; 54 | } 55 | 56 | $a = Html::el('a') 57 | ->href($this->createLink( 58 | $this->grid, 59 | $this->href, 60 | $this->getItemParams($row, $this->params) + $this->parameters 61 | )); 62 | 63 | if ($this->dataAttributes !== []) { 64 | foreach ($this->dataAttributes as $key => $attrValue) { 65 | $a->data((string) $key, $attrValue); 66 | } 67 | } 68 | 69 | if ($this->openInNewTab) { 70 | $a->addAttributes(['target' => '_blank']); 71 | } 72 | 73 | if ($this->title !== null) { 74 | $a->setAttribute('title', $this->title); 75 | } 76 | 77 | if ($this->class !== null) { 78 | $a->setAttribute('class', $this->class); 79 | } 80 | 81 | $element = $a; 82 | 83 | if ($this->icon !== null) { 84 | $a->addHtml( 85 | Html::el('span')->setAttribute('class', Datagrid::$iconPrefix . $this->icon) 86 | ); 87 | 88 | if (strlen($value) > 0) { 89 | $a->addHtml(' '); 90 | } 91 | } 92 | 93 | if ($this->isTemplateEscaped()) { 94 | $a->addText($value); 95 | } else { 96 | $a->addHtml($value); 97 | } 98 | 99 | return $element; 100 | } 101 | 102 | /** 103 | * @return static 104 | */ 105 | public function addParameters(array $parameters): self 106 | { 107 | $this->parameters = $parameters; 108 | 109 | return $this; 110 | } 111 | 112 | /** 113 | * @return static 114 | */ 115 | public function setIcon(?string $icon = null): self 116 | { 117 | $this->icon = $icon; 118 | 119 | return $this; 120 | } 121 | 122 | /** 123 | * @return static 124 | */ 125 | public function setDataAttribute(string $key, mixed $value): self 126 | { 127 | $this->dataAttributes[$key] = $value; 128 | 129 | return $this; 130 | } 131 | 132 | /** 133 | * @return static 134 | */ 135 | public function setTitle(string $title): self 136 | { 137 | $this->title = $title; 138 | 139 | return $this; 140 | } 141 | 142 | public function getTitle(): ?string 143 | { 144 | return $this->title; 145 | } 146 | 147 | /** 148 | * @return static 149 | */ 150 | public function setClass(?string $class): self 151 | { 152 | $this->class = $class; 153 | 154 | return $this; 155 | } 156 | 157 | public function getClass(): ?string 158 | { 159 | return $this->class; 160 | } 161 | 162 | public function isOpenInNewTab(): bool 163 | { 164 | return $this->openInNewTab; 165 | } 166 | 167 | /** 168 | * @return static 169 | */ 170 | public function setOpenInNewTab(bool $openInNewTab = true): self 171 | { 172 | $this->openInNewTab = $openInNewTab; 173 | 174 | return $this; 175 | } 176 | 177 | } 178 | -------------------------------------------------------------------------------- /src/Column/ColumnNumber.php: -------------------------------------------------------------------------------- 1 | self::PHP_FLOAT_SAFE_MIN && $value < self::PHP_FLOAT_SAFE_MAX) { 37 | return number_format( 38 | (float) $value, 39 | (int) $this->numberFormat[0], 40 | (string) $this->numberFormat[1], 41 | (string) $this->numberFormat[2] 42 | ); 43 | } 44 | 45 | return $this->parseBigIntNumber((string) $value); 46 | } 47 | 48 | /** 49 | * @return static 50 | */ 51 | public function setFormat( 52 | int $decimals = 0, 53 | string $decPoint = '.', 54 | string $thousandsSep = ' ' 55 | ): self 56 | { 57 | $this->numberFormat = [$decimals, $decPoint, $thousandsSep]; 58 | 59 | return $this; 60 | } 61 | 62 | public function getFormat(): array 63 | { 64 | return [ 65 | $this->numberFormat[0], 66 | $this->numberFormat[1], 67 | $this->numberFormat[2], 68 | ]; 69 | } 70 | 71 | protected function parseBigIntNumber(string $number): string 72 | { 73 | $decimal = null; 74 | 75 | if (str_contains($number, '.')) { 76 | [$integer, $decimal] = explode('.', $number, 2); 77 | } else { 78 | $integer = $number; 79 | } 80 | 81 | if ($this->numberFormat[0] > 0) { 82 | $decimal = substr( 83 | $decimal . str_repeat('0', $this->numberFormat[0]), 84 | 0, 85 | $this->numberFormat[0] 86 | ); 87 | } 88 | 89 | $integer = preg_replace('/\B(?=(\d{3})+(?!\d))/', $this->numberFormat[2], $integer); 90 | 91 | if (strlen((string) $decimal) > 0) { 92 | return $integer . $this->numberFormat[1] . $decimal; 93 | } 94 | 95 | return (string) $integer; 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/Column/ColumnStatus.php: -------------------------------------------------------------------------------- 1 | key = $key; 40 | 41 | $this->setTemplate(__DIR__ . '/../templates/column_status.latte'); 42 | } 43 | 44 | public function getKey(): string 45 | { 46 | return $this->key; 47 | } 48 | 49 | public function getOptions(): array 50 | { 51 | return $this->options; 52 | } 53 | 54 | /** 55 | * @throws DatagridColumnStatusException 56 | */ 57 | public function getOption(mixed $value): Option 58 | { 59 | foreach ($this->options as $option) { 60 | if ($option->getValue() === $value) { 61 | return $option; 62 | } 63 | } 64 | 65 | throw new DatagridColumnStatusException(sprintf('Option [%s] does not exist', $value)); 66 | } 67 | 68 | public function getColumn(): string 69 | { 70 | return $this->column; 71 | } 72 | 73 | /** 74 | * Find selected option for current item/row 75 | */ 76 | public function getCurrentOption(Row $row): ?Option 77 | { 78 | foreach ($this->getOptions() as $option) { 79 | if ($option->getValue() === $row->getValue($this->getColumn())) { 80 | return $option; 81 | } 82 | } 83 | 84 | return null; 85 | } 86 | 87 | /** 88 | * @throws DatagridColumnStatusException 89 | */ 90 | public function addOption(mixed $value, string $text): Option 91 | { 92 | if (!is_scalar($value)) { 93 | throw new DatagridColumnStatusException('Option value has to be scalar'); 94 | } 95 | 96 | $option = new Option($this->grid, $this, $value, $text); 97 | 98 | $this->options[] = $option; 99 | 100 | return $option; 101 | } 102 | 103 | /** 104 | * Set all options at once 105 | * 106 | * @param string[] $options 107 | * @return static 108 | */ 109 | public function setOptions(array $options): self 110 | { 111 | foreach ($options as $value => $text) { 112 | $this->addOption($value, $text); 113 | } 114 | 115 | return $this; 116 | } 117 | 118 | public function removeOption(mixed $value): void 119 | { 120 | foreach ($this->options as $key => $option) { 121 | if ($option->getValue() === $value) { 122 | unset($this->options[$key]); 123 | } 124 | } 125 | } 126 | 127 | /** 128 | * Column can have variables that will be passed to custom template scope 129 | */ 130 | public function getTemplateVariables(): array 131 | { 132 | return array_merge($this->templateVariables, [ 133 | 'options' => $this->getOptions(), 134 | 'column' => $this->getColumn(), 135 | 'key' => $this->getKey(), 136 | 'status' => $this, 137 | ]); 138 | } 139 | 140 | public function setReplacement(array $replacements): Column 141 | { 142 | throw new DatagridColumnStatusException( 143 | 'Cannot set replacement for Column Status. For status texts replacement use ->setOptions($replacements)' 144 | ); 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /src/Column/ColumnText.php: -------------------------------------------------------------------------------- 1 | column]; 24 | } else { 25 | $columns = is_string($columns) 26 | ? [$columns] 27 | : $columns; 28 | } 29 | 30 | return $this->grid->addFilterText($this->key, $this->name, $columns); 31 | } 32 | 33 | public function setFilterSelect( 34 | array $options, 35 | ?string $column = null 36 | ): FilterSelect 37 | { 38 | $column ??= $this->column; 39 | 40 | return $this->grid->addFilterSelect($this->key, $this->name, $options, $column); 41 | } 42 | 43 | public function setFilterMultiSelect( 44 | array $options, 45 | ?string $column = null 46 | ): FilterMultiSelect 47 | { 48 | $column ??= $this->column; 49 | 50 | return $this->grid->addFilterMultiSelect($this->key, $this->name, $options, $column); 51 | } 52 | 53 | public function setFilterDate(?string $column = null): FilterDate 54 | { 55 | $column ??= $this->column; 56 | 57 | return $this->grid->addFilterDate($this->key, $this->name, $column); 58 | } 59 | 60 | public function setFilterRange( 61 | ?string $column = null, 62 | string $nameSecond = '-' 63 | ): FilterRange 64 | { 65 | $column ??= $this->column; 66 | 67 | return $this->grid->addFilterRange($this->key, $this->name, $column, $nameSecond); 68 | } 69 | 70 | public function setFilterDateRange( 71 | ?string $column = null, 72 | string $nameSecond = '-' 73 | ): FilterDateRange 74 | { 75 | $column ??= $this->column; 76 | 77 | return $this->grid->addFilterDateRange($this->key, $this->name, $column, $nameSecond); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/Column/ItemDetail.php: -------------------------------------------------------------------------------- 1 | title = 'contributte_datagrid.show'; 45 | $this->class = sprintf('btn btn-xs %s ajax', $grid::$btnSecondaryClass); 46 | $this->icon = 'eye'; 47 | } 48 | 49 | /** 50 | * Render row item detail button 51 | */ 52 | public function renderButton(Row $row): Html 53 | { 54 | $a = Html::el('a') 55 | ->href($this->grid->link('getItemDetail!', ['id' => $row->getId()])) 56 | ->data('toggle-detail', $row->getId()) 57 | ->data('toggle-detail-grid-fullname', $this->grid->getFullName()) 58 | ->data('toggle-detail-grid', $this->grid->getName()); 59 | 60 | $this->tryAddIcon($a, $this->getIcon(), $this->getText()); 61 | 62 | $a->addText($this->text); 63 | 64 | if ($this->title !== null) { 65 | $a->setAttribute( 66 | 'title', 67 | $this->grid->getTranslator()->translate($this->title) 68 | ); 69 | } 70 | 71 | if ($this->class !== '') { 72 | $a->setAttribute('class', $this->class); 73 | } 74 | 75 | return $a; 76 | } 77 | 78 | public function render(mixed $item): mixed 79 | { 80 | if ($this->getType() === 'block') { 81 | throw new DatagridItemDetailException('ItemDetail is set to render as block, but block #detail is not defined'); 82 | } 83 | 84 | if ($this->getRenderer() === null) { 85 | throw new LogicException('Renderer is not set'); 86 | } 87 | 88 | return call_user_func($this->getRenderer(), $item); 89 | } 90 | 91 | public function getPrimaryWhereColumn(): string 92 | { 93 | return $this->primaryWhereColumn; 94 | } 95 | 96 | /** 97 | * Set item detail type 98 | * 99 | * @return static 100 | */ 101 | public function setType(string $type): self 102 | { 103 | $this->type = $type; 104 | 105 | return $this; 106 | } 107 | 108 | /** 109 | * Get item detail type 110 | */ 111 | public function getType(): ?string 112 | { 113 | return $this->type; 114 | } 115 | 116 | /** 117 | * Set item detail template 118 | * 119 | * @return static 120 | */ 121 | public function setTemplate(string $template): self 122 | { 123 | $this->template = $template; 124 | 125 | return $this; 126 | } 127 | 128 | /** 129 | * Get item detail template 130 | */ 131 | public function getTemplate(): ?string 132 | { 133 | return $this->template; 134 | } 135 | 136 | /** 137 | * @return static 138 | */ 139 | public function setRenderer(callable $renderer): self 140 | { 141 | $this->renderer = $renderer; 142 | 143 | return $this; 144 | } 145 | 146 | public function getRenderer(): ?callable 147 | { 148 | return $this->renderer; 149 | } 150 | 151 | /** 152 | * @return static 153 | */ 154 | public function setForm(ItemDetailForm $form): self 155 | { 156 | $this->form = $form; 157 | 158 | return $this; 159 | } 160 | 161 | public function getForm(): ?ItemDetailForm 162 | { 163 | return $this->form; 164 | } 165 | 166 | /** 167 | * @return static 168 | */ 169 | public function setTemplateParameters(array $templateParameters): self 170 | { 171 | $this->templateParameters = $templateParameters; 172 | 173 | return $this; 174 | } 175 | 176 | public function getTemplateVariables(): array 177 | { 178 | return $this->templateParameters; 179 | } 180 | 181 | } 182 | -------------------------------------------------------------------------------- /src/Column/MultiAction.php: -------------------------------------------------------------------------------- 1 | setTemplate(__DIR__ . '/../templates/column_multi_action.latte'); 42 | } 43 | 44 | public function renderButton(): Html 45 | { 46 | $button = Html::el('button') 47 | ->setAttribute('type', 'button') 48 | ->data('bs-toggle', 'dropdown'); 49 | 50 | $this->tryAddIcon($button, $this->getIcon(), $this->getName()); 51 | 52 | $button->addText($this->grid->getTranslator()->translate($this->name)); 53 | 54 | if ($this->hasCaret()) { 55 | $button->addHtml(' '); 56 | $button->addHtml(''); 57 | } 58 | 59 | if ($this->getTitle() !== null) { 60 | $button->setAttribute( 61 | 'title', 62 | $this->grid->getTranslator()->translate($this->getTitle()) 63 | ); 64 | } 65 | 66 | if ($this->getClass() !== '') { 67 | $button->setAttribute('class', $this->getClass() . ' dropdown-toggle'); 68 | } 69 | 70 | return $button; 71 | } 72 | 73 | /** 74 | * @return static 75 | */ 76 | public function addAction( 77 | string $key, 78 | string $name, 79 | ?string $href = null, 80 | ?array $params = null 81 | ): self 82 | { 83 | if (isset($this->actions[$key])) { 84 | throw new DatagridException( 85 | sprintf('There is already action at key [%s] defined for MultiAction.', $key) 86 | ); 87 | } 88 | 89 | $href ??= $key; 90 | 91 | if ($params === null) { 92 | $params = [$this->grid->getPrimaryKey()]; 93 | } 94 | 95 | $action = new Action($this->grid, $key, $href, $name, $params); 96 | 97 | $action->setClass('dropdown-item datagrid-multiaction-dropdown-item'); 98 | 99 | $this->actions[$key] = $action; 100 | 101 | return $this; 102 | } 103 | 104 | /** 105 | * @return array 106 | */ 107 | public function getActions(): array 108 | { 109 | return $this->actions; 110 | } 111 | 112 | public function getAction(string $key): Action 113 | { 114 | if (!isset($this->actions[$key])) { 115 | throw new DatagridException( 116 | sprintf('There is no action at key [%s] defined for MultiAction.', $key) 117 | ); 118 | } 119 | 120 | return $this->actions[$key]; 121 | } 122 | 123 | /** 124 | * Column can have variables that will be passed to custom template scope 125 | */ 126 | public function getTemplateVariables(): array 127 | { 128 | return array_merge($this->templateVariables, [ 129 | 'multiAction' => $this, 130 | ]); 131 | } 132 | 133 | public function setRowCondition( 134 | string $actionKey, 135 | callable $rowCondition 136 | ): void 137 | { 138 | $this->rowConditions[$actionKey] = $rowCondition; 139 | } 140 | 141 | public function testRowCondition(string $actionKey, Row $row): bool 142 | { 143 | if (!isset($this->rowConditions[$actionKey])) { 144 | return true; 145 | } 146 | 147 | return (bool) call_user_func($this->rowConditions[$actionKey], $row->getItem()); 148 | } 149 | 150 | } 151 | -------------------------------------------------------------------------------- /src/Column/Renderer.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 20 | $this->conditionCallback = $conditionCallback; 21 | } 22 | 23 | /** 24 | * Get custom renderer callback 25 | */ 26 | public function getCallback(): callable 27 | { 28 | return $this->callback; 29 | } 30 | 31 | /** 32 | * Get custom renderer condition callback 33 | */ 34 | public function getConditionCallback(): ?callable 35 | { 36 | return $this->conditionCallback; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/ColumnsSummary.php: -------------------------------------------------------------------------------- 1 | summary = array_fill_keys(array_values($columns), 0); 35 | $this->rowCallback = $rowCallback; 36 | 37 | foreach (array_keys($this->summary) as $key) { 38 | $column = $this->datagrid->getColumn($key); 39 | 40 | if ($column instanceof ColumnNumber) { 41 | $arg = $column->getFormat(); 42 | array_unshift($arg, $key); 43 | 44 | $this->setFormat(...$arg); 45 | } 46 | } 47 | } 48 | 49 | public function add(Row $row): void 50 | { 51 | foreach (array_keys($this->summary) as $key) { 52 | $column = $this->datagrid->getColumn($key); 53 | 54 | $value = $this->getValue($row, $column); 55 | $this->summary[$key] += $value; 56 | } 57 | } 58 | 59 | public function render(string $key): ?string 60 | { 61 | try { 62 | return $this->useRenderer($key); 63 | } catch (DatagridColumnRendererException) { 64 | // No need to worry. 65 | } 66 | 67 | if (!isset($this->summary[$key])) { 68 | return ''; 69 | } 70 | 71 | return number_format( 72 | $this->summary[$key], 73 | $this->format[$key][0], 74 | $this->format[$key][1], 75 | $this->format[$key][2] 76 | ); 77 | } 78 | 79 | public function useRenderer(string $key): mixed 80 | { 81 | if (!isset($this->summary[$key])) { 82 | return null; 83 | } 84 | 85 | $renderer = $this->getRenderer(); 86 | 87 | if ($renderer === null) { 88 | throw new DatagridColumnRendererException(); 89 | } 90 | 91 | return call_user_func_array($renderer->getCallback(), [$this->summary[$key], $key]); 92 | } 93 | 94 | public function getRenderer(): ?Renderer 95 | { 96 | return $this->renderer; 97 | } 98 | 99 | /** 100 | * @return static 101 | */ 102 | public function setRenderer(callable $renderer): self 103 | { 104 | $this->renderer = new Renderer($renderer, null); 105 | 106 | return $this; 107 | } 108 | 109 | /** 110 | * @return static 111 | */ 112 | public function setFormat( 113 | string $key, 114 | int $decimals = 0, 115 | string $dec_point = '.', 116 | string $thousands_sep = ' ' 117 | ): self 118 | { 119 | $this->format[$key] = [$decimals, $dec_point, $thousands_sep]; 120 | 121 | return $this; 122 | } 123 | 124 | public function someColumnsExist(array $columns): bool 125 | { 126 | foreach (array_keys($columns) as $key) { 127 | if (isset($this->summary[$key])) { 128 | return true; 129 | } 130 | } 131 | 132 | return false; 133 | } 134 | 135 | /** 136 | * @return static 137 | */ 138 | public function setPositionTop(bool $top = true): self 139 | { 140 | $this->positionTop = $top !== false; 141 | 142 | return $this; 143 | } 144 | 145 | public function getPositionTop(): bool 146 | { 147 | return $this->positionTop; 148 | } 149 | 150 | /** 151 | * Get value from column using Row::getValue() or custom callback 152 | */ 153 | private function getValue(Row $row, Column $column): mixed 154 | { 155 | if ($this->rowCallback === null) { 156 | return $row->getValue($column->getColumn()); 157 | } 158 | 159 | return call_user_func_array($this->rowCallback, [$row->getItem(), $column->getColumn()]); 160 | } 161 | 162 | } 163 | -------------------------------------------------------------------------------- /src/Components/DatagridPaginator/DatagridPaginator.php: -------------------------------------------------------------------------------- 1 | templateFile = $templateFile; 36 | } 37 | 38 | public function getTemplateFile(): string 39 | { 40 | return $this->templateFile ?? __DIR__ . '/templates/data_grid_paginator.latte'; 41 | } 42 | 43 | public function getOriginalTemplateFile(): string 44 | { 45 | return __DIR__ . '/templates/data_grid_paginator.latte'; 46 | } 47 | 48 | public function getPaginator(): Paginator 49 | { 50 | if ($this->paginator === null) { 51 | $this->paginator = new Paginator(); 52 | } 53 | 54 | return $this->paginator; 55 | } 56 | 57 | public function render(): void 58 | { 59 | $paginator = $this->getPaginator(); 60 | $page = $paginator->page; 61 | 62 | if ($paginator->pageCount < 2) { 63 | $steps = [$page]; 64 | 65 | } else { 66 | $arr = range(max($paginator->firstPage, $page - 2), (int) min($paginator->lastPage, $page + 2)); 67 | 68 | /** 69 | * Something to do with steps in template... 70 | * [Default $count = 3;] 71 | */ 72 | $count = 1; 73 | 74 | $perPage = $paginator->pageCount; 75 | 76 | $quotient = ($perPage - 1) / $count; 77 | 78 | for ($i = 0; $i <= $count; $i++) { 79 | $arr[] = round($quotient * $i) + $paginator->firstPage; 80 | } 81 | 82 | sort($arr); 83 | $steps = array_values(array_unique($arr)); 84 | } 85 | 86 | if (!$this->getTemplate() instanceof Template) { 87 | throw new UnexpectedValueException(); 88 | } 89 | 90 | $this->getTemplate()->setTranslator($this->translator); 91 | 92 | if (!isset($this->getTemplate()->steps)) { 93 | $this->getTemplate()->steps = $steps; 94 | } 95 | 96 | if (!isset($this->getTemplate()->paginator)) { 97 | $this->getTemplate()->paginator = $paginator; 98 | } 99 | 100 | $this->getTemplate()->iconPrefix = $this->iconPrefix; 101 | $this->getTemplate()->btnSecondaryClass = $this->btnSecondaryClass; 102 | $this->getTemplate()->originalTemplate = $this->getOriginalTemplateFile(); 103 | $this->getTemplate()->setFile($this->getTemplateFile()); 104 | $this->getTemplate()->render(); 105 | } 106 | 107 | /** 108 | * Loads state informations. 109 | */ 110 | public function loadState(array $params): void 111 | { 112 | parent::loadState($params); 113 | 114 | if ($this->getParent() instanceof Datagrid) { 115 | $this->getPaginator()->page = $this->getParent()->page; 116 | } 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/Components/DatagridPaginator/templates/data_grid_paginator.latte: -------------------------------------------------------------------------------- 1 | {* 2 | * @param Paginator $paginator 3 | * @param array $steps 4 | * @param string $iconPrefix 5 | *} 6 | 7 | {var $link = [$control->getParent(), link]} 8 | 9 |

10 | {capture $firstButton} 11 |  {='contributte_datagrid.previous'|translate} 12 | {/capture} 13 | 14 | {if $paginator->isFirst()} 15 | {$firstButton} 16 | {else} 17 | 18 | {/if} 19 | 20 | {foreach $steps as $step} 21 | {if $step == $paginator->page} 22 | {$step} 23 | {else} 24 | {$step} 25 | {/if} 26 | 27 | {if $iterator->nextValue > $step + 1}{/if} 28 | {/foreach} 29 | 30 | {capture $lastButton} 31 | {='contributte_datagrid.next'|translate}  32 | {/capture} 33 | 34 | {if $paginator->isLast()} 35 | {$lastButton} 36 | {else} 37 | 38 | {/if} 39 |
40 | -------------------------------------------------------------------------------- /src/CsvDataModel.php: -------------------------------------------------------------------------------- 1 | getHeader(); 27 | } 28 | 29 | foreach ($this->data as $item) { 30 | $return[] = $this->getRow($item); 31 | } 32 | 33 | return $return; 34 | } 35 | 36 | public function getHeader(): array 37 | { 38 | $header = []; 39 | 40 | foreach ($this->columns as $column) { 41 | $header[] = $this->translator->translate($column->getName()); 42 | } 43 | 44 | return $header; 45 | } 46 | 47 | /** 48 | * Get item values saved into row 49 | */ 50 | public function getRow(mixed $item): array 51 | { 52 | $row = []; 53 | 54 | foreach ($this->columns as $column) { 55 | $row[] = strip_tags((string) $column->render($item)); 56 | } 57 | 58 | return $row; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/DataModel.php: -------------------------------------------------------------------------------- 1 | getConnection()->getDriver(); 60 | 61 | if ($driver instanceof OdbcDriver) { 62 | $source = new DibiFluentMssqlDataSource($source, $primaryKey); 63 | 64 | } elseif ($driver instanceof MsSqlDriver) { 65 | $source = new DibiFluentMssqlDataSource($source, $primaryKey); 66 | 67 | } elseif ($driver instanceof PostgreDriver) { 68 | $source = new DibiFluentPostgreDataSource($source, $primaryKey); 69 | 70 | } elseif ($driver instanceof SqlsrvDriver) { 71 | $source = new DibiFluentMssqlDataSource($source, $primaryKey); 72 | 73 | } else { 74 | $source = new DibiFluentDataSource($source, $primaryKey); 75 | } 76 | } elseif ($source instanceof Selection) { 77 | $driver = NetteDatabaseSelectionHelper::getDriver($source); 78 | 79 | $source = $driver instanceof NDBMsSqlDriver || $driver instanceof NDBSqlsrvDriver ? new NetteDatabaseTableMssqlDataSource($source, $primaryKey) : new NetteDatabaseTableDataSource($source, $primaryKey); 80 | } elseif ($source instanceof QueryBuilder) { 81 | $source = new DoctrineDataSource($source, $primaryKey); 82 | 83 | } elseif ($source instanceof Collection) { 84 | $source = new DoctrineCollectionDataSource($source, $primaryKey); 85 | 86 | } elseif ($source instanceof ICollection) { 87 | $source = new NextrasDataSource($source, $primaryKey); 88 | 89 | } elseif (!($source instanceof IDataSource)) { 90 | throw new DatagridWrongDataSourceException(sprintf( 91 | 'Datagrid can not take [%s] as data source.', 92 | is_object($source) ? $source::class : 'null' 93 | )); 94 | } 95 | 96 | $this->dataSource = $source; 97 | } 98 | 99 | public function getDataSource(): IDataSource 100 | { 101 | return $this->dataSource; 102 | } 103 | 104 | public function filterData( 105 | ?DatagridPaginator $paginatorComponent, 106 | Sorting $sorting, 107 | array $filters 108 | ): iterable 109 | { 110 | $this->onBeforeFilter($this->dataSource); 111 | 112 | $this->dataSource->filter($filters); 113 | 114 | $this->onAfterFilter($this->dataSource); 115 | 116 | /** 117 | * Paginator is optional 118 | */ 119 | if ($paginatorComponent !== null) { 120 | $paginator = $paginatorComponent->getPaginator(); 121 | $paginator->setItemCount($this->dataSource->getCount()); 122 | 123 | $this->dataSource->sort($sorting)->limit( 124 | $paginator->getOffset(), 125 | $paginator->getItemsPerPage() 126 | ); 127 | 128 | $this->onAfterPaginated($this->dataSource); 129 | 130 | return $this->dataSource->getData(); 131 | } 132 | 133 | return $this->dataSource->sort($sorting)->getData(); 134 | } 135 | 136 | public function filterRow(array $condition): mixed 137 | { 138 | $this->onBeforeFilter($this->dataSource); 139 | $this->onAfterFilter($this->dataSource); 140 | 141 | return $this->dataSource->filterOne($condition)->getData(); 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /src/DataSource/ApiDataSource.php: -------------------------------------------------------------------------------- 1 | getResponse(['count' => '']); 32 | } 33 | 34 | /** 35 | * {@inheritDoc} 36 | */ 37 | public function getData(): array 38 | { 39 | return $this->data !== [] ? $this->data : $this->getResponse([ 40 | 'sort' => $this->sortColumn, 41 | 'order' => $this->orderColumn, 42 | 'limit' => $this->limit, 43 | 'offset' => $this->offset, 44 | 'filter' => $this->filter, 45 | 'one' => $this->filterOne, 46 | ]); 47 | } 48 | 49 | /** 50 | * {@inheritDoc} 51 | */ 52 | public function filter(array $filters): void 53 | { 54 | /** 55 | * First, save all filter values to array 56 | */ 57 | foreach ($filters as $filter) { 58 | if ($filter->isValueSet() && $filter->getConditionCallback() === null) { 59 | $this->filter[$filter->getKey()] = $filter->getCondition(); 60 | } 61 | } 62 | 63 | /** 64 | * Download filtered data 65 | */ 66 | $this->data = $this->getData(); 67 | 68 | /** 69 | * Apply possible user filter callbacks 70 | */ 71 | foreach ($filters as $filter) { 72 | if ($filter->isValueSet() && $filter->getConditionCallback() !== null) { 73 | $this->data = (array) call_user_func_array( 74 | $filter->getConditionCallback(), 75 | [$this->data, $filter->getValue()] 76 | ); 77 | } 78 | } 79 | } 80 | 81 | /** 82 | * {@inheritDoc} 83 | */ 84 | public function filterOne(array $condition): IDataSource 85 | { 86 | $this->filter = $condition; 87 | $this->filterOne = 1; 88 | 89 | return $this; 90 | } 91 | 92 | public function limit(int $offset, int $limit): IDataSource 93 | { 94 | $this->offset = $offset; 95 | $this->limit = $limit; 96 | 97 | return $this; 98 | } 99 | 100 | public function sort(Sorting $sorting): IDataSource 101 | { 102 | /** 103 | * there is only one iteration 104 | */ 105 | foreach ($sorting->getSort() as $column => $order) { 106 | $this->sortColumn = $column; 107 | $this->orderColumn = $order; 108 | } 109 | 110 | return $this; 111 | } 112 | 113 | /** 114 | * Get data of remote source 115 | */ 116 | protected function getResponse(array $params = []): mixed 117 | { 118 | $queryString = http_build_query($params + $this->queryParams); 119 | $url = sprintf('%s?%s', $this->url, $queryString); 120 | 121 | $content = file_get_contents($url); 122 | 123 | if ($content === false) { 124 | throw new UnexpectedValueException(sprintf('Could not open URL %s', $url)); 125 | } 126 | 127 | return json_decode($content, null, 512, JSON_THROW_ON_ERROR); 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /src/DataSource/DibiFluentMssqlDataSource.php: -------------------------------------------------------------------------------- 1 | dataSource; 28 | $clone->removeClause('ORDER BY'); 29 | 30 | return $clone->count(); 31 | } 32 | 33 | /** 34 | * {@inheritDoc} 35 | */ 36 | public function filterOne(array $condition): IDataSource 37 | { 38 | $this->dataSource->where($condition); 39 | 40 | return $this; 41 | } 42 | 43 | public function limit(int $offset, int $limit): IDataSource 44 | { 45 | $sql = (string) $this->dataSource; 46 | 47 | $result = $this->dataSource->getConnection() 48 | ->query('%sql OFFSET ? ROWS FETCH NEXT ? ROWS ONLY', $sql, $offset, $limit); 49 | 50 | if (!$result instanceof Result) { 51 | throw new UnexpectedValueException(); 52 | } 53 | 54 | $this->data = $result->fetchAll(); 55 | 56 | return $this; 57 | } 58 | 59 | protected function applyFilterDate(FilterDate $filter): void 60 | { 61 | $conditions = $filter->getCondition(); 62 | 63 | try { 64 | $date = DateTimeHelper::tryConvertToDateTime( 65 | $conditions[$filter->getColumn()], 66 | [$filter->getPhpFormat()] 67 | ); 68 | 69 | $this->dataSource->where( 70 | 'CONVERT(varchar(10), %n, 112) = ?', 71 | $filter->getColumn(), 72 | $date->format('Ymd') 73 | ); 74 | } catch (DatagridDateTimeHelperException) { 75 | // ignore the invalid filter value 76 | } 77 | } 78 | 79 | protected function applyFilterDateRange(FilterDateRange $filter): void 80 | { 81 | $conditions = $filter->getCondition(); 82 | 83 | $valueFrom = $conditions[$filter->getColumn()]['from']; 84 | $valueTo = $conditions[$filter->getColumn()]['to']; 85 | 86 | if ($valueFrom) { 87 | $this->dataSource->where( 88 | 'CONVERT(varchar(10), %n, 112) >= ?', 89 | $filter->getColumn(), 90 | $valueFrom 91 | ); 92 | } 93 | 94 | if ($valueTo) { 95 | $this->dataSource->where( 96 | 'CONVERT(varchar(10), %n, 112) <= ?', 97 | $filter->getColumn(), 98 | $valueTo 99 | ); 100 | } 101 | } 102 | 103 | protected function applyFilterText(FilterText $filter): void 104 | { 105 | $condition = $filter->getCondition(); 106 | $driver = $this->dataSource->getConnection()->getDriver(); 107 | $or = []; 108 | 109 | foreach ($condition as $column => $value) { 110 | $column = Helpers::escape($driver, $column, Fluent::Identifier); 111 | 112 | if ($filter->isExactSearch()) { 113 | $this->dataSource->where(sprintf('%s = %%s', $column), $value); 114 | 115 | continue; 116 | } 117 | 118 | $or[] = sprintf('%s LIKE "%%%s%%"', $column, $value); 119 | } 120 | 121 | if (count($or) > 1) { 122 | $this->dataSource->where($filter->hasConjunctionSearch() ? '(%and)' : '(%or)', $or); 123 | } else { 124 | $this->dataSource->where($or); 125 | } 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /src/DataSource/DibiFluentPostgreDataSource.php: -------------------------------------------------------------------------------- 1 | getCondition(); 13 | $driver = $this->dataSource->getConnection()->getDriver(); 14 | $or = []; 15 | 16 | foreach ($condition as $column => $value) { 17 | 18 | $column = '[' . $column . ']'; 19 | 20 | if ($filter->isExactSearch()) { 21 | $this->dataSource->where(sprintf('%s = %%s', $column), $value); 22 | 23 | continue; 24 | } 25 | 26 | $words = $filter->hasSplitWordsSearch() === false ? [$value] : explode(' ', $value); 27 | 28 | foreach ($words as $word) { 29 | $or[] = $column . ' ILIKE ' . $driver->escapeText('%' . $word . '%'); 30 | } 31 | } 32 | 33 | if (count($or) > 1) { 34 | $this->dataSource->where($filter->hasConjunctionSearch() ? '(%and)' : '(%or)', $or); 35 | } else { 36 | $this->dataSource->where($or); 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/DataSource/DoctrineCollectionDataSource.php: -------------------------------------------------------------------------------- 1 | criteria = Criteria::create(); 37 | $this->dataSource = $collection; 38 | } 39 | 40 | public function getCount(): int 41 | { 42 | return $this->dataSource->matching($this->criteria)->count(); 43 | } 44 | 45 | /** 46 | * {@inheritDoc} 47 | */ 48 | public function getData(): array 49 | { 50 | return $this->dataSource->matching($this->criteria)->toArray(); 51 | } 52 | 53 | /** 54 | * {@inheritDoc} 55 | */ 56 | public function filterOne(array $condition): IDataSource 57 | { 58 | foreach ($condition as $column => $value) { 59 | if ($column === $this->primaryKey && is_numeric($value)) { 60 | $value = (int) $value; 61 | } 62 | 63 | $expr = Criteria::expr()->eq($column, $value); 64 | $this->criteria->andWhere($expr); 65 | } 66 | 67 | return $this; 68 | } 69 | 70 | public function limit(int $offset, int $limit): IDataSource 71 | { 72 | $this->criteria->setFirstResult($offset)->setMaxResults($limit); 73 | 74 | return $this; 75 | } 76 | 77 | public function sort(Sorting $sorting): IDataSource 78 | { 79 | if (is_callable($sorting->getSortCallback())) { 80 | call_user_func( 81 | $sorting->getSortCallback(), 82 | $this->criteria, 83 | $sorting->getSort() 84 | ); 85 | 86 | return $this; 87 | } 88 | 89 | if ($sorting->getSort() !== []) { 90 | $this->criteria->orderBy($sorting->getSort()); 91 | 92 | return $this; 93 | } 94 | 95 | $this->criteria->orderBy([$this->primaryKey => 'ASC']); 96 | 97 | return $this; 98 | } 99 | 100 | public function processAggregation(IAggregationFunction $function): void 101 | { 102 | $function->processDataSource(clone $this->dataSource); 103 | } 104 | 105 | /** 106 | * @return Collection&Selectable 107 | */ 108 | public function getDataSource(): mixed 109 | { 110 | return $this->dataSource; 111 | } 112 | 113 | protected function applyFilterDate(FilterDate $filter): void 114 | { 115 | foreach ($filter->getCondition() as $value) { 116 | try { 117 | $date = DateTimeHelper::tryConvertToDateTime($value, [$filter->getPhpFormat()]); 118 | 119 | $from = Criteria::expr()->gte($filter->getColumn(), $date->format('Y-m-d 00:00:00')); 120 | $to = Criteria::expr()->lte($filter->getColumn(), $date->format('Y-m-d 23:59:59')); 121 | 122 | $this->criteria->andWhere($from)->andWhere($to); 123 | } catch (DatagridDateTimeHelperException) { 124 | // ignore the invalid filter value 125 | } 126 | } 127 | } 128 | 129 | protected function applyFilterDateRange(FilterDateRange $filter): void 130 | { 131 | $conditions = $filter->getCondition(); 132 | $values = $conditions[$filter->getColumn()]; 133 | 134 | $valueFrom = $values['from']; 135 | 136 | if ($valueFrom) { 137 | try { 138 | $dateFrom = DateTimeHelper::tryConvertToDateTime($valueFrom, [$filter->getPhpFormat()]); 139 | $dateFrom->setTime(0, 0, 0); 140 | 141 | $expr = Criteria::expr()->gte($filter->getColumn(), $dateFrom->format('Y-m-d H:i:s')); 142 | $this->criteria->andWhere($expr); 143 | } catch (DatagridDateTimeHelperException) { 144 | // ignore the invalid filter value 145 | } 146 | } 147 | 148 | $valueTo = $values['to']; 149 | 150 | if ($valueTo) { 151 | try { 152 | $dateTo = DateTimeHelper::tryConvertToDateTime($valueTo, [$filter->getPhpFormat()]); 153 | $dateTo->setTime(23, 59, 59); 154 | 155 | $expr = Criteria::expr()->lte($filter->getColumn(), $dateTo->format('Y-m-d H:i:s')); 156 | $this->criteria->andWhere($expr); 157 | } catch (DatagridDateTimeHelperException) { 158 | // ignore the invalid filter value 159 | } 160 | } 161 | } 162 | 163 | protected function applyFilterRange(FilterRange $filter): void 164 | { 165 | $conditions = $filter->getCondition(); 166 | 167 | $valueFrom = $conditions[$filter->getColumn()]['from']; 168 | $valueTo = $conditions[$filter->getColumn()]['to']; 169 | 170 | if (is_numeric($valueFrom)) { 171 | $expr = Criteria::expr()->gte($filter->getColumn(), $valueFrom); 172 | $this->criteria->andWhere($expr); 173 | } 174 | 175 | if (is_numeric($valueTo)) { 176 | $expr = Criteria::expr()->lte($filter->getColumn(), $valueTo); 177 | $this->criteria->andWhere($expr); 178 | } 179 | } 180 | 181 | protected function applyFilterText(FilterText $filter): void 182 | { 183 | $exprs = []; 184 | 185 | foreach ($filter->getCondition() as $column => $value) { 186 | if ($filter->isExactSearch()) { 187 | $exprs[] = Criteria::expr()->eq($column, $value); 188 | 189 | continue; 190 | } 191 | 192 | $words = $filter->hasSplitWordsSearch() === false ? [$value] : explode(' ', $value); 193 | 194 | foreach ($words as $word) { 195 | $exprs[] = Criteria::expr()->contains($column, $word); 196 | } 197 | } 198 | 199 | $expr = call_user_func_array([Criteria::expr(), $filter->hasConjunctionSearch() ? 'andX' : 'orX'], $exprs); 200 | $this->criteria->andWhere($expr); 201 | } 202 | 203 | protected function applyFilterMultiSelect(FilterMultiSelect $filter): void 204 | { 205 | $values = $filter->getCondition()[$filter->getColumn()]; 206 | 207 | $expr = Criteria::expr()->in($filter->getColumn(), $values); 208 | $this->criteria->andWhere($expr); 209 | } 210 | 211 | protected function applyFilterSelect(FilterSelect $filter): void 212 | { 213 | foreach ($filter->getCondition() as $column => $value) { 214 | $expr = Criteria::expr()->eq($column, $value); 215 | $this->criteria->andWhere($expr); 216 | } 217 | } 218 | 219 | } 220 | -------------------------------------------------------------------------------- /src/DataSource/ElasticsearchDataSource.php: -------------------------------------------------------------------------------- 1 | searchParamsBuilder = new SearchParamsBuilder($indexName); 29 | 30 | if ($rowFactory === null) { 31 | $rowFactory = static fn (array $hit): array => $hit['_source']; 32 | } 33 | 34 | $this->rowFactory = $rowFactory; 35 | } 36 | 37 | public function getCount(): int 38 | { 39 | /** @var Elasticsearch $searchResult */ 40 | $searchResult = $this->client->search($this->searchParamsBuilder->buildParams()); 41 | 42 | if (!isset($searchResult['hits'])) { 43 | throw new UnexpectedValueException(); 44 | } 45 | 46 | return is_array($searchResult['hits']['total']) 47 | ? $searchResult['hits']['total']['value'] 48 | : $searchResult['hits']['total']; 49 | } 50 | 51 | /** 52 | * {@inheritDoc} 53 | */ 54 | public function getData(): array 55 | { 56 | /** @var Elasticsearch $searchResult */ 57 | $searchResult = $this->client->search($this->searchParamsBuilder->buildParams()); 58 | 59 | if (!isset($searchResult['hits'])) { 60 | throw new UnexpectedValueException(); 61 | } 62 | 63 | return array_map($this->rowFactory, $searchResult['hits']['hits']); 64 | } 65 | 66 | /** 67 | * {@inheritDoc} 68 | */ 69 | public function filterOne(array $condition): IDataSource 70 | { 71 | foreach ($condition as $value) { 72 | $this->searchParamsBuilder->addIdsQuery($value); 73 | } 74 | 75 | return $this; 76 | } 77 | 78 | public function limit(int $offset, int $limit): IDataSource 79 | { 80 | $this->searchParamsBuilder->setFrom($offset); 81 | $this->searchParamsBuilder->setSize($limit); 82 | 83 | return $this; 84 | } 85 | 86 | public function applyFilterDate(FilterDate $filter): void 87 | { 88 | foreach ($filter->getCondition() as $column => $value) { 89 | $timestampFrom = null; 90 | $timestampTo = null; 91 | 92 | if ($value) { 93 | $dateFrom = DateTimeHelper::tryConvertToDateTime($value, [$filter->getPhpFormat()]); 94 | $dateFrom->setTime(0, 0, 0); 95 | 96 | $timestampFrom = $dateFrom->getTimestamp(); 97 | 98 | $dateTo = DateTimeHelper::tryConvertToDateTime($value, [$filter->getPhpFormat()]); 99 | $dateTo->setTime(23, 59, 59); 100 | 101 | $timestampTo = $dateTo->getTimestamp(); 102 | 103 | $this->searchParamsBuilder->addRangeQuery($column, $timestampFrom, $timestampTo); 104 | } 105 | } 106 | } 107 | 108 | public function applyFilterDateRange(FilterDateRange $filter): void 109 | { 110 | foreach ($filter->getCondition() as $column => $values) { 111 | $timestampFrom = null; 112 | $timestampTo = null; 113 | 114 | if ($values['from']) { 115 | $dateFrom = DateTimeHelper::tryConvertToDateTime($values['from'], [$filter->getPhpFormat()]); 116 | $dateFrom->setTime(0, 0, 0); 117 | 118 | $timestampFrom = $dateFrom->getTimestamp(); 119 | } 120 | 121 | if ($values['to']) { 122 | $dateTo = DateTimeHelper::tryConvertToDateTime($values['to'], [$filter->getPhpFormat()]); 123 | $dateTo->setTime(23, 59, 59); 124 | 125 | $timestampTo = $dateTo->getTimestamp(); 126 | } 127 | 128 | if (is_int($timestampFrom) || is_int($timestampTo)) { 129 | $this->searchParamsBuilder->addRangeQuery($column, $timestampFrom, $timestampTo); 130 | } 131 | } 132 | } 133 | 134 | public function applyFilterRange(FilterRange $filter): void 135 | { 136 | foreach ($filter->getCondition() as $column => $value) { 137 | $this->searchParamsBuilder->addRangeQuery($column, $value['from'] ?? null, $value['to'] ?? null); 138 | } 139 | } 140 | 141 | public function applyFilterText(FilterText $filter): void 142 | { 143 | foreach ($filter->getCondition() as $column => $value) { 144 | if ($filter->isExactSearch()) { 145 | $this->searchParamsBuilder->addMatchQuery($column, $value); 146 | } else { 147 | $this->searchParamsBuilder->addPhrasePrefixQuery($column, $value); 148 | } 149 | } 150 | } 151 | 152 | public function applyFilterMultiSelect(FilterMultiSelect $filter): void 153 | { 154 | foreach ($filter->getCondition() as $column => $values) { 155 | $this->searchParamsBuilder->addBooleanMatchQuery($column, $values); 156 | } 157 | } 158 | 159 | public function applyFilterSelect(FilterSelect $filter): void 160 | { 161 | foreach ($filter->getCondition() as $column => $value) { 162 | $this->searchParamsBuilder->addMatchQuery($column, $value); 163 | } 164 | } 165 | 166 | /** 167 | * {@inheritDoc} 168 | * 169 | * @throws RuntimeException 170 | */ 171 | public function sort(Sorting $sorting): IDataSource 172 | { 173 | if (is_callable($sorting->getSortCallback())) { 174 | throw new RuntimeException('No can do - not implemented yet'); 175 | } 176 | 177 | foreach ($sorting->getSort() as $column => $order) { 178 | $this->searchParamsBuilder->setSort( 179 | [$column => ['order' => strtolower($order)]] 180 | ); 181 | } 182 | 183 | return $this; 184 | } 185 | 186 | public function getDataSource(): SearchParamsBuilder 187 | { 188 | return $this->searchParamsBuilder; 189 | } 190 | 191 | } 192 | -------------------------------------------------------------------------------- /src/DataSource/FilterableDataSource.php: -------------------------------------------------------------------------------- 1 | $filters 23 | */ 24 | public function filter(array $filters): void 25 | { 26 | foreach ($filters as $filter) { 27 | if ($filter->isValueSet()) { 28 | if ($filter->getConditionCallback() !== null) { 29 | $value = $filter->getValue(); 30 | 31 | if (is_array($value)) { 32 | $value = ArrayHash::from($filter->getValue()); 33 | } 34 | 35 | ($filter->getConditionCallback())($this->getDataSource(), $value); 36 | } else { 37 | if ($filter instanceof FilterText) { 38 | $this->applyFilterText($filter); 39 | } elseif ($filter instanceof FilterMultiSelect) { 40 | $this->applyFilterMultiSelect($filter); 41 | } elseif ($filter instanceof FilterSelect) { 42 | $this->applyFilterSelect($filter); 43 | } elseif ($filter instanceof FilterDate) { 44 | $this->applyFilterDate($filter); 45 | } elseif ($filter instanceof FilterDateRange) { 46 | $this->applyFilterDateRange($filter); 47 | } elseif ($filter instanceof FilterRange) { 48 | $this->applyFilterRange($filter); 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | abstract protected function applyFilterDate(FilterDate $filter): void; 56 | 57 | abstract protected function applyFilterDateRange(FilterDateRange $filter): void; 58 | 59 | abstract protected function applyFilterRange(FilterRange $filter): void; 60 | 61 | abstract protected function applyFilterText(FilterText $filter): void; 62 | 63 | abstract protected function applyFilterMultiSelect(FilterMultiSelect $filter): void; 64 | 65 | abstract protected function applyFilterSelect(FilterSelect $filter): void; 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/DataSource/IDataSource.php: -------------------------------------------------------------------------------- 1 | $filters 25 | */ 26 | public function filter(array $filters): void; 27 | 28 | /** 29 | * Filter data - get one row 30 | * 31 | * @return static 32 | */ 33 | public function filterOne(array $condition): self; 34 | 35 | /** 36 | * Apply limit and offset on data 37 | * 38 | * @phpstan-param positive-int|0 $offset 39 | * @phpstan-param positive-int|0 $limit 40 | * @return static 41 | */ 42 | public function limit(int $offset, int $limit): self; 43 | 44 | /** 45 | * Sort data 46 | * 47 | * @return static 48 | */ 49 | public function sort(Sorting $sorting): self; 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/DataSource/NetteDatabaseTableMssqlDataSource.php: -------------------------------------------------------------------------------- 1 | getCondition(); 16 | 17 | try { 18 | $date = DateTimeHelper::tryConvertToDateTime($conditions[$filter->getColumn()], [$filter->getPhpFormat()]); 19 | 20 | $this->dataSource->where( 21 | sprintf('CONVERT(varchar(10), %s, 112) = ?', $filter->getColumn()), 22 | $date->format('Ymd') 23 | ); 24 | } catch (DatagridDateTimeHelperException) { 25 | // ignore the invalid filter value 26 | } 27 | } 28 | 29 | protected function applyFilterDateRange(FilterDateRange $filter): void 30 | { 31 | $conditions = $filter->getCondition(); 32 | 33 | $valueFrom = $conditions[$filter->getColumn()]['from']; 34 | $valueTo = $conditions[$filter->getColumn()]['to']; 35 | 36 | if ($valueFrom) { 37 | try { 38 | $dateFrom = DateTimeHelper::tryConvertToDateTime($valueFrom, [$filter->getPhpFormat()]); 39 | $dateFrom->setTime(0, 0, 0); 40 | 41 | $this->dataSource->where( 42 | sprintf('CONVERT(varchar(10), %s, 112) >= ?', $filter->getColumn()), 43 | $dateFrom->format('Ymd') 44 | ); 45 | } catch (DatagridDateTimeHelperException) { 46 | // ignore the invalid filter value 47 | } 48 | } 49 | 50 | if ($valueTo) { 51 | try { 52 | $dateTo = DateTimeHelper::tryConvertToDateTime($valueTo, [$filter->getPhpFormat()]); 53 | $dateTo->setTime(23, 59, 59); 54 | 55 | $this->dataSource->where( 56 | sprintf('CONVERT(varchar(10), %s, 112) <= ?', $filter->getColumn()), 57 | $dateTo->format('Ymd') 58 | ); 59 | } catch (DatagridDateTimeHelperException) { 60 | // ignore the invalid filter value 61 | } 62 | } 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/DataSource/SearchParamsBuilder.php: -------------------------------------------------------------------------------- 1 | phrasePrefixQueries[] = [$field => $query]; 31 | } 32 | 33 | public function addMatchQuery(string $field, mixed $query): void 34 | { 35 | $this->matchQueries[] = [$field => $query]; 36 | } 37 | 38 | public function addBooleanMatchQuery(string $field, array $queries): void 39 | { 40 | $this->booleanMatchQueries[] = [$field => $queries]; 41 | } 42 | 43 | public function addRangeQuery(string $field, ?int $from, ?int $to): void 44 | { 45 | $this->rangeQueries[] = [$field => ['from' => $from, 'to' => $to]]; 46 | } 47 | 48 | public function addIdsQuery(array $ids): void 49 | { 50 | $this->idsQueries[] = $ids; 51 | } 52 | 53 | public function setSort(array $sort): void 54 | { 55 | $this->sort = $sort; 56 | } 57 | 58 | public function setFrom(int $from): void 59 | { 60 | $this->from = $from; 61 | } 62 | 63 | public function setSize(int $size): void 64 | { 65 | $this->size = $size; 66 | } 67 | 68 | public function buildParams(): array 69 | { 70 | $return = [ 71 | 'index' => $this->indexName, 72 | ]; 73 | 74 | if ($this->sort !== [] || ($this->from !== null) || ($this->size !== null)) { 75 | $return['body'] = []; 76 | } 77 | 78 | if ($this->sort !== []) { 79 | $return['body']['sort'] = $this->sort; 80 | } 81 | 82 | if ($this->from !== null) { 83 | $return['body']['from'] = $this->from; 84 | } 85 | 86 | if ($this->size !== null) { 87 | $return['body']['size'] = $this->size; 88 | } 89 | 90 | if ($this->phrasePrefixQueries === [] 91 | && $this->matchQueries === [] 92 | && $this->booleanMatchQueries === [] 93 | && $this->rangeQueries === [] 94 | && $this->idsQueries === []) { 95 | return $return; 96 | } 97 | 98 | $return['body']['query'] = [ 99 | 'bool' => [ 100 | 'must' => [], 101 | ], 102 | ]; 103 | 104 | foreach ($this->phrasePrefixQueries as $phrasePrefixQuery) { 105 | foreach ($phrasePrefixQuery as $field => $query) { 106 | $return['body']['query']['bool']['must'][] = [ 107 | 'multi_match' => [ 108 | 'query' => $query, 109 | 'type' => 'phrase_prefix', 110 | 'fields' => [$field], 111 | ], 112 | ]; 113 | } 114 | } 115 | 116 | foreach ($this->matchQueries as $matchQuery) { 117 | foreach ($matchQuery as $field => $query) { 118 | $return['body']['query']['bool']['must'][] = [ 119 | 'match' => [ 120 | $field => [ 121 | 'query' => $query, 122 | ], 123 | ], 124 | ]; 125 | } 126 | } 127 | 128 | foreach ($this->booleanMatchQueries as $booleanMatchQuery) { 129 | foreach ($booleanMatchQuery as $field => $queries) { 130 | if ($queries === []) { 131 | continue; 132 | } 133 | 134 | $boolFilter = []; 135 | 136 | foreach ($queries as $query) { 137 | $boolFilter[] = [ 138 | 'match' => [ 139 | $field => [ 140 | 'query' => $query, 141 | ], 142 | ], 143 | ]; 144 | } 145 | 146 | $return['body']['query']['bool']['must'][] = [ 147 | 'bool' => [ 148 | 'should' => [$boolFilter], 149 | ], 150 | ]; 151 | } 152 | } 153 | 154 | foreach ($this->rangeQueries as $rangeQuery) { 155 | foreach ($rangeQuery as $field => $range) { 156 | if ($range['from'] === null && $range['to'] === null) { 157 | continue; 158 | } 159 | 160 | $rangeFilter = ['range' => [$field => []]]; 161 | 162 | if ($range['from'] !== null) { 163 | $rangeFilter['range'][$field]['gte'] = $range['from']; 164 | } 165 | 166 | if ($range['to'] !== null) { 167 | $rangeFilter['range'][$field]['lte'] = $range['to']; 168 | } 169 | 170 | $return['body']['query']['bool']['must'][] = $rangeFilter; 171 | } 172 | } 173 | 174 | foreach ($this->idsQueries as $ids) { 175 | $return['body']['query']['bool']['must'][] = [ 176 | 'ids' => [ 177 | 'values' => $ids, 178 | ], 179 | ]; 180 | } 181 | 182 | return $return; 183 | } 184 | 185 | } 186 | -------------------------------------------------------------------------------- /src/Exception/DatagridActionCallbackException.php: -------------------------------------------------------------------------------- 1 | text = $text; 44 | $this->callback = $callback; 45 | $this->title = $text; 46 | } 47 | 48 | public function render(): Html 49 | { 50 | $a = Html::el('a', [ 51 | 'class' => [$this->class], 52 | 'title' => $this->grid->getTranslator()->translate($this->getTitle()), 53 | 'href' => $this->link, 54 | 'target' => $this->target, 55 | ]); 56 | 57 | $this->tryAddIcon( 58 | $a, 59 | $this->getIcon(), 60 | $this->grid->getTranslator()->translate($this->getTitle()) 61 | ); 62 | 63 | $a->addText($this->grid->getTranslator()->translate($this->text)); 64 | 65 | if ($this->isAjax()) { 66 | $a->appendAttribute('class', 'ajax'); 67 | } 68 | 69 | if ($this->confirmDialog !== null) { 70 | $a->setAttribute('data-datagrid-confirm', $this->confirmDialog); 71 | } 72 | 73 | return $a; 74 | } 75 | 76 | /** 77 | * @return static 78 | */ 79 | public function setConfirmDialog(string $confirmDialog): self 80 | { 81 | $this->confirmDialog = $confirmDialog; 82 | 83 | return $this; 84 | } 85 | 86 | /** 87 | * Tell export which columns to use when exporting data 88 | * 89 | * @return static 90 | */ 91 | public function setColumns(array $columns): self 92 | { 93 | $this->columns = $columns; 94 | 95 | return $this; 96 | } 97 | 98 | /** 99 | * Get columns for export 100 | */ 101 | public function getColumns(): array 102 | { 103 | return $this->columns; 104 | } 105 | 106 | /** 107 | * Export signal url 108 | * 109 | * @return static 110 | */ 111 | public function setLink(Link $link): self 112 | { 113 | $this->link = $link; 114 | 115 | return $this; 116 | } 117 | 118 | /** 119 | * Tell export whether to be called via ajax or not 120 | * 121 | * @return static 122 | */ 123 | public function setAjax(bool $ajax = true): self 124 | { 125 | $this->ajax = $ajax; 126 | 127 | return $this; 128 | } 129 | 130 | public function isAjax(): bool 131 | { 132 | return $this->ajax; 133 | } 134 | 135 | /** 136 | * Is export filtered? 137 | */ 138 | public function isFiltered(): bool 139 | { 140 | return $this->filtered; 141 | } 142 | 143 | /** 144 | * Call export callback 145 | */ 146 | public function invoke(iterable $data): void 147 | { 148 | ($this->callback)($data, $this->grid); 149 | } 150 | 151 | /** 152 | * Adds target to html:a [_blank, _self, _parent, _top] 153 | */ 154 | public function setTarget(?string $target = null): void 155 | { 156 | if (in_array($target, ['_blank', '_self', '_parent', '_top'], true)) { 157 | $this->target = $target; 158 | } 159 | } 160 | 161 | } 162 | -------------------------------------------------------------------------------- /src/Export/ExportCsv.php: -------------------------------------------------------------------------------- 1 | getExportCallback($name, $outputEncoding, $delimiter, $includeBom), 30 | $filtered 31 | ); 32 | } 33 | 34 | private function getExportCallback( 35 | string $name, 36 | string $outputEncoding, 37 | string $delimiter, 38 | bool $includeBom 39 | ): callable 40 | { 41 | return function ( 42 | array $data, 43 | Datagrid $grid 44 | ) use ( 45 | $name, 46 | $outputEncoding, 47 | $delimiter, 48 | $includeBom 49 | ): void { 50 | $columns = $this->getColumns(); 51 | 52 | if ($columns === []) { 53 | $columns = $this->grid->getColumns(); 54 | } 55 | 56 | $csvDataModel = new CsvDataModel($data, $columns, $this->grid->getTranslator()); 57 | 58 | $this->grid->getPresenter()->sendResponse(new CsvResponse( 59 | $csvDataModel->getSimpleData(), 60 | $name, 61 | $outputEncoding, 62 | $delimiter, 63 | $includeBom 64 | )); 65 | }; 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/Filter/Filter.php: -------------------------------------------------------------------------------- 1 | ['form-control', 'form-control-sm'], 28 | ]; 29 | 30 | private ?string $placeholder = null; 31 | 32 | public function __construct(protected Datagrid $grid, protected string $key, protected string $name) 33 | { 34 | } 35 | 36 | abstract public function getCondition(): array; 37 | 38 | /** 39 | * Get filter key 40 | */ 41 | public function getKey(): string 42 | { 43 | return $this->key; 44 | } 45 | 46 | /** 47 | * Get filter name 48 | */ 49 | public function getName(): string 50 | { 51 | return $this->name; 52 | } 53 | 54 | /** 55 | * Tell whether value has been set in this fitler 56 | */ 57 | public function isValueSet(): bool 58 | { 59 | return $this->valueSet; 60 | } 61 | 62 | /** 63 | * @return static 64 | */ 65 | public function setValue(mixed $value): self 66 | { 67 | $this->value = $value; 68 | $this->valueSet = true; 69 | 70 | return $this; 71 | } 72 | 73 | public function getValue(): mixed 74 | { 75 | return $this->value; 76 | } 77 | 78 | /** 79 | * Set HTML attribute "placeholder" 80 | * 81 | * @return static 82 | */ 83 | public function setPlaceholder(string $placeholder): self 84 | { 85 | $this->placeholder = $placeholder; 86 | 87 | return $this; 88 | } 89 | 90 | public function getPlaceholder(): ?string 91 | { 92 | return $this->placeholder; 93 | } 94 | 95 | /** 96 | * Set custom condition on filter 97 | * 98 | * @return static 99 | */ 100 | public function setCondition(callable $conditionCallback): self 101 | { 102 | $this->conditionCallback = $conditionCallback; 103 | 104 | return $this; 105 | } 106 | 107 | public function getConditionCallback(): ?callable 108 | { 109 | return $this->conditionCallback; 110 | } 111 | 112 | /** 113 | * @return static 114 | */ 115 | public function setTemplate(string $template): self 116 | { 117 | $this->template = $template; 118 | 119 | return $this; 120 | } 121 | 122 | public function getTemplate(): ?string 123 | { 124 | return $this->template; 125 | } 126 | 127 | public function getType(): ?string 128 | { 129 | return $this->type; 130 | } 131 | 132 | /** 133 | * @return static 134 | */ 135 | public function addAttribute(string $name, mixed $value): self 136 | { 137 | $this->attributes[$name][] = $value; 138 | 139 | return $this; 140 | } 141 | 142 | /** 143 | * @return static 144 | */ 145 | public function setAttribute(string $name, mixed $value): self 146 | { 147 | $this->attributes[$name] = (array) $value; 148 | 149 | return $this; 150 | } 151 | 152 | public function getAttributes(): array 153 | { 154 | return $this->attributes; 155 | } 156 | 157 | protected function addAttributes(BaseControl $input): BaseControl 158 | { 159 | if ($this->grid->hasAutoSubmit()) { 160 | $input->setHtmlAttribute('data-autosubmit', true); 161 | } else { 162 | $input->setHtmlAttribute('data-datagrid-manualsubmit', true); 163 | } 164 | 165 | foreach ($this->attributes as $key => $value) { 166 | if (is_array($value)) { 167 | $value = array_unique($value); 168 | $value = implode(' ', $value); 169 | } 170 | 171 | $input->setHtmlAttribute($key, $value); 172 | } 173 | 174 | return $input; 175 | } 176 | 177 | } 178 | -------------------------------------------------------------------------------- /src/Filter/FilterDate.php: -------------------------------------------------------------------------------- 1 | addText($this->key, $this->name); 19 | 20 | $control->setHtmlAttribute('data-provide', 'datepicker') 21 | ->setHtmlAttribute('data-date-orientation', 'bottom') 22 | ->setHtmlAttribute('data-date-format', $this->getJsFormat()) 23 | ->setHtmlAttribute('data-date-today-highlight', 'true') 24 | ->setHtmlAttribute('data-date-autoclose', 'true'); 25 | 26 | $this->addAttributes($control); 27 | 28 | if ($this->grid->hasAutoSubmit()) { 29 | $control->setHtmlAttribute('data-autosubmit-change', true); 30 | } 31 | 32 | if ($this->getPlaceholder() !== null) { 33 | $control->setHtmlAttribute('placeholder', $this->getPlaceholder()); 34 | } 35 | } 36 | 37 | /** 38 | * Set format for datepicker etc 39 | */ 40 | public function setFormat(string $phpFormat, string $jsFormat): IFilterDate 41 | { 42 | $this->format = [$phpFormat, $jsFormat]; 43 | 44 | return $this; 45 | } 46 | 47 | /** 48 | * Get php format for datapicker 49 | */ 50 | public function getPhpFormat(): string 51 | { 52 | return $this->format[0]; 53 | } 54 | 55 | /** 56 | * Get js format for datepicker 57 | */ 58 | public function getJsFormat(): string 59 | { 60 | return $this->format[1]; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/Filter/FilterDateRange.php: -------------------------------------------------------------------------------- 1 | addContainer($this->key); 22 | 23 | $from = $container->addText('from', $this->name); 24 | 25 | $from->setHtmlAttribute('data-provide', 'datepicker') 26 | ->setHtmlAttribute('data-date-orientation', 'bottom') 27 | ->setHtmlAttribute('data-date-format', $this->getJsFormat()) 28 | ->setHtmlAttribute('data-date-today-highlight', 'true') 29 | ->setHtmlAttribute('data-date-autoclose', 'true'); 30 | 31 | $to = $container->addText('to', $this->nameSecond); 32 | 33 | $to->setHtmlAttribute('data-provide', 'datepicker') 34 | ->setHtmlAttribute('data-date-orientation', 'bottom') 35 | ->setHtmlAttribute('data-date-format', $this->getJsFormat()) 36 | ->setHtmlAttribute('data-date-today-highlight', 'true') 37 | ->setHtmlAttribute('data-date-autoclose', 'true'); 38 | 39 | $this->addAttributes($from); 40 | $this->addAttributes($to); 41 | 42 | if ($this->grid->hasAutoSubmit()) { 43 | $from->setHtmlAttribute('data-autosubmit-change', true); 44 | $to->setHtmlAttribute('data-autosubmit-change', true); 45 | } 46 | 47 | $placeholders = $this->getPlaceholders(); 48 | 49 | if ($placeholders !== []) { 50 | $textFrom = reset($placeholders); 51 | 52 | if ($textFrom) { 53 | $from->setHtmlAttribute('placeholder', $textFrom); 54 | } 55 | 56 | $textTo = end($placeholders); 57 | 58 | if ($textTo && ($textTo !== $textFrom)) { 59 | $to->setHtmlAttribute('placeholder', $textTo); 60 | } 61 | } 62 | } 63 | 64 | /** 65 | * Set format for datepicker etc 66 | */ 67 | public function setFormat(string $phpFormat, string $jsFormat): IFilterDate 68 | { 69 | $this->format = [$phpFormat, $jsFormat]; 70 | 71 | return $this; 72 | } 73 | 74 | /** 75 | * Get php format for datapicker 76 | */ 77 | public function getPhpFormat(): string 78 | { 79 | return $this->format[0]; 80 | } 81 | 82 | /** 83 | * Get js format for datepicker 84 | */ 85 | public function getJsFormat(): string 86 | { 87 | return $this->format[1]; 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/Filter/FilterMultiSelect.php: -------------------------------------------------------------------------------- 1 | ['selectpicker'], 18 | 'data-selected-text-format' => ['count'], 19 | ]; 20 | 21 | public function __construct( 22 | Datagrid $grid, 23 | string $key, 24 | string $name, 25 | array $options, 26 | string $column 27 | ) 28 | { 29 | parent::__construct($grid, $key, $name, $options, $column); 30 | 31 | $this->addAttribute('data-selected-icon-check', Datagrid::$iconPrefix . 'check'); 32 | } 33 | 34 | /** 35 | * Get filter condition 36 | */ 37 | public function getCondition(): array 38 | { 39 | $return = [$this->column => []]; 40 | 41 | foreach ($this->getValue() as $value) { 42 | $return[$this->column][] = $value; 43 | } 44 | 45 | return $return; 46 | } 47 | 48 | protected function addControl( 49 | Container $container, 50 | string $key, 51 | string $name, 52 | array $options 53 | ): BaseControl 54 | { 55 | /** 56 | * Set some translated texts 57 | */ 58 | $form = $container->lookup(Form::class); 59 | 60 | if (!$form instanceof Form) { 61 | throw new UnexpectedValueException(); 62 | } 63 | 64 | $translator = $form->getTranslator(); 65 | 66 | if ($translator === null) { 67 | throw new UnexpectedValueException(); 68 | } 69 | 70 | $this->addAttribute( 71 | 'title', 72 | $translator->translate('contributte_datagrid.multiselect_choose') 73 | ); 74 | $this->addAttribute( 75 | 'data-i18n-selected', 76 | $translator->translate('contributte_datagrid.multiselect_selected') 77 | ); 78 | 79 | /** 80 | * Add input to container 81 | */ 82 | $input = $container->addMultiSelect($key, $name, $options); 83 | 84 | $this->addAttributes($input); 85 | 86 | return $input; 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/Filter/FilterRange.php: -------------------------------------------------------------------------------- 1 | addContainer($this->key); 31 | 32 | $from = $container->addText('from', $this->name); 33 | $to = $container->addText('to', $this->nameSecond); 34 | 35 | $this->addAttributes($from); 36 | $this->addAttributes($to); 37 | 38 | $placeholders = $this->getPlaceholders(); 39 | 40 | if ($placeholders !== []) { 41 | $text_from = reset($placeholders); 42 | 43 | if ($text_from) { 44 | $from->setHtmlAttribute('placeholder', $text_from); 45 | } 46 | 47 | $text_to = end($placeholders); 48 | 49 | if ($text_to && ($text_to !== $text_from)) { 50 | $to->setHtmlAttribute('placeholder', $text_to); 51 | } 52 | } 53 | } 54 | 55 | /** 56 | * Set html attr placeholder of both inputs 57 | * 58 | * @return static 59 | */ 60 | public function setPlaceholders(array $placeholders): self 61 | { 62 | $this->placeholders = $placeholders; 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * Get html attr placeholders 69 | */ 70 | public function getPlaceholders(): array 71 | { 72 | return $this->placeholders; 73 | } 74 | 75 | /** 76 | * Get filter condition 77 | */ 78 | public function getCondition(): array 79 | { 80 | $value = $this->getValue(); 81 | 82 | return [ 83 | $this->column => [ 84 | 'from' => $value['from'] ?? '', 85 | 'to' => $value['to'] ?? '', 86 | ], 87 | ]; 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/Filter/FilterSelect.php: -------------------------------------------------------------------------------- 1 | ['selectpicker'], 18 | ]; 19 | 20 | protected ?string $template = 'datagrid_filter_select.latte'; 21 | 22 | protected ?string $type = 'select'; 23 | 24 | protected ?string $prompt = null; 25 | 26 | public function __construct( 27 | Datagrid $grid, 28 | string $key, 29 | string $name, 30 | protected array $options, 31 | string $column 32 | ) 33 | { 34 | parent::__construct($grid, $key, $name, $column); 35 | } 36 | 37 | public function addToFormContainer(Container $container): void 38 | { 39 | $form = $container->lookup(Form::class); 40 | 41 | if (!$form instanceof Form) { 42 | throw new UnexpectedValueException(); 43 | } 44 | 45 | $translator = $form->getTranslator(); 46 | 47 | if ($translator === null) { 48 | throw new UnexpectedValueException(); 49 | } 50 | 51 | $select = $this->addControl($container, $this->key, $this->name, $this->options); 52 | 53 | if (!$this->translateOptions) { 54 | $select->setTranslator(null); 55 | } 56 | } 57 | 58 | /** 59 | * @return static 60 | */ 61 | public function setTranslateOptions(bool $translateOptions = true): self 62 | { 63 | $this->translateOptions = $translateOptions; 64 | 65 | return $this; 66 | } 67 | 68 | public function getOptions(): array 69 | { 70 | return $this->options; 71 | } 72 | 73 | public function getTranslateOptions(): bool 74 | { 75 | return $this->translateOptions; 76 | } 77 | 78 | public function getCondition(): array 79 | { 80 | return [$this->column => $this->getValue()]; 81 | } 82 | 83 | public function getPrompt(): ?string 84 | { 85 | return $this->prompt; 86 | } 87 | 88 | /** 89 | * @return static 90 | */ 91 | public function setPrompt(?string $prompt): self 92 | { 93 | $this->prompt = $prompt; 94 | 95 | return $this; 96 | } 97 | 98 | protected function addControl( 99 | Container $container, 100 | string $key, 101 | string $name, 102 | array $options 103 | ): BaseControl 104 | { 105 | $input = $container->addSelect($key, $name, $options); 106 | 107 | if ($this->getPrompt() !== null) { 108 | $input->setPrompt($this->getPrompt()); 109 | } 110 | 111 | $this->addAttributes($input); 112 | 113 | return $input; 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /src/Filter/FilterText.php: -------------------------------------------------------------------------------- 1 | addText($this->key, $this->name); 40 | 41 | $this->addAttributes($control); 42 | 43 | if ($this->getPlaceholder() !== null) { 44 | $control->setHtmlAttribute('placeholder', $this->getPlaceholder()); 45 | } 46 | } 47 | 48 | /** 49 | * Return array of conditions to put in result [column1 => value, column2 => value] 50 | * If more than one column exists in fitler text, 51 | * than there is OR clause put betweeen their conditions 52 | * Or callback in case of custom condition callback 53 | */ 54 | public function getCondition(): array 55 | { 56 | return array_fill_keys($this->columns, $this->getValue()); 57 | } 58 | 59 | public function isExactSearch(): bool 60 | { 61 | return $this->exact; 62 | } 63 | 64 | /** 65 | * @return static 66 | */ 67 | public function setExactSearch(bool $exact = true): self 68 | { 69 | $this->exact = $exact; 70 | 71 | return $this; 72 | } 73 | 74 | /** 75 | * @return static 76 | */ 77 | public function setSplitWordsSearch(bool $splitWordsSearch): self 78 | { 79 | $this->splitWordsSearch = $splitWordsSearch; 80 | 81 | return $this; 82 | } 83 | 84 | public function hasSplitWordsSearch(): bool 85 | { 86 | return $this->splitWordsSearch; 87 | } 88 | 89 | public function setConjunctionSearch(bool $conjunctionSearch = true): void 90 | { 91 | $this->conjunctionSearch = $conjunctionSearch; 92 | } 93 | 94 | public function hasConjunctionSearch(): bool 95 | { 96 | return $this->conjunctionSearch; 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/Filter/IFilterDate.php: -------------------------------------------------------------------------------- 1 | column; 26 | } 27 | 28 | public function getCondition(): array 29 | { 30 | return [$this->column => $this->getValue()]; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/Filter/SubmitButton.php: -------------------------------------------------------------------------------- 1 | text); 27 | 28 | $this->text = 'contributte_datagrid.filter_submit_button'; 29 | $this->class = 'btn btn-sm btn-primary'; 30 | $this->icon = 'search'; 31 | 32 | $this->control = Html::el('button', ['type' => 'submit', 'name' => 'submit']); 33 | } 34 | 35 | public function getControl(Stringable|string|null $caption = null): Html 36 | { 37 | $el = parent::getControl($caption); 38 | 39 | $el->setAttribute('type', 'submit'); 40 | $el->setAttribute('class', $this->getClass()); 41 | 42 | if ($this->getIcon() !== null) { 43 | $el->addHtml( 44 | Html::el('span')->appendAttribute( 45 | 'class', 46 | Datagrid::$iconPrefix . $this->getIcon() 47 | ) 48 | ); 49 | 50 | if ($this->getText() !== '') { 51 | $el->addHtml(' '); 52 | } 53 | } 54 | 55 | $el->addText($this->grid->getTranslator()->translate($this->getText())); 56 | 57 | return $el; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/GroupAction/GroupAction.php: -------------------------------------------------------------------------------- 1 | title; 29 | } 30 | 31 | /** 32 | * @return static 33 | */ 34 | public function setClass(string $class): self 35 | { 36 | $this->class = $class; 37 | 38 | return $this; 39 | } 40 | 41 | public function getClass(): string 42 | { 43 | return $this->class; 44 | } 45 | 46 | /** 47 | * @return static 48 | */ 49 | public function setAttribute(string $key, mixed $value): self 50 | { 51 | $this->attributes[$key] = $value; 52 | 53 | return $this; 54 | } 55 | 56 | public function getAttributes(): array 57 | { 58 | return $this->attributes; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/GroupAction/GroupButtonAction.php: -------------------------------------------------------------------------------- 1 | class = $class; 22 | } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/GroupAction/GroupMultiSelectAction.php: -------------------------------------------------------------------------------- 1 | options; 16 | } 17 | 18 | /** 19 | * Has the action some options? 20 | */ 21 | public function hasOptions(): bool 22 | { 23 | return $this->options !== []; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/GroupAction/GroupTextAction.php: -------------------------------------------------------------------------------- 1 | shouldBeRendered; 18 | } 19 | 20 | public function setShouldBeRendered(bool $shouldBeRendered): void 21 | { 22 | $this->shouldBeRendered = $shouldBeRendered; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/InlineEdit/InlineEdit.php: -------------------------------------------------------------------------------- 1 | */ 62 | protected array $dataAttributes = []; 63 | 64 | public function __construct(protected Datagrid $grid, protected ?string $primaryWhereColumn = null) 65 | { 66 | $this->title = 'contributte_datagrid.edit'; 67 | $this->class = sprintf('btn btn-xs %s ajax', $grid::$btnSecondaryClass); 68 | $this->icon = 'pencil pencil-alt'; 69 | 70 | $this->onControlAfterAdd[] = [$this, 'addControlsClasses']; 71 | } 72 | 73 | /** 74 | * @return static 75 | */ 76 | public function setItemId(mixed $id): self 77 | { 78 | $this->itemID = $id; 79 | 80 | return $this; 81 | } 82 | 83 | public function getItemId(): mixed 84 | { 85 | return $this->itemID; 86 | } 87 | 88 | public function getPrimaryWhereColumn(): ?string 89 | { 90 | return $this->primaryWhereColumn; 91 | } 92 | 93 | public function renderButton(Row $row): Html 94 | { 95 | $a = Html::el('a') 96 | ->href($this->grid->link('inlineEdit!', ['id' => $row->getId()])); 97 | 98 | $this->tryAddIcon($a, $this->getIcon(), $this->getText()); 99 | 100 | if ($this->dataAttributes !== []) { 101 | foreach ($this->dataAttributes as $key => $value) { 102 | $a->data($key, $value); 103 | } 104 | } 105 | 106 | $a->addText($this->text); 107 | 108 | if ($this->title !== null) { 109 | $a->setAttribute( 110 | 'title', 111 | $this->grid->getTranslator()->translate($this->title) 112 | ); 113 | } 114 | 115 | if ($this->class !== '') { 116 | $a->appendAttribute('class', $this->class); 117 | } 118 | 119 | $a->appendAttribute('class', 'datagrid-inline-edit-trigger'); 120 | 121 | return $a; 122 | } 123 | 124 | /** 125 | * Render row item detail button 126 | */ 127 | public function renderButtonAdd(): Html 128 | { 129 | $a = Html::el('a') 130 | ->href($this->grid->link('showInlineAdd!')); 131 | 132 | $this->tryAddIcon($a, $this->getIcon(), $this->getText()); 133 | 134 | if ($this->dataAttributes !== []) { 135 | foreach ($this->dataAttributes as $key => $value) { 136 | $a->data($key, $value); 137 | } 138 | } 139 | 140 | $a->addText($this->text); 141 | 142 | if ($this->title !== null) { 143 | $a->setAttribute( 144 | 'title', 145 | $this->grid->getTranslator()->translate($this->title) 146 | ); 147 | } 148 | 149 | if ($this->class !== '') { 150 | $a->appendAttribute('class', $this->class); 151 | } 152 | 153 | return $a; 154 | } 155 | 156 | /** 157 | * @return static 158 | */ 159 | public function setPositionTop(bool $positionTop = true): self 160 | { 161 | $this->positionTop = $positionTop; 162 | 163 | return $this; 164 | } 165 | 166 | public function isPositionTop(): bool 167 | { 168 | return $this->positionTop; 169 | } 170 | 171 | public function isPositionBottom(): bool 172 | { 173 | return !$this->positionTop; 174 | } 175 | 176 | public function addControlsClasses(Container $container): void 177 | { 178 | foreach ($container->getControls() as $key => $control) { 179 | switch ($key) { 180 | case 'submit': 181 | $control->setValidationScope([$container]); 182 | $control->setAttribute('class', 'btn btn-xs btn-primary'); 183 | 184 | break; 185 | 186 | case 'cancel': 187 | $control->setValidationScope([]); 188 | $control->setAttribute('class', 'btn btn-xs btn-danger'); 189 | 190 | break; 191 | 192 | default: 193 | if ($control->getControlPrototype()->getAttribute('class') === null) { 194 | $control->setAttribute('class', 'form-control form-control-sm'); 195 | } 196 | 197 | break; 198 | } 199 | } 200 | } 201 | 202 | /** 203 | * @return static 204 | */ 205 | public function setShowNonEditingColumns(bool $show = true): self 206 | { 207 | $this->showNonEditingColumns = $show; 208 | 209 | return $this; 210 | } 211 | 212 | public function showNonEditingColumns(): bool 213 | { 214 | return $this->showNonEditingColumns; 215 | } 216 | 217 | /** 218 | * @return static 219 | */ 220 | public function setDataAttribute(string $key, mixed $value): self 221 | { 222 | $this->dataAttributes[$key] = $value; 223 | 224 | return $this; 225 | } 226 | 227 | } 228 | -------------------------------------------------------------------------------- /src/Localization/SimpleTranslator.php: -------------------------------------------------------------------------------- 1 | 'No items found. You can reset the filter', 12 | 'contributte_datagrid.no_item_found' => 'No items found.', 13 | 'contributte_datagrid.here' => 'here', 14 | 'contributte_datagrid.items' => 'Items', 15 | 'contributte_datagrid.all' => 'all', 16 | 'contributte_datagrid.from' => 'from', 17 | 'contributte_datagrid.reset_filter' => 'Reset filter', 18 | 'contributte_datagrid.group_actions' => 'Group actions', 19 | 'contributte_datagrid.show' => 'Show', 20 | 'contributte_datagrid.add' => 'Add', 21 | 'contributte_datagrid.edit' => 'Edit', 22 | 'contributte_datagrid.show_all_columns' => 'Show all columns', 23 | 'contributte_datagrid.show_default_columns' => 'Show default columns', 24 | 'contributte_datagrid.hide_column' => 'Hide column', 25 | 'contributte_datagrid.action' => 'Action', 26 | 'contributte_datagrid.previous' => 'Previous', 27 | 'contributte_datagrid.next' => 'Next', 28 | 'contributte_datagrid.choose' => 'Choose', 29 | 'contributte_datagrid.choose_input_required' => 'Group action text not allow empty value', 30 | 'contributte_datagrid.execute' => 'Execute', 31 | 'contributte_datagrid.save' => 'Save', 32 | 'contributte_datagrid.cancel' => 'Cancel', 33 | 'contributte_datagrid.multiselect_choose' => 'Choose', 34 | 'contributte_datagrid.multiselect_selected' => '{0} selected', 35 | 'contributte_datagrid.filter_submit_button' => 'Filter', 36 | 'contributte_datagrid.show_filter' => 'Show filter', 37 | 'contributte_datagrid.per_page_submit' => 'Change', 38 | ]; 39 | 40 | public function __construct(array $dictionary = []) 41 | { 42 | // BC support for 'ublaboo' translations 43 | $oldPrefix = 'ublaboo_'; 44 | $newPrefix = 'contributte_'; 45 | foreach ($dictionary as $key => $value) { 46 | if (str_starts_with($key, $oldPrefix)) { 47 | $newKey = $newPrefix . substr($key, strlen($oldPrefix)); 48 | // Only change the keys that are in the default $dictionary (other keys are left as-is) 49 | if (array_key_exists($newKey, $this->dictionary)) { 50 | trigger_error(sprintf("Translation key '%s' is deprecated, please use '%s' instead", $key, $newKey), E_USER_DEPRECATED); 51 | unset($dictionary[$key]); 52 | $dictionary[$newKey] = $value; 53 | } 54 | } 55 | } 56 | 57 | $this->dictionary = array_merge($this->dictionary, $dictionary); 58 | } 59 | 60 | public function translate(mixed $message, mixed ...$parameters): string 61 | { 62 | return $this->dictionary[$message] ?? $message; 63 | } 64 | 65 | public function setDictionary(array $dictionary): void 66 | { 67 | $this->dictionary = $dictionary; 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/Response/CsvResponse.php: -------------------------------------------------------------------------------- 1 | > */ 22 | protected array $data; 23 | 24 | protected string $outputEncoding; 25 | 26 | protected bool $includeBom; 27 | 28 | protected string $name; 29 | 30 | /** @var string[] */ 31 | protected array $headers = [ 32 | 'Expires' => '0', 33 | 'Cache-Control' => 'no-cache', 34 | 'Pragma' => 'Public', 35 | ]; 36 | 37 | /** 38 | * @param array> $data Input data 39 | */ 40 | public function __construct( 41 | array $data, 42 | string $name = 'export.csv', 43 | string $outputEncoding = 'utf-8', 44 | string $delimiter = ';', 45 | bool $includeBom = false 46 | ) 47 | { 48 | if (strpos($name, '.csv') === false) { 49 | $name = sprintf('%s.csv', $name); 50 | } 51 | 52 | $this->name = $name; 53 | $this->delimiter = $delimiter; 54 | $this->data = $data; 55 | $this->outputEncoding = $outputEncoding; 56 | $this->includeBom = $includeBom; 57 | } 58 | 59 | public function send(HttpRequest $httpRequest, HttpResponse $httpResponse): void 60 | { 61 | // Disable tracy bar 62 | if (class_exists(Debugger::class)) { 63 | Debugger::$productionMode = true; 64 | } 65 | 66 | // Set Content-Type header 67 | $httpResponse->setContentType($this->contentType, $this->outputEncoding); 68 | 69 | // Set Content-Disposition header 70 | $httpResponse->setHeader('Content-Disposition', sprintf('attachment; filename="%s"', $this->name)); 71 | 72 | // Set other headers 73 | foreach ($this->headers as $key => $value) { 74 | $httpResponse->setHeader($key, $value); 75 | } 76 | 77 | if (function_exists('ob_start')) { 78 | ob_start(); 79 | } 80 | 81 | // Output data 82 | if ($this->includeBom) { 83 | echo $this->getBom(); 84 | } 85 | 86 | foreach ($this->data as $row) { 87 | $csvRow = $this->printCsv((array) $row); // @phpstan-ignore-line 88 | 89 | if (strtolower($this->outputEncoding) === 'utf-8') { 90 | echo $csvRow; 91 | } elseif (strtolower($this->outputEncoding) === 'windows-1250') { 92 | echo iconv('utf-8', $this->outputEncoding, $csvRow); 93 | } else { 94 | echo mb_convert_encoding($csvRow, $this->outputEncoding); 95 | } 96 | } 97 | 98 | if (function_exists('ob_end_flush')) { 99 | ob_end_flush(); 100 | } 101 | } 102 | 103 | private function getBom(): string 104 | { 105 | switch (strtolower($this->outputEncoding)) { 106 | case 'utf-8': 107 | return b"\xEF\xBB\xBF"; 108 | case 'utf-16': 109 | return b"\xFF\xFE"; 110 | default: 111 | return ''; 112 | } 113 | } 114 | 115 | /** 116 | * @param array $row 117 | */ 118 | private function printCsv(array $row): string 119 | { 120 | $out = fopen('php://memory', 'wb+'); 121 | 122 | if ($out === false) { 123 | throw new InvalidStateException('Unable to open memory stream'); 124 | } 125 | 126 | fputcsv($out, $row, $this->delimiter, escape: '\\'); 127 | rewind($out); 128 | $c = stream_get_contents($out); 129 | fclose($out); 130 | 131 | if ($c === false) { 132 | throw new InvalidStateException('Unable to read from memory stream'); 133 | } 134 | 135 | return $c; 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /src/Status/Option.php: -------------------------------------------------------------------------------- 1 | value; 37 | } 38 | 39 | public function endOption(): ColumnStatus 40 | { 41 | return $this->columnStatus; 42 | } 43 | 44 | /** 45 | * @return static 46 | */ 47 | public function setTitle(string $title): self 48 | { 49 | $this->title = $title; 50 | 51 | return $this; 52 | } 53 | 54 | public function getTitle(): ?string 55 | { 56 | return $this->title; 57 | } 58 | 59 | /** 60 | * @return static 61 | */ 62 | public function setClass(string $class, ?string $classSecondary = null): self 63 | { 64 | $this->class = $class; 65 | 66 | if ($classSecondary !== null) { 67 | $this->classSecondary = $classSecondary; 68 | } 69 | 70 | return $this; 71 | } 72 | 73 | public function getClass(): ?string 74 | { 75 | return $this->class; 76 | } 77 | 78 | /** 79 | * @return static 80 | */ 81 | public function setClassSecondary(string $classSecondary): self 82 | { 83 | $this->classSecondary = $classSecondary; 84 | 85 | return $this; 86 | } 87 | 88 | public function getClassSecondary(): string 89 | { 90 | return $this->classSecondary; 91 | } 92 | 93 | /** 94 | * @return static 95 | */ 96 | public function setClassInDropdown(string $classInDropdown): self 97 | { 98 | $this->classInDropdown = $classInDropdown; 99 | 100 | return $this; 101 | } 102 | 103 | public function getClassInDropdown(): string 104 | { 105 | return $this->classInDropdown; 106 | } 107 | 108 | /** 109 | * @return static 110 | */ 111 | public function setIcon(string $icon): self 112 | { 113 | $this->icon = $icon; 114 | 115 | return $this; 116 | } 117 | 118 | public function getIcon(): ?string 119 | { 120 | return $this->icon; 121 | } 122 | 123 | /** 124 | * @return static 125 | */ 126 | public function setIconSecondary(string $iconSecondary): self 127 | { 128 | $this->iconSecondary = $iconSecondary; 129 | 130 | return $this; 131 | } 132 | 133 | public function getIconSecondary(): ?string 134 | { 135 | return $this->iconSecondary; 136 | } 137 | 138 | public function getText(): string 139 | { 140 | return $this->text; 141 | } 142 | 143 | /** 144 | * @return static 145 | */ 146 | public function setConfirmation(IConfirmation $confirmation): self 147 | { 148 | $this->confirmation = $confirmation; 149 | 150 | return $this; 151 | } 152 | 153 | public function getConfirmationDialog(Row $row): ?string 154 | { 155 | if ($this->confirmation === null) { 156 | return null; 157 | } 158 | 159 | if ($this->confirmation instanceof CallbackConfirmation) { 160 | return ($this->confirmation->getCallback())($row->getItem()); 161 | } 162 | 163 | if ($this->confirmation instanceof StringConfirmation) { 164 | $question = $this->grid->getTranslator()->translate($this->confirmation->getQuestion()); 165 | 166 | if ($this->confirmation->getPlaceholderName() === null) { 167 | return $question; 168 | } 169 | 170 | return str_replace( 171 | '%s', 172 | (string) $row->getValue($this->confirmation->getPlaceholderName()), 173 | $question 174 | ); 175 | } 176 | 177 | throw new DatagridException('Unsupported confirmation'); 178 | } 179 | 180 | } 181 | -------------------------------------------------------------------------------- /src/Toolbar/ToolbarButton.php: -------------------------------------------------------------------------------- 1 | text = $text; 37 | } 38 | 39 | /** 40 | * Render toolbar button 41 | */ 42 | public function renderButton(): Html 43 | { 44 | try { 45 | // Renderer function may be used 46 | return $this->useRenderer(); 47 | } catch (DatagridColumnRendererException) { 48 | // Do not use renderer 49 | } 50 | 51 | $link = $this->createLink($this->grid, $this->href, $this->params); 52 | 53 | $a = Html::el('a')->href($link); 54 | 55 | $this->tryAddIcon($a, $this->getIcon(), $this->getText()); 56 | 57 | if ($this->attributes !== []) { 58 | $a->addAttributes($this->attributes); 59 | } 60 | 61 | $a->addText($this->grid->getTranslator()->translate($this->text)); 62 | 63 | if ($this->getTitle() !== null) { 64 | $a->setAttribute( 65 | 'title', 66 | $this->grid->getTranslator()->translate($this->getTitle()) 67 | ); 68 | } 69 | 70 | $a->setAttribute('class', $this->getClass()); 71 | 72 | if ($this->confirmDialog !== null) { 73 | $a->setAttribute('data-datagrid-confirm', $this->confirmDialog); 74 | } 75 | 76 | return $a; 77 | } 78 | 79 | /** 80 | * @return static 81 | */ 82 | public function addAttributes(array $attrs): static 83 | { 84 | $this->attributes += $attrs; 85 | 86 | return $this; 87 | } 88 | 89 | /** 90 | * Add Confirm dialog 91 | */ 92 | public function setConfirmDialog(string $confirmDialog): self 93 | { 94 | $this->confirmDialog = $confirmDialog; 95 | 96 | return $this; 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/Traits/TButtonCaret.php: -------------------------------------------------------------------------------- 1 | caret = $useCaret; 16 | 17 | return $this; 18 | } 19 | 20 | public function hasCaret(): bool 21 | { 22 | return $this->caret; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/Traits/TButtonClass.php: -------------------------------------------------------------------------------- 1 | class = $class; 16 | 17 | return $this; 18 | } 19 | 20 | public function getClass(): string 21 | { 22 | return $this->class; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/Traits/TButtonIcon.php: -------------------------------------------------------------------------------- 1 | icon = $icon; 16 | 17 | return $this; 18 | } 19 | 20 | public function getIcon(): ?string 21 | { 22 | return $this->icon; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/Traits/TButtonRenderer.php: -------------------------------------------------------------------------------- 1 | getRenderer(); 23 | 24 | $args = $row instanceof Row ? [$row->getItem()] : []; 25 | 26 | if ($renderer === null) { 27 | throw new DatagridColumnRendererException(); 28 | } 29 | 30 | if ($renderer->getConditionCallback() !== null) { 31 | if (call_user_func_array($renderer->getConditionCallback(), $args) === false) { 32 | throw new DatagridColumnRendererException(); 33 | } 34 | 35 | return call_user_func_array($renderer->getCallback(), $args); 36 | } 37 | 38 | return call_user_func_array($renderer->getCallback(), $args); 39 | } 40 | 41 | /** 42 | * Set renderer callback and (it may be optional - the condition callback will decide) 43 | * 44 | * @return static 45 | * @throws DatagridException 46 | */ 47 | public function setRenderer( 48 | callable $renderer, 49 | ?callable $conditionCallback = null 50 | ): self 51 | { 52 | if ($this->hasReplacements()) { 53 | throw new DatagridException('Use either Column::setReplacement() or Column::setRenderer, not both.'); 54 | } 55 | 56 | $this->renderer = new Renderer($renderer, $conditionCallback); 57 | 58 | return $this; 59 | } 60 | 61 | /** 62 | * @return static 63 | */ 64 | public function setRendererOnCondition( 65 | callable $renderer, 66 | callable $conditionCallback 67 | ): self 68 | { 69 | return $this->setRenderer($renderer, $conditionCallback); 70 | } 71 | 72 | public function getRenderer(): ?Renderer 73 | { 74 | return $this->renderer; 75 | } 76 | 77 | public function hasReplacements(): bool 78 | { 79 | return $this->replacements !== []; 80 | } 81 | 82 | public function applyReplacements(Row $row, string $column): array 83 | { 84 | $value = $row->getValue($column); 85 | 86 | if ((is_scalar($value) || $value === null) && 87 | isset($this->replacements[gettype($value) === 'double' ? (int) $value : $value])) { 88 | return [true, $this->replacements[gettype($value) === 'double' ? (int) $value : $value]]; 89 | } 90 | 91 | return [false, null]; 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/Traits/TButtonText.php: -------------------------------------------------------------------------------- 1 | text = $text; 16 | 17 | return $this; 18 | } 19 | 20 | public function getText(): string 21 | { 22 | return $this->text; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/Traits/TButtonTitle.php: -------------------------------------------------------------------------------- 1 | title = $title; 16 | 17 | return $this; 18 | } 19 | 20 | public function getTitle(): ?string 21 | { 22 | return $this->title; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/Traits/TButtonTryAddIcon.php: -------------------------------------------------------------------------------- 1 | addHtml(Html::el('span')->setAttribute('class', trim($iconClass))); 21 | 22 | if (mb_strlen($name) > 1) { 23 | $el->addHtml(' '); 24 | } 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/Traits/TLink.php: -------------------------------------------------------------------------------- 1 | getPresenter(); 32 | 33 | if (str_contains($href, ':')) { 34 | return $presenter->link($href, $params); 35 | } 36 | 37 | for ($iteration = 0; $iteration < 10; $iteration++) { 38 | $targetComponent = $targetComponent->getParent(); 39 | 40 | if (!$targetComponent instanceof Component) { 41 | throw $this->createHierarchyLookupException($grid, $href, $params); 42 | } 43 | 44 | try { 45 | $link = $targetComponent->link($href, $params); 46 | } catch (InvalidLinkException) { 47 | $link = false; 48 | } 49 | 50 | if (is_string($link)) { 51 | if ( 52 | str_starts_with($link, '#error') || 53 | (strrpos($href, '!') !== false && str_starts_with($link, '#')) || 54 | (in_array($presenter->invalidLinkMode, [Presenter::InvalidLinkWarning, Presenter::InvalidLinkSilent], true) && str_starts_with($link, '#')) 55 | ) { 56 | continue; // Did not find signal handler 57 | } 58 | 59 | return $link; // Found signal handler! 60 | } else { 61 | continue; // Did not find signal handler 62 | } 63 | } 64 | 65 | // Went 10 steps up to the Presenter and did not find any signal handler 66 | throw $this->createHierarchyLookupException($grid, $href, $params); 67 | } 68 | 69 | private function createHierarchyLookupException( 70 | Datagrid $grid, 71 | string $href, 72 | array $params 73 | ): DatagridLinkCreationException 74 | { 75 | $parent = $grid->getParent(); 76 | $presenter = $grid->getPresenter(); 77 | 78 | if ($parent === null) { 79 | throw new UnexpectedValueException( 80 | sprintf('%s can not live withnout a parent component', self::class) 81 | ); 82 | } 83 | 84 | $desiredHandler = $parent::class . '::handle' . ucfirst($href) . '()'; 85 | 86 | return new DatagridLinkCreationException( 87 | 'Datagrid could not create link "' 88 | . $href . '" - did not find any signal handler in componenet hierarchy from ' 89 | . $parent::class . ' up to the ' 90 | . $presenter::class . '. ' 91 | . 'Try adding handler ' . $desiredHandler 92 | ); 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/Traits/TRenderCondition.php: -------------------------------------------------------------------------------- 1 | renderConditionCallback = $condition; 19 | 20 | return $this; 21 | } 22 | 23 | public function shouldBeRendered(Row $row): bool 24 | { 25 | $condition = $this->renderConditionCallback; 26 | 27 | return is_callable($condition) 28 | ? ($condition)($row->getItem()) 29 | : true; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/Utils/ArraysHelper.php: -------------------------------------------------------------------------------- 1 | getTimezone(); 60 | $date = new DateTime('now', $tz !== false ? $tz : null); 61 | $date->setTimestamp($value->getTimestamp()); 62 | 63 | return $date; 64 | } 65 | 66 | foreach ($formats as $format) { 67 | $date = DateTime::createFromFormat($format, (string) $value); 68 | 69 | if ($date === false) { 70 | continue; 71 | } 72 | 73 | return $date; 74 | } 75 | 76 | $timestamp = strtotime((string) $value); 77 | 78 | if ($timestamp !== false) { 79 | $date = new DateTime(); 80 | $date->setTimestamp($timestamp); 81 | 82 | return $date; 83 | } 84 | 85 | throw new DatagridDateTimeHelperException(); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/Utils/ItemDetailForm.php: -------------------------------------------------------------------------------- 1 | */ 22 | private array $containerSetByName = []; 23 | 24 | public function __construct(callable $callableSetContainer) 25 | { 26 | $this->monitor( 27 | Presenter::class, 28 | function (Presenter $presenter): void { 29 | $this->loadHttpData(); 30 | } 31 | ); 32 | 33 | $this->callableSetContainer = $callableSetContainer; 34 | } 35 | 36 | public function offsetGet(mixed $name): IComponent 37 | { 38 | return $this->getComponentAndSetContainer($name); 39 | } 40 | 41 | public function getComponentAndSetContainer(mixed $name): IComponent 42 | { 43 | $container = $this->addContainer($name); 44 | 45 | if (!isset($this->containerSetByName[$name])) { 46 | call_user_func($this->callableSetContainer, $container); 47 | 48 | $this->containerSetByName[$name] = true; 49 | } 50 | 51 | return $container; 52 | } 53 | 54 | /** 55 | * @throws UnexpectedValueException 56 | */ 57 | private function getHttpData(): mixed 58 | { 59 | if ($this->httpPost === null) { 60 | $lookupPath = $this->lookupPath(Form::class); 61 | $form = $this->getForm(); 62 | 63 | $path = explode(self::NameSeparator, $lookupPath); 64 | 65 | /** @var array $httpData */ 66 | $httpData = $form->getHttpData(); 67 | $this->httpPost = Arrays::get($httpData, $path, null); 68 | } 69 | 70 | return $this->httpPost; 71 | } 72 | 73 | /** 74 | * @throws UnexpectedValueException 75 | */ 76 | private function loadHttpData(): void 77 | { 78 | $form = $this->getForm(); 79 | 80 | if ($form->isSubmitted() === false) { 81 | return; 82 | } 83 | 84 | foreach ((array) $this->getHttpData() as $name => $value) { 85 | if ((is_iterable($value))) { 86 | $this->getComponentAndSetContainer($name); 87 | } 88 | } 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/Utils/NetteDatabaseSelectionHelper.php: -------------------------------------------------------------------------------- 1 | getConnection(); 16 | 17 | return $connection->getDriver(); 18 | } 19 | 20 | public static function getContext(Selection $selection): Explorer 21 | { 22 | $reflection = new ReflectionClass($selection); 23 | 24 | $explorerProperty = $reflection->getProperty('explorer'); 25 | $explorerProperty->setAccessible(true); 26 | 27 | return $explorerProperty->getValue($selection); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/Utils/PropertyAccessHelper.php: -------------------------------------------------------------------------------- 1 | getValue($class, $property); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/Utils/Sorting.php: -------------------------------------------------------------------------------- 1 | sortCallback = $sortCallback; 17 | } 18 | 19 | /** 20 | * @return array|string[] 21 | */ 22 | public function getSort(): array 23 | { 24 | return $this->sort; 25 | } 26 | 27 | public function getSortCallback(): ?callable 28 | { 29 | return $this->sortCallback; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/templates/column_multi_action.latte: -------------------------------------------------------------------------------- 1 | {** 2 | * @param $multiAction Contributte\Datagrid\Column\MultiAction 3 | *} 4 | 5 | 19 | -------------------------------------------------------------------------------- /src/templates/column_status.latte: -------------------------------------------------------------------------------- 1 | {** 2 | * @param $row Contributte\Datagrid\Row 3 | * @param $status Contributte\Datagrid\Column\ColumnStatus 4 | *} 5 | 6 | {var $activeOption = $status->getCurrentOption($row)} 7 | 8 | 38 | -------------------------------------------------------------------------------- /src/templates/datagrid_filter_date.latte: -------------------------------------------------------------------------------- 1 | {** 2 | * @param Filter $filter 3 | * @param Nette\Forms\Controls\TextInput $input 4 | * @param string $iconPrefix Icon prefix (fa fa-) 5 | *} 6 | 7 | {if $outer} 8 |
9 | {label $input class => 'col-sm-3 control-label' /} 10 |
11 |
12 | {input $input} 13 | 16 |
17 |
18 |
19 | {else} 20 |
21 |
22 | {input $input} 23 | 26 |
27 |
28 | {/if} 29 | -------------------------------------------------------------------------------- /src/templates/datagrid_filter_daterange.latte: -------------------------------------------------------------------------------- 1 | {** 2 | * @param Filter $filter 3 | * @param Nette\Forms\Container $input 4 | * @param string $iconPrefix Icon prefix (fa fa-) 5 | *} 6 | 7 | {var $container = $input} 8 | 9 | {if $outer} 10 |
11 | {label $container['from'], class => 'col-sm-3 control-label' /} 12 |
13 |
14 | {input $container['from']} 15 | 18 |
19 |
20 | {label $container['to'], class => 'filter-range-delimiter col-sm-1 control-label' /} 21 |
22 |
23 | {input $container['to']} 24 | 27 |
28 |
29 |
30 | {else} 31 |
32 |
33 | {input $container['from']} 34 | 37 |
38 | 39 |
-
40 | 41 |
42 | {input $container['to']} 43 | 46 |
47 |
48 | {/if} 49 | -------------------------------------------------------------------------------- /src/templates/datagrid_filter_range.latte: -------------------------------------------------------------------------------- 1 | {** 2 | * @param Filter $filter 3 | * @param Nette\Forms\Container $input 4 | *} 5 | 6 | {var $container = $input} 7 | 8 | {if $outer} 9 |
10 | {label $container['from'], class => 'col-sm-3 control-label' /} 11 |
12 | {input $container['from']} 13 |
14 | {label $container['to'], class => 'filter-range-delimiter col-sm-1 control-label' /} 15 |
16 | {input $container['to']} 17 |
18 |
19 | {else} 20 |
21 |
22 | {input $container['from']} 23 | 24 |
-
25 | 26 | {input $container['to']} 27 |
28 |
29 | {/if} 30 | -------------------------------------------------------------------------------- /src/templates/datagrid_filter_select.latte: -------------------------------------------------------------------------------- 1 | {** 2 | * @param Filter $filter 3 | * @param Nette\Forms\Controls\SelectBox $input 4 | *} 5 | 6 | {if $outer} 7 |
8 | {label $input class => 'col-sm-3 control-label' /} 9 |
10 | {input $input} 11 |
12 |
13 | {else} 14 | {input $input} 15 | {/if} 16 | -------------------------------------------------------------------------------- /src/templates/datagrid_filter_text.latte: -------------------------------------------------------------------------------- 1 | {** 2 | * @param Filter $filter 3 | * @param Nette\Forms\Controls\TextInput $input 4 | *} 5 | 6 | {if $outer} 7 |
8 | {label $input class => 'col-sm-3 control-label' /} 9 |
10 | {input $input} 11 |
12 |
13 | {else} 14 | {input $input} 15 | {/if} 16 | -------------------------------------------------------------------------------- /src/templates/datagrid_tree.latte: -------------------------------------------------------------------------------- 1 | {** 2 | * @param array $columns Available columns 3 | * @param array $actions Available actions 4 | * @param array $exports Available exports 5 | * @param Row[] $rows List of rows (each contain a item from data source) 6 | * @param Datagrid $control Parent (Datagrid) 7 | * @param string $originalTemplate Original template file path 8 | * @param string $iconPrefix Icon prefix (fa fa-) 9 | *} 10 | 11 | {extends $originalTemplate} 12 | 13 |
isSortable()}data-sortable-tree data-sortable-url="{plink $control->getSortableHandler()}" data-sortable-parent-path="{$control->getSortableParentPath()}"{/if}> 14 | {snippetArea items} 15 |
16 |
17 | 18 | {foreach $toolbarButtons as $toolbarButton}{$toolbarButton->renderButton()}{/foreach} 19 | 20 |
21 |
22 |
23 | {foreach $columns as $key => $column} 24 | {$column->getName()|translate} 25 | {breakIf TRUE} 26 | {/foreach} 27 |
28 | 29 |
30 |
31 | {foreach $columns as $key => $column} 32 | {continueIf $iterator->isFirst()} 33 |
34 | {$column->getName()|translate} 35 |
36 | {/foreach} 37 |
38 |
39 |
40 | {var $tmpRow = reset($rows)} 41 | 42 | {foreach $actions as $key => $action} 43 | {if $tmpRow->hasAction($key)} 44 | {if $action->hasTemplate()} 45 | {include $action->getTemplate(), item => $tmpRow->getItem(), (expand) $action->getTemplateVariables(), row => $tmpRow} 46 | {else} 47 | {$action->render($tmpRow)|noescape} 48 | {/if} 49 | {/if} 50 | {/foreach} 51 | 52 | 53 | 54 | 55 |
56 |
57 |
58 |
59 |
60 | 61 | {foreach $rows as $row} 62 | {var $hasChildren = $control->hasTreeViewChildrenCallback() ? $control->treeViewChildrenCallback($row->getItem()) : $row->getValue($treeViewHasChildrenColumn)} 63 | {var $item = $row->getItem()} 64 | 65 |
66 |
67 |
68 | 69 | 70 | 71 | {foreach $columns as $key => $column} 72 | {var $col = 'col-' . $key} 73 | {php $column = $row->applyColumnCallback($key, clone $column)} 74 | 75 | {if $column->hasTemplate()} 76 | {include $column->getTemplate(), item => $item, (expand) $column->getTemplateVariables()} 77 | {else} 78 | {ifset #$col} 79 | {include #$col, item => $item} 80 | {else} 81 | {if $column->isTemplateEscaped()} 82 | {$column->render($row)} 83 | {else} 84 | {$column->render($row)|noescape} 85 | {/if} 86 | {/ifset} 87 | {/if} 88 | 89 | {breakIf TRUE} 90 | {/foreach} 91 |
92 |
93 |
94 | {foreach $columns as $key => $column} 95 | {continueIf $iterator->isFirst()} 96 | 97 |
98 | {var $col = 'col-' . $key} 99 | {php $column = $row->applyColumnCallback($key, clone $column)} 100 | 101 | {if $column->hasTemplate()} 102 | {include $column->getTemplate(), row => $row, item => $item, (expand) $column->getTemplateVariables()} 103 | {else} 104 | {ifset #$col} 105 | {include #$col, item => $item} 106 | {else} 107 | {if $column->isTemplateEscaped()} 108 | {$column->render($row)} 109 | {else} 110 | {$column->render($row)|noescape} 111 | {/if} 112 | {/ifset} 113 | {/if} 114 |
115 | {/foreach} 116 |
117 |
118 |
119 | {foreach $actions as $key => $action} 120 | {if $row->hasAction($key)} 121 | {if $action->hasTemplate()} 122 | {include $action->getTemplate(), item => $item, (expand) $action->getTemplateVariables(), row => $row} 123 | {else} 124 | {$action->render($row)|noescape} 125 | {/if} 126 | {/if} 127 | {/foreach} 128 | 129 | 130 | 131 | 132 |
133 |
134 |
135 |
136 |
isSortable()}data-sortable-tree data-sortable-url="{plink $control->getSortableHandler()}"{/if}>
137 |
138 | {/foreach} 139 | {if !$rows} 140 | {='contributte_datagrid.no_item_found'|translate} 141 | {/if} 142 | {/snippetArea} 143 |
144 | --------------------------------------------------------------------------------