kmaterialize 2.3.3-1.0.21 → 2.3.3-1.0.23
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/components/air-datepicker/air-datepicker.js +152 -0
- package/components/alert/alert.js +54 -0
- package/components/button/buttons.js +220 -0
- package/components/card/cards.js +149 -0
- package/components/carousel/carousel.js +574 -0
- package/components/carousel/slider.js +340 -0
- package/components/chip/chips.js +341 -0
- package/components/collapsible/collapsible.js +156 -0
- package/components/color-input/color-input.js +166 -0
- package/components/datepicker/datepicker.js +1035 -0
- package/components/dialog/materialbox.js +377 -0
- package/components/dialog/modal.js +72 -0
- package/components/dropdown/dropdown.js +505 -0
- package/components/file-input/file-input.js +169 -0
- package/components/kanban/_kanban.scss +69 -0
- package/components/kanban/kanban.js +168 -0
- package/components/kanban/kanban.ts +57 -8
- package/components/loading/_loading.scss +68 -0
- package/components/loading/loading.ts +104 -0
- package/components/loading/loadingSpec.js +102 -0
- package/components/navigation-drawer/sidenav.js +454 -0
- package/components/number-input/number-input.js +68 -0
- package/components/password-input/password-input.js +49 -0
- package/components/popup/_popup.scss +106 -0
- package/components/popup/popup.ts +113 -0
- package/components/popup/popupSpec.js +140 -0
- package/components/search/autocomplete.js +470 -0
- package/components/slider/range.js +169 -0
- package/components/snackbar/toasts.js +244 -0
- package/components/tabs/tabs.js +289 -0
- package/components/textfield/forms.js +172 -0
- package/components/textfield/select.js +385 -0
- package/components/timepicker/timepicker.js +698 -0
- package/components/tom-select/tom-select-field.js +130 -0
- package/components/toolbar/toolbar.js +134 -0
- package/components/tooltip/tooltip.js +270 -0
- package/dist/css/materialize.colors.min.css +1 -1
- package/dist/css/materialize.css +322 -1
- package/dist/css/materialize.min.css +2 -2
- package/dist/css/materialize.min.css.map +1 -1
- package/dist/js/materialize.cjs.js +258 -42
- package/dist/js/materialize.d.ts +50 -3
- package/dist/js/materialize.js +258 -42
- package/dist/js/materialize.min.js +2 -2
- package/dist/js/materialize.mjs +257 -43
- package/package.json +6 -1
- package/sass/materialize.scss +2 -0
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
import { Utils } from '../../src/utils';
|
|
2
|
+
import { Component } from '../../src/component';
|
|
3
|
+
const _defaults = {
|
|
4
|
+
alignment: 'left',
|
|
5
|
+
autoFocus: true,
|
|
6
|
+
constrainWidth: true,
|
|
7
|
+
container: null,
|
|
8
|
+
coverTrigger: true,
|
|
9
|
+
closeOnClick: true,
|
|
10
|
+
hover: false,
|
|
11
|
+
inDuration: 150,
|
|
12
|
+
outDuration: 250,
|
|
13
|
+
onOpenStart: null,
|
|
14
|
+
onOpenEnd: null,
|
|
15
|
+
onCloseStart: null,
|
|
16
|
+
onCloseEnd: null,
|
|
17
|
+
onItemClick: null
|
|
18
|
+
};
|
|
19
|
+
export class Dropdown extends Component {
|
|
20
|
+
static _dropdowns = [];
|
|
21
|
+
/** ID of the dropdown element. */
|
|
22
|
+
id;
|
|
23
|
+
/** The DOM element of the dropdown. */
|
|
24
|
+
dropdownEl;
|
|
25
|
+
/** If the dropdown is open. */
|
|
26
|
+
isOpen;
|
|
27
|
+
/** If the dropdown content is scrollable. */
|
|
28
|
+
isScrollable;
|
|
29
|
+
isTouchMoving;
|
|
30
|
+
/** The index of the item focused. */
|
|
31
|
+
focusedIndex;
|
|
32
|
+
filterQuery;
|
|
33
|
+
filterTimeout;
|
|
34
|
+
constructor(el, options) {
|
|
35
|
+
super(el, options, Dropdown);
|
|
36
|
+
this.el['M_Dropdown'] = this;
|
|
37
|
+
Dropdown._dropdowns.push(this);
|
|
38
|
+
this.id = Utils.getIdFromTrigger(el);
|
|
39
|
+
this.dropdownEl = document.getElementById(this.id);
|
|
40
|
+
this.options = {
|
|
41
|
+
...Dropdown.defaults,
|
|
42
|
+
...options
|
|
43
|
+
};
|
|
44
|
+
this.isOpen = false;
|
|
45
|
+
this.isScrollable = false;
|
|
46
|
+
this.isTouchMoving = false;
|
|
47
|
+
this.focusedIndex = -1;
|
|
48
|
+
this.filterQuery = [];
|
|
49
|
+
this.el.ariaExpanded = 'false';
|
|
50
|
+
// Move dropdown-content after dropdown-trigger
|
|
51
|
+
this._moveDropdownToElement();
|
|
52
|
+
this._makeDropdownFocusable();
|
|
53
|
+
this._setupEventHandlers();
|
|
54
|
+
}
|
|
55
|
+
static get defaults() {
|
|
56
|
+
return _defaults;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Initializes instances of Dropdown.
|
|
60
|
+
* @param els HTML elements.
|
|
61
|
+
* @param options Component options.
|
|
62
|
+
*/
|
|
63
|
+
static init(els, options = {}) {
|
|
64
|
+
return super.init(els, options, Dropdown);
|
|
65
|
+
}
|
|
66
|
+
static getInstance(el) {
|
|
67
|
+
return el['M_Dropdown'];
|
|
68
|
+
}
|
|
69
|
+
destroy() {
|
|
70
|
+
this._resetDropdownStyles();
|
|
71
|
+
this._removeEventHandlers();
|
|
72
|
+
Dropdown._dropdowns.splice(Dropdown._dropdowns.indexOf(this), 1);
|
|
73
|
+
this.el['M_Dropdown'] = undefined;
|
|
74
|
+
}
|
|
75
|
+
_setupEventHandlers() {
|
|
76
|
+
// Trigger keydown handler
|
|
77
|
+
this.el.addEventListener('keydown', this._handleTriggerKeydown);
|
|
78
|
+
// Item click handler
|
|
79
|
+
this.dropdownEl?.addEventListener('click', this._handleDropdownClick);
|
|
80
|
+
// Hover event handlers
|
|
81
|
+
if (this.options.hover) {
|
|
82
|
+
this.el.addEventListener('mouseenter', this._handleMouseEnter);
|
|
83
|
+
this.el.addEventListener('mouseleave', this._handleMouseLeave);
|
|
84
|
+
this.dropdownEl.addEventListener('mouseleave', this._handleMouseLeave);
|
|
85
|
+
// Click event handlers
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
this.el.addEventListener('click', this._handleClick);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
_removeEventHandlers() {
|
|
92
|
+
this.el.removeEventListener('keydown', this._handleTriggerKeydown);
|
|
93
|
+
this.dropdownEl.removeEventListener('click', this._handleDropdownClick);
|
|
94
|
+
if (this.options.hover) {
|
|
95
|
+
this.el.removeEventListener('mouseenter', this._handleMouseEnter);
|
|
96
|
+
this.el.removeEventListener('mouseleave', this._handleMouseLeave);
|
|
97
|
+
this.dropdownEl.removeEventListener('mouseleave', this._handleMouseLeave);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
this.el.removeEventListener('click', this._handleClick);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
_setupTemporaryEventHandlers() {
|
|
104
|
+
document.body.addEventListener('click', this._handleDocumentClick);
|
|
105
|
+
document.body.addEventListener('touchmove', this._handleDocumentTouchmove);
|
|
106
|
+
this.dropdownEl.addEventListener('keydown', this._handleDropdownKeydown);
|
|
107
|
+
window.addEventListener('resize', this._handleWindowResize);
|
|
108
|
+
}
|
|
109
|
+
_removeTemporaryEventHandlers() {
|
|
110
|
+
document.body.removeEventListener('click', this._handleDocumentClick);
|
|
111
|
+
document.body.removeEventListener('touchmove', this._handleDocumentTouchmove);
|
|
112
|
+
this.dropdownEl.removeEventListener('keydown', this._handleDropdownKeydown);
|
|
113
|
+
window.removeEventListener('resize', this._handleWindowResize);
|
|
114
|
+
}
|
|
115
|
+
_handleClick = (e) => {
|
|
116
|
+
e.preventDefault();
|
|
117
|
+
//this._moveDropdown((<HTMLElement>e.target).closest('li'));
|
|
118
|
+
if (this.isOpen) {
|
|
119
|
+
this.close();
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
this.open();
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
_handleMouseEnter = () => {
|
|
126
|
+
//this._moveDropdown((<HTMLElement>e.target).closest('li'));
|
|
127
|
+
this.open();
|
|
128
|
+
};
|
|
129
|
+
_handleMouseLeave = (e) => {
|
|
130
|
+
const toEl = e.relatedTarget;
|
|
131
|
+
const leaveToDropdownContent = !!toEl.closest('.dropdown-content');
|
|
132
|
+
let leaveToActiveDropdownTrigger = false;
|
|
133
|
+
const closestTrigger = toEl.closest('.dropdown-trigger');
|
|
134
|
+
if (closestTrigger && !!closestTrigger['M_Dropdown'] && closestTrigger['M_Dropdown'].isOpen) {
|
|
135
|
+
leaveToActiveDropdownTrigger = true;
|
|
136
|
+
}
|
|
137
|
+
// Close hover dropdown if mouse did not leave to either active dropdown-trigger or dropdown-content
|
|
138
|
+
if (!leaveToActiveDropdownTrigger && !leaveToDropdownContent) {
|
|
139
|
+
this.close();
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
_handleDocumentClick = (e) => {
|
|
143
|
+
const target = e.target;
|
|
144
|
+
if (this.options.closeOnClick && target.closest('.dropdown-content') && !this.isTouchMoving) {
|
|
145
|
+
// isTouchMoving to check if scrolling on mobile.
|
|
146
|
+
this.close();
|
|
147
|
+
}
|
|
148
|
+
else if (!target.closest('.dropdown-content') && !target.closest('.dropdown-trigger')) {
|
|
149
|
+
// Do this one frame later so that if the element clicked also triggers _handleClick
|
|
150
|
+
// For example, if a label for a select was clicked, that we don't close/open the dropdown
|
|
151
|
+
setTimeout(() => {
|
|
152
|
+
if (this.isOpen) {
|
|
153
|
+
this.close();
|
|
154
|
+
}
|
|
155
|
+
}, 0);
|
|
156
|
+
}
|
|
157
|
+
this.isTouchMoving = false;
|
|
158
|
+
};
|
|
159
|
+
_handleTriggerKeydown = (e) => {
|
|
160
|
+
// ARROW DOWN OR ENTER WHEN SELECT IS CLOSED - open Dropdown
|
|
161
|
+
const arrowDownOrEnter = Utils.keys.ARROW_DOWN.includes(e.key) || Utils.keys.ENTER.includes(e.key);
|
|
162
|
+
if (arrowDownOrEnter && !this.isOpen) {
|
|
163
|
+
e.preventDefault();
|
|
164
|
+
this.open();
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
_handleDocumentTouchmove = (e) => {
|
|
168
|
+
const target = e.target;
|
|
169
|
+
if (target.closest('.dropdown-content')) {
|
|
170
|
+
this.isTouchMoving = true;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
_handleDropdownClick = (e) => {
|
|
174
|
+
// onItemClick callback
|
|
175
|
+
if (typeof this.options.onItemClick === 'function') {
|
|
176
|
+
const itemEl = e.target.closest('li');
|
|
177
|
+
this.options.onItemClick.call(this, itemEl);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
_handleDropdownKeydown = (e) => {
|
|
181
|
+
const arrowUpOrDown = Utils.keys.ARROW_DOWN.includes(e.key) || Utils.keys.ARROW_UP.includes(e.key);
|
|
182
|
+
if (Utils.keys.TAB.includes(e.key)) {
|
|
183
|
+
e.preventDefault();
|
|
184
|
+
this.close();
|
|
185
|
+
}
|
|
186
|
+
// Navigate down dropdown list
|
|
187
|
+
else if (arrowUpOrDown && this.isOpen) {
|
|
188
|
+
e.preventDefault();
|
|
189
|
+
const direction = Utils.keys.ARROW_DOWN.includes(e.key) ? 1 : -1;
|
|
190
|
+
let newFocusedIndex = this.focusedIndex;
|
|
191
|
+
let hasFoundNewIndex = false;
|
|
192
|
+
do {
|
|
193
|
+
newFocusedIndex = newFocusedIndex + direction;
|
|
194
|
+
if (!!this.dropdownEl.children[newFocusedIndex] &&
|
|
195
|
+
this.dropdownEl.children[newFocusedIndex].tabIndex !== -1) {
|
|
196
|
+
hasFoundNewIndex = true;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
} while (newFocusedIndex < this.dropdownEl.children.length && newFocusedIndex >= 0);
|
|
200
|
+
if (hasFoundNewIndex) {
|
|
201
|
+
// Remove active class from old element
|
|
202
|
+
if (this.focusedIndex >= 0)
|
|
203
|
+
this.dropdownEl.children[this.focusedIndex].classList.remove('active');
|
|
204
|
+
this.focusedIndex = newFocusedIndex;
|
|
205
|
+
this._focusFocusedItem();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// ENTER selects choice on focused item
|
|
209
|
+
else if (Utils.keys.ENTER.includes(e.key) && this.isOpen) {
|
|
210
|
+
// Search for <a> and <button>
|
|
211
|
+
const focusedElement = this.dropdownEl.children[this.focusedIndex];
|
|
212
|
+
const activatableElement = focusedElement?.querySelector('a, button');
|
|
213
|
+
// Click a or button tag if exists, otherwise click li tag
|
|
214
|
+
if (!!activatableElement) {
|
|
215
|
+
activatableElement.click();
|
|
216
|
+
}
|
|
217
|
+
else if (!!focusedElement) {
|
|
218
|
+
if (focusedElement instanceof HTMLElement) {
|
|
219
|
+
focusedElement.click();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Close dropdown on ESC
|
|
224
|
+
else if (Utils.keys.ESC.includes(e.key) && this.isOpen) {
|
|
225
|
+
e.preventDefault();
|
|
226
|
+
this.close();
|
|
227
|
+
}
|
|
228
|
+
// CASE WHEN USER TYPE LTTERS
|
|
229
|
+
const keyText = e.key.toLowerCase();
|
|
230
|
+
const isLetter = /[a-zA-Z0-9-_]/.test(keyText);
|
|
231
|
+
const specialKeys = [
|
|
232
|
+
...Utils.keys.ARROW_DOWN,
|
|
233
|
+
...Utils.keys.ARROW_UP,
|
|
234
|
+
...Utils.keys.ENTER,
|
|
235
|
+
...Utils.keys.ESC,
|
|
236
|
+
...Utils.keys.TAB
|
|
237
|
+
];
|
|
238
|
+
if (isLetter && !specialKeys.includes(e.key)) {
|
|
239
|
+
this.filterQuery.push(keyText);
|
|
240
|
+
const string = this.filterQuery.join('');
|
|
241
|
+
const newOptionEl = Array.from(this.dropdownEl.querySelectorAll('li')).find((el) => el.innerText.toLowerCase().indexOf(string) === 0);
|
|
242
|
+
if (newOptionEl) {
|
|
243
|
+
this.focusedIndex = [...newOptionEl.parentNode.children].indexOf(newOptionEl);
|
|
244
|
+
this._focusFocusedItem();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
this.filterTimeout = setTimeout(this._resetFilterQuery, 1000);
|
|
248
|
+
};
|
|
249
|
+
_handleWindowResize = () => {
|
|
250
|
+
// Only re-place the dropdown if it's still visible
|
|
251
|
+
// Accounts for elements hiding via media queries
|
|
252
|
+
if (this.el.offsetParent) {
|
|
253
|
+
this.recalculateDimensions();
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
_resetFilterQuery = () => {
|
|
257
|
+
this.filterQuery = [];
|
|
258
|
+
};
|
|
259
|
+
_resetDropdownStyles() {
|
|
260
|
+
this.dropdownEl.style.display = '';
|
|
261
|
+
this._resetDropdownPositioningStyles();
|
|
262
|
+
this.dropdownEl.style.transform = '';
|
|
263
|
+
this.dropdownEl.style.opacity = '';
|
|
264
|
+
}
|
|
265
|
+
_resetDropdownPositioningStyles() {
|
|
266
|
+
this.dropdownEl.style.width = '';
|
|
267
|
+
this.dropdownEl.style.height = '';
|
|
268
|
+
this.dropdownEl.style.left = '';
|
|
269
|
+
this.dropdownEl.style.top = '';
|
|
270
|
+
this.dropdownEl.style.transformOrigin = '';
|
|
271
|
+
}
|
|
272
|
+
_moveDropdownToElement(containerEl = null) {
|
|
273
|
+
if (this.options.container) {
|
|
274
|
+
this.options.container.append(this.dropdownEl);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (containerEl) {
|
|
278
|
+
if (!containerEl.contains(this.dropdownEl))
|
|
279
|
+
containerEl.append(this.dropdownEl);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
this.el.after(this.dropdownEl);
|
|
283
|
+
}
|
|
284
|
+
_makeDropdownFocusable() {
|
|
285
|
+
if (!this.dropdownEl)
|
|
286
|
+
return;
|
|
287
|
+
this.dropdownEl.popover = '';
|
|
288
|
+
// Needed for arrow key navigation
|
|
289
|
+
this.dropdownEl.tabIndex = 0;
|
|
290
|
+
// Only set tabindex if it hasn't been set by user
|
|
291
|
+
Array.from(this.dropdownEl.children).forEach((el) => {
|
|
292
|
+
if (!el.getAttribute('tabindex'))
|
|
293
|
+
el.setAttribute('tabindex', '0');
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
_focusFocusedItem() {
|
|
297
|
+
if (this.focusedIndex >= 0 &&
|
|
298
|
+
this.focusedIndex < this.dropdownEl.children.length &&
|
|
299
|
+
this.options.autoFocus) {
|
|
300
|
+
this.dropdownEl.children[this.focusedIndex].focus({
|
|
301
|
+
preventScroll: true
|
|
302
|
+
});
|
|
303
|
+
this.dropdownEl.children[this.focusedIndex].scrollIntoView({
|
|
304
|
+
behavior: 'smooth',
|
|
305
|
+
block: 'nearest',
|
|
306
|
+
inline: 'nearest'
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
_getDropdownPosition(closestOverflowParent) {
|
|
311
|
+
// const offsetParentBRect = this.el.offsetParent.getBoundingClientRect();
|
|
312
|
+
const triggerBRect = this.el.getBoundingClientRect();
|
|
313
|
+
const dropdownBRect = this.dropdownEl.getBoundingClientRect();
|
|
314
|
+
let idealHeight = dropdownBRect.height;
|
|
315
|
+
let idealWidth = dropdownBRect.width;
|
|
316
|
+
let idealXPos = triggerBRect.left - dropdownBRect.left;
|
|
317
|
+
let idealYPos = triggerBRect.top - dropdownBRect.top;
|
|
318
|
+
const dropdownBounds = {
|
|
319
|
+
left: idealXPos,
|
|
320
|
+
top: idealYPos,
|
|
321
|
+
height: idealHeight,
|
|
322
|
+
width: idealWidth
|
|
323
|
+
};
|
|
324
|
+
const alignments = Utils.checkPossibleAlignments(this.el, closestOverflowParent, dropdownBounds, this.options.coverTrigger ? 0 : triggerBRect.height);
|
|
325
|
+
let verticalAlignment = 'top';
|
|
326
|
+
let horizontalAlignment = this.options.alignment;
|
|
327
|
+
idealYPos += this.options.coverTrigger ? 0 : triggerBRect.height;
|
|
328
|
+
// Reset isScrollable
|
|
329
|
+
this.isScrollable = false;
|
|
330
|
+
if (!alignments.top) {
|
|
331
|
+
if (alignments.bottom) {
|
|
332
|
+
verticalAlignment = 'bottom';
|
|
333
|
+
if (!this.options.coverTrigger) {
|
|
334
|
+
idealYPos -= triggerBRect.height;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
this.isScrollable = true;
|
|
339
|
+
// Determine which side has most space and cutoff at correct height
|
|
340
|
+
idealHeight -= 20; // Add padding when cutoff
|
|
341
|
+
if (alignments.spaceOnTop > alignments.spaceOnBottom) {
|
|
342
|
+
verticalAlignment = 'bottom';
|
|
343
|
+
idealHeight += alignments.spaceOnTop;
|
|
344
|
+
idealYPos -= this.options.coverTrigger
|
|
345
|
+
? alignments.spaceOnTop - 20
|
|
346
|
+
: alignments.spaceOnTop - 20 + triggerBRect.height;
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
idealHeight += alignments.spaceOnBottom;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// If preferred horizontal alignment is possible
|
|
354
|
+
if (!alignments[horizontalAlignment]) {
|
|
355
|
+
const oppositeAlignment = horizontalAlignment === 'left' ? 'right' : 'left';
|
|
356
|
+
if (alignments[oppositeAlignment]) {
|
|
357
|
+
horizontalAlignment = oppositeAlignment;
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
// Determine which side has most space and cutoff at correct height
|
|
361
|
+
if (alignments.spaceOnLeft > alignments.spaceOnRight) {
|
|
362
|
+
horizontalAlignment = 'right';
|
|
363
|
+
idealWidth += alignments.spaceOnLeft;
|
|
364
|
+
idealXPos -= alignments.spaceOnLeft;
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
horizontalAlignment = 'left';
|
|
368
|
+
idealWidth += alignments.spaceOnRight;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (verticalAlignment === 'bottom') {
|
|
373
|
+
idealYPos =
|
|
374
|
+
idealYPos - dropdownBRect.height + (this.options.coverTrigger ? triggerBRect.height : 0);
|
|
375
|
+
}
|
|
376
|
+
if (horizontalAlignment === 'right') {
|
|
377
|
+
idealXPos = idealXPos - dropdownBRect.width + triggerBRect.width;
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
x: idealXPos,
|
|
381
|
+
y: idealYPos,
|
|
382
|
+
verticalAlignment: verticalAlignment,
|
|
383
|
+
horizontalAlignment: horizontalAlignment,
|
|
384
|
+
height: idealHeight,
|
|
385
|
+
width: idealWidth
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
_animateIn() {
|
|
389
|
+
const duration = this.options.inDuration;
|
|
390
|
+
this.dropdownEl.style.transition = 'none';
|
|
391
|
+
// from
|
|
392
|
+
this.dropdownEl.style.opacity = '0';
|
|
393
|
+
this.dropdownEl.style.transform = 'scale(0.3, 0.3)';
|
|
394
|
+
setTimeout(() => {
|
|
395
|
+
// easeOutQuad (opacity) & easeOutQuint
|
|
396
|
+
this.dropdownEl.style.transition = `opacity ${duration}ms ease, transform ${duration}ms ease`;
|
|
397
|
+
// to
|
|
398
|
+
this.dropdownEl.style.opacity = '1';
|
|
399
|
+
this.dropdownEl.style.transform = 'scale(1, 1)';
|
|
400
|
+
}, 1);
|
|
401
|
+
setTimeout(() => {
|
|
402
|
+
if (this.options.autoFocus)
|
|
403
|
+
this.dropdownEl.focus();
|
|
404
|
+
if (typeof this.options.onOpenEnd === 'function')
|
|
405
|
+
this.options.onOpenEnd.call(this, this.el);
|
|
406
|
+
}, duration);
|
|
407
|
+
}
|
|
408
|
+
_animateOut() {
|
|
409
|
+
const duration = this.options.outDuration;
|
|
410
|
+
// easeOutQuad (opacity) & easeOutQuint
|
|
411
|
+
this.dropdownEl.style.transition = `opacity ${duration}ms ease, transform ${duration}ms ease`;
|
|
412
|
+
// to
|
|
413
|
+
this.dropdownEl.style.opacity = '0';
|
|
414
|
+
this.dropdownEl.style.transform = 'scale(0.3, 0.3)';
|
|
415
|
+
setTimeout(() => {
|
|
416
|
+
this._resetDropdownStyles();
|
|
417
|
+
if (typeof this.options.onCloseEnd === 'function')
|
|
418
|
+
this.options.onCloseEnd.call(this, this.el);
|
|
419
|
+
}, duration);
|
|
420
|
+
}
|
|
421
|
+
_getClosestAncestor(el, condition) {
|
|
422
|
+
let ancestor = el.parentNode;
|
|
423
|
+
while (ancestor !== null && ancestor !== document) {
|
|
424
|
+
if (condition(ancestor)) {
|
|
425
|
+
return ancestor;
|
|
426
|
+
}
|
|
427
|
+
ancestor = ancestor.parentElement;
|
|
428
|
+
}
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
_placeDropdown() {
|
|
432
|
+
// Container here will be closest ancestor with overflow: hidden
|
|
433
|
+
let closestOverflowParent = this._getClosestAncestor(this.dropdownEl, (ancestor) => {
|
|
434
|
+
return (!['HTML', 'BODY'].includes(ancestor.tagName) &&
|
|
435
|
+
getComputedStyle(ancestor).overflow !== 'visible');
|
|
436
|
+
});
|
|
437
|
+
// Fallback
|
|
438
|
+
if (!closestOverflowParent) {
|
|
439
|
+
closestOverflowParent = ((!!this.dropdownEl.offsetParent ? this.dropdownEl.offsetParent : this.dropdownEl.parentNode));
|
|
440
|
+
}
|
|
441
|
+
if (getComputedStyle(closestOverflowParent).position === 'static')
|
|
442
|
+
closestOverflowParent.style.position = 'relative';
|
|
443
|
+
//this._moveDropdown(closestOverflowParent);
|
|
444
|
+
// Set width before calculating positionInfo
|
|
445
|
+
const idealWidth = this.options.constrainWidth
|
|
446
|
+
? this.el.getBoundingClientRect().width
|
|
447
|
+
: this.dropdownEl.getBoundingClientRect().width;
|
|
448
|
+
this.dropdownEl.style.width = idealWidth + 'px';
|
|
449
|
+
const positionInfo = this._getDropdownPosition(closestOverflowParent);
|
|
450
|
+
this.dropdownEl.style.left = positionInfo.x + 'px';
|
|
451
|
+
this.dropdownEl.style.top = positionInfo.y + 'px';
|
|
452
|
+
this.dropdownEl.style.height = positionInfo.height + 'px';
|
|
453
|
+
this.dropdownEl.style.width = positionInfo.width + 'px';
|
|
454
|
+
this.dropdownEl.style.transformOrigin = `${positionInfo.horizontalAlignment === 'left' ? '0' : '100%'} ${positionInfo.verticalAlignment === 'top' ? '0' : '100%'}`;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Open dropdown.
|
|
458
|
+
*/
|
|
459
|
+
open = () => {
|
|
460
|
+
if (this.isOpen)
|
|
461
|
+
return;
|
|
462
|
+
this.isOpen = true;
|
|
463
|
+
// onOpenStart callback
|
|
464
|
+
if (typeof this.options.onOpenStart === 'function') {
|
|
465
|
+
this.options.onOpenStart.call(this, this.el);
|
|
466
|
+
}
|
|
467
|
+
// Reset styles
|
|
468
|
+
this._resetDropdownStyles();
|
|
469
|
+
this.dropdownEl.style.display = 'block';
|
|
470
|
+
this._placeDropdown();
|
|
471
|
+
this._animateIn();
|
|
472
|
+
// Do this one frame later so that we don't bind an event handler that's immediately
|
|
473
|
+
// called when the event bubbles up to the document and closes the dropdown
|
|
474
|
+
setTimeout(() => this._setupTemporaryEventHandlers(), 0);
|
|
475
|
+
this.el.ariaExpanded = 'true';
|
|
476
|
+
};
|
|
477
|
+
/**
|
|
478
|
+
* Close dropdown.
|
|
479
|
+
*/
|
|
480
|
+
close = () => {
|
|
481
|
+
if (!this.isOpen)
|
|
482
|
+
return;
|
|
483
|
+
this.isOpen = false;
|
|
484
|
+
this.focusedIndex = -1;
|
|
485
|
+
// onCloseStart callback
|
|
486
|
+
if (typeof this.options.onCloseStart === 'function') {
|
|
487
|
+
this.options.onCloseStart.call(this, this.el);
|
|
488
|
+
}
|
|
489
|
+
this._animateOut();
|
|
490
|
+
this._removeTemporaryEventHandlers();
|
|
491
|
+
if (this.options.autoFocus) {
|
|
492
|
+
this.el.focus();
|
|
493
|
+
}
|
|
494
|
+
this.el.ariaExpanded = 'false';
|
|
495
|
+
};
|
|
496
|
+
/**
|
|
497
|
+
* While dropdown is open, you can recalculate its dimensions if its contents have changed.
|
|
498
|
+
*/
|
|
499
|
+
recalculateDimensions = () => {
|
|
500
|
+
if (this.isOpen) {
|
|
501
|
+
this._resetDropdownPositioningStyles();
|
|
502
|
+
this._placeDropdown();
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { Component } from '../../src/component';
|
|
2
|
+
import { loadPeer } from '../../src/peer-loader';
|
|
3
|
+
const _defaults = {
|
|
4
|
+
plugins: []
|
|
5
|
+
};
|
|
6
|
+
// Each plugin needs its own literal `import('literal-string')` call (see
|
|
7
|
+
// peer-loader.ts) so Vite/webpack/Rollup can statically discover it - a
|
|
8
|
+
// generic specifier-driven loop can't produce that, hence one entry per
|
|
9
|
+
// plugin instead of building the import call from PLUGIN_SPECS's string.
|
|
10
|
+
const PLUGIN_LOADERS = {
|
|
11
|
+
'image-preview': {
|
|
12
|
+
specifier: 'filepond-plugin-image-preview',
|
|
13
|
+
globalName: 'FilePondPluginImagePreview',
|
|
14
|
+
importer: () => import('filepond-plugin-image-preview')
|
|
15
|
+
},
|
|
16
|
+
'file-validate-type': {
|
|
17
|
+
specifier: 'filepond-plugin-file-validate-type',
|
|
18
|
+
globalName: 'FilePondPluginFileValidateType',
|
|
19
|
+
importer: () => import('filepond-plugin-file-validate-type')
|
|
20
|
+
},
|
|
21
|
+
'image-exif-orientation': {
|
|
22
|
+
specifier: 'filepond-plugin-image-exif-orientation',
|
|
23
|
+
globalName: 'FilePondPluginImageExifOrientation',
|
|
24
|
+
importer: () => import('filepond-plugin-image-exif-orientation')
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
// @implement /Users/kzarshenas/Sites/CrazyProject/CrazyPHP/src/Front/Library/Utility/Form/File.ts
|
|
28
|
+
// Enhances the EXISTING `.file-field` structure in place - add
|
|
29
|
+
// `data-file-picker="filepond"` (+ optional `data-file-plugins="a,b,c"`) to
|
|
30
|
+
// an otherwise completely stock `.file-field`, nothing else about the
|
|
31
|
+
// markup changes. FilePond.create() hides the native `<input type=file>`
|
|
32
|
+
// and injects its own root as a *sibling*, it never removes anything from
|
|
33
|
+
// the DOM, so the surrounding button/label/file-path-wrapper stay intact
|
|
34
|
+
// at rest - only the interactive drop area swaps in visually.
|
|
35
|
+
//
|
|
36
|
+
// FilePond and its plugins are optional peerDependencies, loaded on demand
|
|
37
|
+
// via peer-loader.
|
|
38
|
+
export class FileInput extends Component {
|
|
39
|
+
pond;
|
|
40
|
+
ready;
|
|
41
|
+
_fileEl;
|
|
42
|
+
_pathEl;
|
|
43
|
+
constructor(el, options) {
|
|
44
|
+
super(el, options, FileInput);
|
|
45
|
+
this.el.M_FileInput = this;
|
|
46
|
+
const pluginsAttr = this.el.dataset.filePlugins;
|
|
47
|
+
this.options = {
|
|
48
|
+
...FileInput.defaults,
|
|
49
|
+
plugins: pluginsAttr
|
|
50
|
+
? pluginsAttr.split(',').map((p) => p.trim())
|
|
51
|
+
: FileInput.defaults.plugins,
|
|
52
|
+
...options
|
|
53
|
+
};
|
|
54
|
+
this._fileEl = this.el.querySelector('input[type="file"]');
|
|
55
|
+
this._pathEl = this.el.querySelector('.file-path');
|
|
56
|
+
this.ready = this._setup();
|
|
57
|
+
}
|
|
58
|
+
static get defaults() {
|
|
59
|
+
return _defaults;
|
|
60
|
+
}
|
|
61
|
+
static init(els, options = {}) {
|
|
62
|
+
return super.init(els, options, FileInput);
|
|
63
|
+
}
|
|
64
|
+
static getInstance(el) {
|
|
65
|
+
return el.M_FileInput;
|
|
66
|
+
}
|
|
67
|
+
destroy() {
|
|
68
|
+
this.el.classList.remove('file-field-enhanced');
|
|
69
|
+
this.el.querySelector('.btn')?.classList.remove('hide');
|
|
70
|
+
this.el.querySelector('.file-path-wrapper')?.classList.remove('hide');
|
|
71
|
+
this.pond?.destroy();
|
|
72
|
+
this.el.M_FileInput = undefined;
|
|
73
|
+
}
|
|
74
|
+
/** Convenience wrapper over the underlying FilePond instance's own getFiles(). */
|
|
75
|
+
getFiles() {
|
|
76
|
+
return this.pond?.getFiles() ?? [];
|
|
77
|
+
}
|
|
78
|
+
async _setup() {
|
|
79
|
+
if (!this._fileEl) {
|
|
80
|
+
console.error(Error('.file-field enhanced with data-file-picker="filepond" needs an input[type=file]'));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const FilePond = await loadPeer({
|
|
84
|
+
specifier: 'filepond',
|
|
85
|
+
globalName: 'FilePond',
|
|
86
|
+
feature: 'File input (FilePond) enhancement',
|
|
87
|
+
cdnHint: '<link rel="stylesheet" href="path/to/filepond.min.css">\n' +
|
|
88
|
+
' <script src="path/to/filepond.min.js"></script>\n' +
|
|
89
|
+
' (self-hosted - copy from node_modules/filepond/dist/, or a CDN of your choice)'
|
|
90
|
+
}, () => import('filepond'));
|
|
91
|
+
const pluginNames = new Set(this.options.plugins);
|
|
92
|
+
// file-validate-type is auto-registered whenever the input restricts
|
|
93
|
+
// `accept`, matching CrazyPHP's own convention, even if the consumer
|
|
94
|
+
// didn't list it explicitly in data-file-plugins.
|
|
95
|
+
if (this._fileEl.accept)
|
|
96
|
+
pluginNames.add('file-validate-type');
|
|
97
|
+
for (const name of pluginNames) {
|
|
98
|
+
const loader = PLUGIN_LOADERS[name];
|
|
99
|
+
if (!loader)
|
|
100
|
+
continue;
|
|
101
|
+
const plugin = await loadPeer({
|
|
102
|
+
specifier: loader.specifier,
|
|
103
|
+
globalName: loader.globalName,
|
|
104
|
+
feature: `File input FilePond plugin "${name}"`,
|
|
105
|
+
cdnHint: `<script src="path/to/${loader.specifier}.min.js"></script> (self-hosted - copy from node_modules/${loader.specifier}/dist/, or a CDN of your choice)`
|
|
106
|
+
}, loader.importer);
|
|
107
|
+
FilePond.registerPlugin(plugin);
|
|
108
|
+
}
|
|
109
|
+
const pondOptions = {};
|
|
110
|
+
const { dataset } = this._fileEl;
|
|
111
|
+
const readBoolean = (value) => value !== 'false';
|
|
112
|
+
pondOptions.allowMultiple = this._fileEl.multiple;
|
|
113
|
+
pondOptions.disabled = this._fileEl.disabled;
|
|
114
|
+
if (dataset.defaultFile)
|
|
115
|
+
pondOptions.files = [dataset.defaultFile];
|
|
116
|
+
if (dataset.maxFiles)
|
|
117
|
+
pondOptions.maxFiles = Number(dataset.maxFiles);
|
|
118
|
+
if (this._fileEl.multiple)
|
|
119
|
+
pondOptions.allowReorder = readBoolean(dataset.allowReorder);
|
|
120
|
+
if (dataset.labelIdle)
|
|
121
|
+
pondOptions.labelIdle = dataset.labelIdle;
|
|
122
|
+
if (dataset.instantUpload !== undefined)
|
|
123
|
+
pondOptions.instantUpload = readBoolean(dataset.instantUpload);
|
|
124
|
+
if (dataset.dropValidation !== undefined)
|
|
125
|
+
pondOptions.dropValidation = readBoolean(dataset.dropValidation);
|
|
126
|
+
if (dataset.imagePreviewHeight)
|
|
127
|
+
pondOptions.imagePreviewHeight = Number(dataset.imagePreviewHeight);
|
|
128
|
+
if (dataset.stylePanelLayout) {
|
|
129
|
+
pondOptions.stylePanelLayout = dataset.stylePanelLayout;
|
|
130
|
+
}
|
|
131
|
+
if (dataset.stylePanelAspectRatio)
|
|
132
|
+
pondOptions.stylePanelAspectRatio = dataset.stylePanelAspectRatio;
|
|
133
|
+
this.pond = FilePond.create(this._fileEl, pondOptions);
|
|
134
|
+
// FilePond inserts its own root as a *sibling* of the input it was
|
|
135
|
+
// given - but that input lives inside .btn (stock .file-field markup
|
|
136
|
+
// is `.btn > input[type=file]`), so the fresh FilePond root lands
|
|
137
|
+
// inside .btn too. Move it back out to be a direct child of .file-field
|
|
138
|
+
// itself before hiding .btn, or hiding .btn would take the whole
|
|
139
|
+
// FilePond widget down with it.
|
|
140
|
+
const pondRoot = this.pond.element;
|
|
141
|
+
if (pondRoot) {
|
|
142
|
+
this.el.appendChild(pondRoot);
|
|
143
|
+
const fileHeight = Number(dataset.fileHeight);
|
|
144
|
+
if (Number.isFinite(fileHeight) && fileHeight > 0) {
|
|
145
|
+
pondRoot.style.height = `${fileHeight}px`;
|
|
146
|
+
}
|
|
147
|
+
// Matches CrazyPHP's own File.ts behavior - FilePond's free-tier
|
|
148
|
+
// "Powered by FilePond" credit link doesn't fit a themed, branded UI.
|
|
149
|
+
pondRoot.querySelectorAll('.filepond--credits').forEach((el) => el.remove());
|
|
150
|
+
}
|
|
151
|
+
// Everything from the stock .file-field is now redundant - FilePond's
|
|
152
|
+
// own drop zone is the entire upload affordance. Hidden, not removed,
|
|
153
|
+
// so the .file-field structure this component promises to preserve is
|
|
154
|
+
// still there, just not shown alongside FilePond's widget.
|
|
155
|
+
this.el.classList.add('file-field-enhanced');
|
|
156
|
+
this.el.querySelector('.btn')?.classList.add('hide');
|
|
157
|
+
this.el.querySelector('.file-path-wrapper')?.classList.add('hide');
|
|
158
|
+
this._syncPathInput();
|
|
159
|
+
this.pond.on('updatefiles', this._syncPathInput);
|
|
160
|
+
}
|
|
161
|
+
_syncPathInput = () => {
|
|
162
|
+
if (!this._pathEl || !this.pond)
|
|
163
|
+
return;
|
|
164
|
+
this._pathEl.value = this.pond
|
|
165
|
+
.getFiles()
|
|
166
|
+
.map((f) => f.filename)
|
|
167
|
+
.join(', ');
|
|
168
|
+
};
|
|
169
|
+
}
|