@contributte/datagrid 0.0.0-20250531-d4580b9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +84 -0
- package/assets/ajax/index.ts +1 -0
- package/assets/ajax/naja.ts +154 -0
- package/assets/css/datagrid-all.css +6 -0
- package/assets/css/datagrid-full.css +7 -0
- package/assets/css/datagrid.css +651 -0
- package/assets/css/happy.css +463 -0
- package/assets/datagrid-all.ts +1 -0
- package/assets/datagrid-full.ts +53 -0
- package/assets/datagrid.ts +228 -0
- package/assets/index.ts +4 -0
- package/assets/integrations/happy.ts +217 -0
- package/assets/integrations/index.ts +4 -0
- package/assets/integrations/sortable-js.ts +58 -0
- package/assets/integrations/tom-select.ts +30 -0
- package/assets/integrations/vanilla-datepicker.ts +25 -0
- package/assets/plugins/features/autosubmit.ts +87 -0
- package/assets/plugins/features/checkboxes.ts +92 -0
- package/assets/plugins/features/confirm.ts +28 -0
- package/assets/plugins/features/editable.ts +118 -0
- package/assets/plugins/features/inline.ts +89 -0
- package/assets/plugins/features/item-detail.ts +48 -0
- package/assets/plugins/features/treeView.ts +44 -0
- package/assets/plugins/index.ts +13 -0
- package/assets/plugins/integrations/datepicker.ts +25 -0
- package/assets/plugins/integrations/happy.ts +19 -0
- package/assets/plugins/integrations/nette-forms.ts +18 -0
- package/assets/plugins/integrations/selectpicker.ts +25 -0
- package/assets/plugins/integrations/sortable.ts +61 -0
- package/assets/types/ajax.d.ts +143 -0
- package/assets/types/datagrid.d.ts +31 -0
- package/assets/types/index.d.ts +41 -0
- package/assets/types/integrations.d.ts +15 -0
- package/assets/utils.ts +179 -0
- package/dist/datagrid-full.css +1 -0
- package/dist/datagrid-full.js +20 -0
- package/dist/datagrid-full.js.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { defaultDatagridNameResolver, isEnter } from "./utils";
|
|
2
|
+
import type { Ajax, DatagridEventMap, DatagridOptions, DatagridsOptions, EventDetail, EventListener, } from "./types";
|
|
3
|
+
import Select from "tom-select";
|
|
4
|
+
import {
|
|
5
|
+
AutosubmitPlugin,
|
|
6
|
+
CheckboxPlugin,
|
|
7
|
+
ConfirmPlugin,
|
|
8
|
+
HappyPlugin,
|
|
9
|
+
InlinePlugin,
|
|
10
|
+
NetteFormsPlugin,
|
|
11
|
+
SelectpickerPlugin,
|
|
12
|
+
SortablePlugin
|
|
13
|
+
} from "./plugins";
|
|
14
|
+
import { SortableJS } from "./integrations";
|
|
15
|
+
import { DatepickerPlugin } from "./plugins";
|
|
16
|
+
import { Happy, TomSelect, VanillaDatepicker } from "./integrations";
|
|
17
|
+
|
|
18
|
+
export class Datagrid extends EventTarget {
|
|
19
|
+
private static readonly defaultOptions: DatagridOptions = {
|
|
20
|
+
confirm: confirm,
|
|
21
|
+
resolveDatagridName: defaultDatagridNameResolver,
|
|
22
|
+
plugins: [],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
public readonly name: string;
|
|
26
|
+
|
|
27
|
+
public readonly ajax: Ajax;
|
|
28
|
+
|
|
29
|
+
private readonly options: DatagridOptions;
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
public readonly el: HTMLElement,
|
|
33
|
+
ajax: Ajax | ((grid: Datagrid) => Ajax),
|
|
34
|
+
options: Partial<DatagridOptions>
|
|
35
|
+
) {
|
|
36
|
+
super();
|
|
37
|
+
|
|
38
|
+
this.options = {
|
|
39
|
+
...Datagrid.defaultOptions,
|
|
40
|
+
...options,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const name = this.resolveDatagridName();
|
|
44
|
+
|
|
45
|
+
if (!name) {
|
|
46
|
+
throw new Error("Cannot resolve name of a datagrid!");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.name = name;
|
|
50
|
+
|
|
51
|
+
this.ajax = typeof ajax === "function" ? ajax(this) : ajax;
|
|
52
|
+
|
|
53
|
+
this.ajax.addEventListener("success", e => {
|
|
54
|
+
if (e.detail.payload?._datagrid_name === this.name && e.detail.payload?._datagrid_init) {
|
|
55
|
+
this.init();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
this.init();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public init() {
|
|
63
|
+
let cancelled = !this.dispatch('beforeInit', { datagrid: this })
|
|
64
|
+
if (!cancelled) {
|
|
65
|
+
this.options.plugins.forEach((plugin) => {
|
|
66
|
+
plugin.onDatagridInit?.(this)
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Uncheck toggle-all
|
|
71
|
+
const checkedRows = this.el.querySelectorAll<HTMLInputElement>("input[data-check]:checked");
|
|
72
|
+
if (checkedRows.length === 1 && checkedRows[0].getAttribute("name") === "toggle-all") {
|
|
73
|
+
const input = checkedRows[0];
|
|
74
|
+
if (input) {
|
|
75
|
+
input.checked = false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.el.querySelectorAll<HTMLInputElement>("input[data-datagrid-manualsubmit]").forEach(inputEl => {
|
|
80
|
+
const form = inputEl.closest("form");
|
|
81
|
+
if (!form) return;
|
|
82
|
+
|
|
83
|
+
inputEl.addEventListener("keydown", e => {
|
|
84
|
+
if (!isEnter(e)) return;
|
|
85
|
+
|
|
86
|
+
e.stopPropagation();
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
return this.ajax.submitForm(form);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
this.ajax.addEventListener("success", ({ detail: { payload } }) => {
|
|
93
|
+
// todo: maybe move?
|
|
94
|
+
if (payload._datagrid_name && payload._datagrid_name === this.name) {
|
|
95
|
+
const getColumnName = (el: HTMLElement) => el.getAttribute(
|
|
96
|
+
"data-datagrid-reset-filter-by-column"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
const resets = Array.from<HTMLElement>(this.el.querySelectorAll(
|
|
100
|
+
`[data-datagrid-reset-filter-by-column]`
|
|
101
|
+
));
|
|
102
|
+
|
|
103
|
+
const nonEmptyFilters = payload.non_empty_filters ? payload.non_empty_filters : [] as string[];
|
|
104
|
+
|
|
105
|
+
resets.forEach((el) => {
|
|
106
|
+
const columnName = getColumnName(el);
|
|
107
|
+
|
|
108
|
+
if (columnName && nonEmptyFilters.includes(columnName)) {
|
|
109
|
+
el.classList.remove("hidden");
|
|
110
|
+
} else {
|
|
111
|
+
el.classList.add("hidden");
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (nonEmptyFilters.length > 0) {
|
|
116
|
+
const href = this.el.querySelector(".reset-filter")
|
|
117
|
+
?.getAttribute("href");
|
|
118
|
+
|
|
119
|
+
if (href) {
|
|
120
|
+
resets.forEach((el) => {
|
|
121
|
+
const columnName = getColumnName(el);
|
|
122
|
+
|
|
123
|
+
const newHref = href.replace("-resetFilter", "-resetColumnFilter");
|
|
124
|
+
el.setAttribute("href", `${newHref}&${this.name}-key=${columnName}`);
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
this.dispatch('afterInit', { datagrid: this });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
public confirm(message: string): boolean {
|
|
135
|
+
return this.options.confirm.bind(this)(message);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
public resolveDatagridName(): string | null {
|
|
139
|
+
return this.options.resolveDatagridName.bind(this)(this.el);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
dispatch<
|
|
143
|
+
K extends string, M extends DatagridEventMap = DatagridEventMap
|
|
144
|
+
>(type: K, detail: K extends keyof M ? EventDetail<M[K]> : any, options?: boolean): boolean {
|
|
145
|
+
return this.dispatchEvent(new CustomEvent(type, { detail }));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
declare addEventListener: <K extends keyof M, M extends DatagridEventMap = DatagridEventMap>(
|
|
149
|
+
type: K,
|
|
150
|
+
listener: EventListener<this, M[K]>,
|
|
151
|
+
options?: boolean | AddEventListenerOptions
|
|
152
|
+
) => void;
|
|
153
|
+
|
|
154
|
+
declare removeEventListener: <K extends keyof M, M extends DatagridEventMap = DatagridEventMap>(
|
|
155
|
+
type: K,
|
|
156
|
+
listener: EventListener<this, M[K]>,
|
|
157
|
+
options?: boolean | AddEventListenerOptions
|
|
158
|
+
) => void;
|
|
159
|
+
|
|
160
|
+
declare dispatchEvent: <K extends string, M extends DatagridEventMap = DatagridEventMap>(
|
|
161
|
+
event: K extends keyof M ? M[K] : CustomEvent
|
|
162
|
+
) => boolean;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export class Datagrids {
|
|
166
|
+
private datagrids: Datagrid[] = [];
|
|
167
|
+
|
|
168
|
+
readonly options: DatagridsOptions;
|
|
169
|
+
|
|
170
|
+
readonly root: HTMLElement;
|
|
171
|
+
|
|
172
|
+
constructor(readonly ajax: Ajax, options: Partial<DatagridsOptions> = {}) {
|
|
173
|
+
this.options = {
|
|
174
|
+
selector: "div[data-datagrid-name]",
|
|
175
|
+
datagrid: {},
|
|
176
|
+
root: document.body,
|
|
177
|
+
...options,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const root = typeof this.options.root === "string"
|
|
181
|
+
? document.querySelector(this.options.root)
|
|
182
|
+
: this.options.root;
|
|
183
|
+
|
|
184
|
+
if (!root || !(root instanceof HTMLElement)) {
|
|
185
|
+
throw new Error("Root element not found or is not an HTMLElement");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.root = root;
|
|
189
|
+
|
|
190
|
+
this.init();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
init() {
|
|
194
|
+
this.ajax.onInit();
|
|
195
|
+
(this.options.datagrid?.plugins ?? []).forEach((plugin) => plugin.onInit?.(this));
|
|
196
|
+
|
|
197
|
+
this.initDatagrids();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
initDatagrids() {
|
|
201
|
+
this.datagrids = Array.from(this.root.querySelectorAll<HTMLElement>(this.options.selector)).map(
|
|
202
|
+
datagrid => new Datagrid(datagrid, this.ajax, this.options.datagrid)
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export const createDatagrids = (ajax: Ajax, _options: Partial<DatagridsOptions> = {}) => {
|
|
208
|
+
return new Datagrids(ajax, _options);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export const createFullDatagrids = (ajax: Ajax, _options: Partial<DatagridsOptions> = {}) => {
|
|
212
|
+
return createDatagrids(ajax, {
|
|
213
|
+
datagrid: {
|
|
214
|
+
plugins: [
|
|
215
|
+
new AutosubmitPlugin(),
|
|
216
|
+
new CheckboxPlugin(),
|
|
217
|
+
new ConfirmPlugin(),
|
|
218
|
+
new InlinePlugin(),
|
|
219
|
+
new NetteFormsPlugin(),
|
|
220
|
+
new HappyPlugin(new Happy()),
|
|
221
|
+
new SortablePlugin(new SortableJS()),
|
|
222
|
+
new DatepickerPlugin(new VanillaDatepicker()),
|
|
223
|
+
new SelectpickerPlugin(new TomSelect(Select))
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
..._options,
|
|
227
|
+
})
|
|
228
|
+
};
|
package/assets/index.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slightly cleaned up & typed version of happy-inputs by paveljanda.
|
|
3
|
+
*/
|
|
4
|
+
export class Happy {
|
|
5
|
+
private colors: string[] = ["primary", "success", "info", "warning", "danger", "white", "gray"];
|
|
6
|
+
|
|
7
|
+
private templates = {
|
|
8
|
+
radio: '<div class="happy-radio"><b></b></div>',
|
|
9
|
+
checkbox:
|
|
10
|
+
'<div class="happy-checkbox"><svg viewBox="0 0 30 30"><rect class="mark-storke" x="15" y="3" rx="1" ry="1" width="10" height="4"/><rect class="mark-storke" x="-7" y="21" rx="1" ry="1" width="19" height="4"/></svg></div>',
|
|
11
|
+
text: "",
|
|
12
|
+
textarea: "",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
init() {
|
|
16
|
+
this.removeBySelector(".happy-radio");
|
|
17
|
+
this.removeBySelector(".happy-checkbox");
|
|
18
|
+
|
|
19
|
+
this.initRadio();
|
|
20
|
+
this.initCheckbox();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @deprecated
|
|
25
|
+
*/
|
|
26
|
+
reset() {
|
|
27
|
+
this.init();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
addColorToInput(input: HTMLElement, happyInput: HTMLElement, classString: string) {
|
|
31
|
+
if (input.classList.contains(classString)) {
|
|
32
|
+
happyInput.classList.add(classString);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
classString = `${classString}-border`;
|
|
36
|
+
|
|
37
|
+
if (input.classList.contains(classString)) {
|
|
38
|
+
happyInput.classList.add(classString);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// i... you know what, no, let "thinkess" be "thinkess"
|
|
43
|
+
addThinkessToInput(input: HTMLElement, happyInput: HTMLElement) {
|
|
44
|
+
if (input.classList.contains("thin")) {
|
|
45
|
+
happyInput.classList.add("thin");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
setNames(input: HTMLElement, happyInput: HTMLElement) {
|
|
50
|
+
happyInput.setAttribute("data-name", input.getAttribute("name") ?? "");
|
|
51
|
+
|
|
52
|
+
var value = input.getAttribute("value");
|
|
53
|
+
|
|
54
|
+
if (value !== "undefined" && value !== null) {
|
|
55
|
+
happyInput.setAttribute("data-value", input.getAttribute("value") ?? "");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
removeBySelector(selector: string) {
|
|
60
|
+
document.querySelectorAll(selector).forEach(el => el.parentNode?.removeChild(el));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
initRadio() {
|
|
64
|
+
document.querySelectorAll<HTMLInputElement>("input[type=radio].happy").forEach(input => {
|
|
65
|
+
/**
|
|
66
|
+
* Paste happy component into html
|
|
67
|
+
*/
|
|
68
|
+
input.insertAdjacentHTML("afterend", this.templates.radio);
|
|
69
|
+
|
|
70
|
+
const happyInput = input.nextElementSibling;
|
|
71
|
+
|
|
72
|
+
if (happyInput instanceof HTMLElement) {
|
|
73
|
+
/**
|
|
74
|
+
* Add optional colors
|
|
75
|
+
*/
|
|
76
|
+
this.colors.forEach(color => {
|
|
77
|
+
this.addColorToInput(input, happyInput, color);
|
|
78
|
+
this.setNames(input, happyInput);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
this.addThinkessToInput(input, happyInput);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Init state
|
|
86
|
+
*/
|
|
87
|
+
this.checkRadioState(input);
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Set aciton functionality for native change
|
|
91
|
+
*/
|
|
92
|
+
document.addEventListener("change", this.radioOnChange.bind(this));
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
initCheckbox() {
|
|
97
|
+
document.querySelectorAll<HTMLInputElement>("input[type=checkbox].happy").forEach(input => {
|
|
98
|
+
/**
|
|
99
|
+
* Paste happy component into html
|
|
100
|
+
*/
|
|
101
|
+
input.insertAdjacentHTML("afterend", this.templates.checkbox);
|
|
102
|
+
|
|
103
|
+
const happyInput = input.nextElementSibling;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Add optional colors
|
|
107
|
+
*/
|
|
108
|
+
if (happyInput instanceof HTMLElement) {
|
|
109
|
+
this.colors.forEach(color => {
|
|
110
|
+
this.addColorToInput(input, happyInput, color);
|
|
111
|
+
this.setNames(input, happyInput);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
this.addThinkessToInput(input, happyInput);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Init state
|
|
119
|
+
*/
|
|
120
|
+
this.checkCheckboxState(input);
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Set action functionality for click || native change
|
|
124
|
+
*/
|
|
125
|
+
document.addEventListener("click", this.checkCheckboxStateOnClick.bind(this));
|
|
126
|
+
document.addEventListener("change", this.checkCheckboxStateOnChange.bind(this));
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
checkCheckboxStateOnClick(event: Event) {
|
|
131
|
+
const target = event.target;
|
|
132
|
+
|
|
133
|
+
// When target is SVGSVGElement (<svg>), return parentNode,
|
|
134
|
+
// When target is a SVGGraphicsElement (<rect>,...), find <svg> and return it's parent node
|
|
135
|
+
// otherwise return target itself.
|
|
136
|
+
const happyInput =
|
|
137
|
+
target instanceof SVGSVGElement
|
|
138
|
+
? target.parentNode
|
|
139
|
+
: target instanceof SVGGraphicsElement
|
|
140
|
+
? target.closest("svg")?.parentNode
|
|
141
|
+
: target;
|
|
142
|
+
|
|
143
|
+
if (!(happyInput instanceof HTMLElement) || !happyInput.classList.contains("happy-checkbox")) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
event.preventDefault();
|
|
148
|
+
|
|
149
|
+
const name = happyInput.getAttribute("data-name");
|
|
150
|
+
const value = happyInput.getAttribute("data-value");
|
|
151
|
+
|
|
152
|
+
const input = document.querySelector(
|
|
153
|
+
`.happy-checkbox[data-name="${name}"]` + (!!value ? `[value="${value}"]` : "")
|
|
154
|
+
);
|
|
155
|
+
if (!(input instanceof HTMLInputElement)) return;
|
|
156
|
+
|
|
157
|
+
const checked = happyInput.classList.contains("active");
|
|
158
|
+
|
|
159
|
+
input.checked = !checked;
|
|
160
|
+
checked ? happyInput.classList.remove("active") : happyInput.classList.add("active");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
checkCheckboxStateOnChange({target}: Event) {
|
|
164
|
+
if (!(target instanceof HTMLInputElement)) return;
|
|
165
|
+
|
|
166
|
+
if (target.classList.contains("happy")) {
|
|
167
|
+
this.checkCheckboxState(target);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
checkRadioState(input: HTMLInputElement) {
|
|
172
|
+
if (!input.checked || !input.hasAttribute("name")) return;
|
|
173
|
+
|
|
174
|
+
const name = input.getAttribute("name");
|
|
175
|
+
const value = input.getAttribute("value");
|
|
176
|
+
|
|
177
|
+
const element = document.querySelector(
|
|
178
|
+
`.happy-checkbox[data-name="${name}"]` + (!!value ? `[data-value="${value}"]` : "")
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
if (element) {
|
|
182
|
+
element.classList.add("active");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
checkCheckboxState(input: HTMLInputElement) {
|
|
187
|
+
const name = input.getAttribute("name");
|
|
188
|
+
if (!name) return;
|
|
189
|
+
|
|
190
|
+
const value = input.getAttribute("value");
|
|
191
|
+
const element = document.querySelector(
|
|
192
|
+
`.happy-checkbox[data-name="${name}"]` + (!!value ? `[data-value="${value}"]` : "")
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
if (!element) return;
|
|
196
|
+
|
|
197
|
+
input.checked ? element.classList.add("active") : element.classList.remove("active");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
radioOnChange({target}: Event) {
|
|
201
|
+
// Check whether target is <input>, is a happy input (.happy) & has the name attribute
|
|
202
|
+
if (
|
|
203
|
+
!(target instanceof HTMLInputElement) ||
|
|
204
|
+
!target.classList.contains("happy") ||
|
|
205
|
+
!target.hasAttribute("name")
|
|
206
|
+
)
|
|
207
|
+
return;
|
|
208
|
+
|
|
209
|
+
const name = target.getAttribute("name")!;
|
|
210
|
+
|
|
211
|
+
document
|
|
212
|
+
.querySelectorAll(`.happy-radio[data-name="${name}"]`)
|
|
213
|
+
.forEach(happyRadio => happyRadio.classList.remove("active"));
|
|
214
|
+
|
|
215
|
+
this.checkRadioState(target);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
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<HTMLElement>("[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<HTMLElement>(".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
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
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<TomSelectType>,
|
|
9
|
+
private opts: RecursivePartial<TomSettings> | ((input: HTMLElement | TomInput) => RecursivePartial<TomSettings>) = {}
|
|
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
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
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<HTMLSelectElement>(`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<HTMLSelectElement | HTMLInputElement>(`[${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
|
+
|