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.
Files changed (47) hide show
  1. package/components/air-datepicker/air-datepicker.js +152 -0
  2. package/components/alert/alert.js +54 -0
  3. package/components/button/buttons.js +220 -0
  4. package/components/card/cards.js +149 -0
  5. package/components/carousel/carousel.js +574 -0
  6. package/components/carousel/slider.js +340 -0
  7. package/components/chip/chips.js +341 -0
  8. package/components/collapsible/collapsible.js +156 -0
  9. package/components/color-input/color-input.js +166 -0
  10. package/components/datepicker/datepicker.js +1035 -0
  11. package/components/dialog/materialbox.js +377 -0
  12. package/components/dialog/modal.js +72 -0
  13. package/components/dropdown/dropdown.js +505 -0
  14. package/components/file-input/file-input.js +169 -0
  15. package/components/kanban/_kanban.scss +69 -0
  16. package/components/kanban/kanban.js +168 -0
  17. package/components/kanban/kanban.ts +57 -8
  18. package/components/loading/_loading.scss +68 -0
  19. package/components/loading/loading.ts +104 -0
  20. package/components/loading/loadingSpec.js +102 -0
  21. package/components/navigation-drawer/sidenav.js +454 -0
  22. package/components/number-input/number-input.js +68 -0
  23. package/components/password-input/password-input.js +49 -0
  24. package/components/popup/_popup.scss +106 -0
  25. package/components/popup/popup.ts +113 -0
  26. package/components/popup/popupSpec.js +140 -0
  27. package/components/search/autocomplete.js +470 -0
  28. package/components/slider/range.js +169 -0
  29. package/components/snackbar/toasts.js +244 -0
  30. package/components/tabs/tabs.js +289 -0
  31. package/components/textfield/forms.js +172 -0
  32. package/components/textfield/select.js +385 -0
  33. package/components/timepicker/timepicker.js +698 -0
  34. package/components/tom-select/tom-select-field.js +130 -0
  35. package/components/toolbar/toolbar.js +134 -0
  36. package/components/tooltip/tooltip.js +270 -0
  37. package/dist/css/materialize.colors.min.css +1 -1
  38. package/dist/css/materialize.css +322 -1
  39. package/dist/css/materialize.min.css +2 -2
  40. package/dist/css/materialize.min.css.map +1 -1
  41. package/dist/js/materialize.cjs.js +258 -42
  42. package/dist/js/materialize.d.ts +50 -3
  43. package/dist/js/materialize.js +258 -42
  44. package/dist/js/materialize.min.js +2 -2
  45. package/dist/js/materialize.mjs +257 -43
  46. package/package.json +6 -1
  47. 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
+ }