kmaterialize 2.3.3-1.0.2 → 2.3.3-1.0.21
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/README.md +4 -4
- package/components/air-datepicker/_air-datepicker.scss +137 -0
- package/components/air-datepicker/air-datepicker.ts +192 -0
- package/components/alert/_alerts.scss +130 -0
- package/components/alert/alert.ts +73 -0
- package/components/appbar/appbar.css +301 -4
- package/components/button/_buttons.scss +39 -1
- package/components/card/_cards.scss +9 -1
- package/components/checkbox/_checkboxes.scss +120 -229
- package/components/collapsible/_collapsible.scss +50 -0
- package/components/color-input/_color-input.scss +124 -0
- package/components/color-input/color-input.ts +203 -0
- package/components/dropdown/dropdown.ts +1 -1
- package/components/file-input/_file-input-enhanced.scss +61 -0
- package/components/file-input/file-input.ts +203 -0
- package/components/kanban/_kanban.scss +148 -0
- package/components/kanban/kanban.ts +133 -0
- package/components/list/_collection.scss +10 -2
- package/components/number-input/number-input.ts +103 -0
- package/components/password-input/password-input.ts +72 -0
- package/components/slider/_range.scss +5 -1
- package/components/switch/_switches.scss +5 -0
- package/components/textfield/_input-fields.scss +4 -4
- package/components/textfield/_select.scss +54 -0
- package/components/tom-select/_tom-select-field.scss +256 -0
- package/components/tom-select/tom-select-field.ts +175 -0
- package/components/toolbar/_toolbar.scss +324 -0
- package/components/toolbar/toolbar.ts +152 -0
- package/dist/css/materialize.colors.min.css +2 -2
- package/dist/css/materialize.css +10575 -711
- package/dist/css/materialize.min.css +2 -2
- package/dist/css/materialize.min.css.map +1 -1
- package/dist/js/materialize.cjs.js +1136 -45
- package/dist/js/materialize.d.ts +227 -2
- package/dist/js/materialize.js +1136 -45
- package/dist/js/materialize.min.js +2 -2
- package/dist/js/materialize.mjs +1128 -46
- package/package.json +45 -1
- package/sass/_colors.scss +191 -5
- package/sass/_global.scss +240 -1
- package/sass/materialize.scss +14 -1
- package/sass/mixins.module.scss +6 -6
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import type PickrCtor from '@simonwep/pickr';
|
|
2
|
+
import { Component, BaseOptions, InitElements, MElement } from '../../src/component';
|
|
3
|
+
import { loadPeer } from '../../src/peer-loader';
|
|
4
|
+
|
|
5
|
+
export interface ColorInputOptions extends BaseOptions {
|
|
6
|
+
theme: 'classic' | 'monolith' | 'nano';
|
|
7
|
+
opacity: boolean;
|
|
8
|
+
swatches: string[];
|
|
9
|
+
locale?: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// @implement /Users/kzarshenas/Sites/CrazyProject/CrazyPHP/src/Front/Library/Utility/Form/Color.ts
|
|
13
|
+
// Built-in French translation for Pickr's UI strings, activated by
|
|
14
|
+
// `data-color-locale="fr-fr"` alone - no separate options object required
|
|
15
|
+
// from the consumer, unlike a fully custom `options.locale` override.
|
|
16
|
+
const FR_FR: Record<string, string> = {
|
|
17
|
+
'ui:dialog': 'boîte de dialogue du sélecteur de couleur',
|
|
18
|
+
'btn:toggle': 'basculer la boîte de dialogue du sélecteur de couleur',
|
|
19
|
+
'btn:swatch': 'échantillon de couleur',
|
|
20
|
+
'btn:last-color': 'utiliser la couleur précédente',
|
|
21
|
+
'btn:save': 'Enregistrer',
|
|
22
|
+
'btn:cancel': 'Annuler',
|
|
23
|
+
'btn:clear': 'Effacer',
|
|
24
|
+
'aria:btn:save': 'enregistrer et fermer',
|
|
25
|
+
'aria:btn:cancel': 'annuler et fermer',
|
|
26
|
+
'aria:btn:clear': 'effacer et fermer',
|
|
27
|
+
'aria:input': 'champ de saisie de couleur',
|
|
28
|
+
'aria:palette': 'zone de sélection des couleurs',
|
|
29
|
+
'aria:hue': 'curseur de sélection de teinte',
|
|
30
|
+
'aria:opacity': "curseur de sélection d'opacité"
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const _defaults: ColorInputOptions = {
|
|
34
|
+
theme: 'classic',
|
|
35
|
+
opacity: true,
|
|
36
|
+
swatches: [
|
|
37
|
+
'rgba(244, 67, 54, 1)',
|
|
38
|
+
'rgba(233, 30, 99, 0.95)',
|
|
39
|
+
'rgba(156, 39, 176, 0.9)',
|
|
40
|
+
'rgba(103, 58, 183, 0.85)',
|
|
41
|
+
'rgba(63, 81, 181, 0.8)',
|
|
42
|
+
'rgba(33, 150, 243, 0.75)',
|
|
43
|
+
'rgba(3, 169, 244, 0.7)',
|
|
44
|
+
'rgba(0, 188, 212, 0.7)',
|
|
45
|
+
'rgba(0, 150, 136, 0.75)',
|
|
46
|
+
'rgba(76, 175, 80, 0.8)',
|
|
47
|
+
'rgba(139, 195, 74, 0.85)',
|
|
48
|
+
'rgba(205, 220, 57, 0.9)',
|
|
49
|
+
'rgba(255, 235, 59, 0.95)',
|
|
50
|
+
'rgba(255, 193, 7, 1)'
|
|
51
|
+
]
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// @implement /Users/kzarshenas/Sites/CrazyProject/CrazyPHP/src/Front/Library/Utility/Form/Color.ts
|
|
55
|
+
// Enhances a native `<input type="color" data-color-picker="pickr">` with a
|
|
56
|
+
// full Pickr popup (palette, hue/opacity sliders, swatches, hex/rgba/hsla).
|
|
57
|
+
// If `data-color-picker` is absent the input is left completely untouched -
|
|
58
|
+
// still a plain native color swatch, matching CrazyPHP's own fallback.
|
|
59
|
+
//
|
|
60
|
+
// Pickr remains an optional peerDependency and is loaded on demand via peer-loader.
|
|
61
|
+
export class ColorInput extends Component<ColorInputOptions> {
|
|
62
|
+
declare el: HTMLInputElement;
|
|
63
|
+
pickr: PickrCtor | undefined;
|
|
64
|
+
ready: Promise<void>;
|
|
65
|
+
private _swatchEl: HTMLDivElement;
|
|
66
|
+
private _labels: HTMLLabelElement[];
|
|
67
|
+
|
|
68
|
+
constructor(el: HTMLInputElement, options: Partial<ColorInputOptions>) {
|
|
69
|
+
super(el, options, ColorInput);
|
|
70
|
+
(this.el as any).M_ColorInput = this;
|
|
71
|
+
|
|
72
|
+
this.options = {
|
|
73
|
+
...ColorInput.defaults,
|
|
74
|
+
...options
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
this._swatchEl = document.createElement('div');
|
|
78
|
+
this.el.after(this._swatchEl);
|
|
79
|
+
this.el.classList.add('hide');
|
|
80
|
+
this.el.addEventListener('change', this._handleInputChange);
|
|
81
|
+
this._labels = Array.from(document.querySelectorAll('label')).filter(
|
|
82
|
+
label => label.htmlFor === this.el.id
|
|
83
|
+
);
|
|
84
|
+
this._labels.forEach(label => label.addEventListener('click', this._handleLabelClick));
|
|
85
|
+
|
|
86
|
+
this.ready = this._setup();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
static get defaults(): ColorInputOptions {
|
|
90
|
+
return _defaults;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
static init(el: HTMLInputElement, options?: Partial<ColorInputOptions>): ColorInput;
|
|
94
|
+
static init(
|
|
95
|
+
els: InitElements<HTMLInputElement | MElement>,
|
|
96
|
+
options?: Partial<ColorInputOptions>
|
|
97
|
+
): ColorInput[];
|
|
98
|
+
static init(
|
|
99
|
+
els: HTMLInputElement | InitElements<HTMLInputElement | MElement>,
|
|
100
|
+
options: Partial<ColorInputOptions> = {}
|
|
101
|
+
): ColorInput | ColorInput[] {
|
|
102
|
+
return super.init(els, options, ColorInput);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
static getInstance(el: HTMLInputElement): ColorInput {
|
|
106
|
+
return (el as any).M_ColorInput;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
destroy() {
|
|
110
|
+
this.el.removeEventListener('change', this._handleInputChange);
|
|
111
|
+
this._labels.forEach(label => label.removeEventListener('click', this._handleLabelClick));
|
|
112
|
+
this.pickr?.destroyAndRemove();
|
|
113
|
+
this._swatchEl.remove();
|
|
114
|
+
this.el.classList.remove('hide');
|
|
115
|
+
(this.el as any).M_ColorInput = undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
_handleInputChange = () => {
|
|
119
|
+
this.pickr?.setColor(this.el.value);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
_handleLabelClick = (event: MouseEvent) => {
|
|
123
|
+
// A label associated with a hidden input[type=color] otherwise opens
|
|
124
|
+
// the browser's native picker (notably in Firefox). Route the same
|
|
125
|
+
// accessible label activation to Pickr instead.
|
|
126
|
+
event.preventDefault();
|
|
127
|
+
void this.ready.then(() => this.pickr?.show());
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
async _setup(): Promise<void> {
|
|
131
|
+
const Pickr = await loadPeer<typeof PickrCtor>(
|
|
132
|
+
{
|
|
133
|
+
specifier: '@simonwep/pickr',
|
|
134
|
+
globalName: 'Pickr',
|
|
135
|
+
feature: 'Color input (Pickr) enhancement',
|
|
136
|
+
cdnHint: '<link rel="stylesheet" href="path/to/pickr-classic.min.css">\n' +
|
|
137
|
+
' <script src="path/to/pickr.min.js"></script>\n' +
|
|
138
|
+
' (self-hosted - copy from node_modules/@simonwep/pickr/dist/, or a CDN of your choice)'
|
|
139
|
+
},
|
|
140
|
+
() => import('@simonwep/pickr')
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const dataset = this.el.dataset;
|
|
144
|
+
const theme = (['classic', 'monolith', 'nano'].includes(dataset.colorTheme ?? '')
|
|
145
|
+
? dataset.colorTheme
|
|
146
|
+
: this.options.theme) as ColorInputOptions['theme'];
|
|
147
|
+
const lockOpacity = dataset.colorOpacity !== undefined
|
|
148
|
+
? ['false', '0', '', 'null'].includes(dataset.colorOpacity)
|
|
149
|
+
: !this.options.opacity;
|
|
150
|
+
|
|
151
|
+
const pickrOptions: any = {
|
|
152
|
+
el: this._swatchEl,
|
|
153
|
+
theme,
|
|
154
|
+
lockOpacity,
|
|
155
|
+
swatches: this.options.swatches,
|
|
156
|
+
components: {
|
|
157
|
+
preview: true,
|
|
158
|
+
opacity: true,
|
|
159
|
+
hue: true,
|
|
160
|
+
interaction: {
|
|
161
|
+
hex: true,
|
|
162
|
+
rgba: true,
|
|
163
|
+
hsla: true,
|
|
164
|
+
hsva: true,
|
|
165
|
+
cmyk: true,
|
|
166
|
+
input: true,
|
|
167
|
+
clear: true,
|
|
168
|
+
save: true
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
if (this.options.locale) {
|
|
174
|
+
pickrOptions.i18n = this.options.locale;
|
|
175
|
+
} else if (dataset.colorLocale === 'fr-fr') {
|
|
176
|
+
pickrOptions.i18n = FR_FR;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const currentValue = this.el.getAttribute('value');
|
|
180
|
+
if (currentValue && currentValue !== 'randomHex()') {
|
|
181
|
+
pickrOptions.default = currentValue;
|
|
182
|
+
} else if (this.el.hasAttribute('default')) {
|
|
183
|
+
const currentDefault = this.el.getAttribute('default');
|
|
184
|
+
if (currentDefault === 'randomHex()') {
|
|
185
|
+
const randomColor = Math.floor(Math.random() * 16777216);
|
|
186
|
+
pickrOptions.default = `#${randomColor.toString(16).padStart(6, '0')}`;
|
|
187
|
+
} else if (currentDefault) {
|
|
188
|
+
pickrOptions.default = currentDefault;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
this.pickr = Pickr.create(pickrOptions);
|
|
193
|
+
if (this.el.disabled) this.pickr.disable();
|
|
194
|
+
this.pickr.on('save', (color: any, instance: any) => {
|
|
195
|
+
const hexa: string = color?.toHEXA().toString();
|
|
196
|
+
if (hexa) {
|
|
197
|
+
this.el.value = hexa;
|
|
198
|
+
this.el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
199
|
+
}
|
|
200
|
+
instance.hide();
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -248,7 +248,7 @@ export class Dropdown extends Component<DropdownOptions> implements Openable {
|
|
|
248
248
|
if (this.options.closeOnClick && target.closest('.dropdown-content') && !this.isTouchMoving) {
|
|
249
249
|
// isTouchMoving to check if scrolling on mobile.
|
|
250
250
|
this.close();
|
|
251
|
-
} else if (!target.closest('.dropdown-content')) {
|
|
251
|
+
} else if (!target.closest('.dropdown-content') && !target.closest('.dropdown-trigger')) {
|
|
252
252
|
// Do this one frame later so that if the element clicked also triggers _handleClick
|
|
253
253
|
// For example, if a label for a select was clicked, that we don't close/open the dropdown
|
|
254
254
|
setTimeout(() => {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// @implement /Users/kzarshenas/Sites/CrazyProject/CrazyPHP/resources/Scss/style/enhancement/filepond_materializecss.scss
|
|
2
|
+
// FilePond's own drop-label defaults to a near-black hardcoded color that
|
|
3
|
+
// doesn't track kmaterialize's theme tokens - use an MD3 token instead.
|
|
4
|
+
// FilePond's injected root is a sibling of the original (now hidden) file
|
|
5
|
+
// input, which pushes the Materialize `<label>` that used to trail the
|
|
6
|
+
// input out of position - pin it back to the field's left edge.
|
|
7
|
+
.filepond--drop-label.filepond--drop-label label {
|
|
8
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.filepond--root + label {
|
|
12
|
+
left: 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Once FileInput (components/file-input/file-input.ts) enhances a
|
|
16
|
+
// .file-field, the stock button/file-path chrome is hidden and FilePond's
|
|
17
|
+
// own root (moved here to be a direct child, not left buried inside the
|
|
18
|
+
// now-hidden .btn) is the entire upload affordance - .file-field's normal
|
|
19
|
+
// 2-column button+text-field grid no longer applies.
|
|
20
|
+
.file-field.file-field-enhanced {
|
|
21
|
+
display: block;
|
|
22
|
+
|
|
23
|
+
> label {
|
|
24
|
+
position: static;
|
|
25
|
+
display: block;
|
|
26
|
+
margin-bottom: 0.375rem;
|
|
27
|
+
color: var(--md-sys-color-on-surface);
|
|
28
|
+
transform: none;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.filepond--root {
|
|
32
|
+
margin: 0.5rem 0 1rem 0;
|
|
33
|
+
font-size: 1rem;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.filepond--panel-root {
|
|
37
|
+
border-radius: 12px;
|
|
38
|
+
background-color: var(--md-sys-color-surface-variant);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.filepond--drop-label {
|
|
42
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.filepond--label-action {
|
|
46
|
+
color: var(--md-sys-color-primary);
|
|
47
|
+
text-decoration-color: var(--md-sys-color-primary);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.filepond--drop-label .material-icons {
|
|
51
|
+
font-size: 1.25rem;
|
|
52
|
+
line-height: 1;
|
|
53
|
+
vertical-align: middle;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.filepond-label-content {
|
|
57
|
+
display: inline-flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
gap: 0.5rem;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import type * as FilePondNS from 'filepond';
|
|
2
|
+
import { Component, BaseOptions, InitElements, MElement } from '../../src/component';
|
|
3
|
+
import { loadPeer } from '../../src/peer-loader';
|
|
4
|
+
|
|
5
|
+
export type FilePondPluginName = 'image-preview' | 'file-validate-type' | 'image-exif-orientation';
|
|
6
|
+
|
|
7
|
+
export interface FileInputOptions extends BaseOptions {
|
|
8
|
+
/** Which optional FilePond plugins to register - each is its own optional peerDependency. */
|
|
9
|
+
plugins: FilePondPluginName[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const _defaults: FileInputOptions = {
|
|
13
|
+
plugins: []
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Each plugin needs its own literal `import('literal-string')` call (see
|
|
17
|
+
// peer-loader.ts) so Vite/webpack/Rollup can statically discover it - a
|
|
18
|
+
// generic specifier-driven loop can't produce that, hence one entry per
|
|
19
|
+
// plugin instead of building the import call from PLUGIN_SPECS's string.
|
|
20
|
+
const PLUGIN_LOADERS: Record<
|
|
21
|
+
FilePondPluginName,
|
|
22
|
+
{ specifier: string; globalName: string; importer: () => Promise<unknown> }
|
|
23
|
+
> = {
|
|
24
|
+
'image-preview': {
|
|
25
|
+
specifier: 'filepond-plugin-image-preview',
|
|
26
|
+
globalName: 'FilePondPluginImagePreview',
|
|
27
|
+
importer: () => import('filepond-plugin-image-preview')
|
|
28
|
+
},
|
|
29
|
+
'file-validate-type': {
|
|
30
|
+
specifier: 'filepond-plugin-file-validate-type',
|
|
31
|
+
globalName: 'FilePondPluginFileValidateType',
|
|
32
|
+
importer: () => import('filepond-plugin-file-validate-type')
|
|
33
|
+
},
|
|
34
|
+
'image-exif-orientation': {
|
|
35
|
+
specifier: 'filepond-plugin-image-exif-orientation',
|
|
36
|
+
globalName: 'FilePondPluginImageExifOrientation',
|
|
37
|
+
importer: () => import('filepond-plugin-image-exif-orientation')
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// @implement /Users/kzarshenas/Sites/CrazyProject/CrazyPHP/src/Front/Library/Utility/Form/File.ts
|
|
42
|
+
// Enhances the EXISTING `.file-field` structure in place - add
|
|
43
|
+
// `data-file-picker="filepond"` (+ optional `data-file-plugins="a,b,c"`) to
|
|
44
|
+
// an otherwise completely stock `.file-field`, nothing else about the
|
|
45
|
+
// markup changes. FilePond.create() hides the native `<input type=file>`
|
|
46
|
+
// and injects its own root as a *sibling*, it never removes anything from
|
|
47
|
+
// the DOM, so the surrounding button/label/file-path-wrapper stay intact
|
|
48
|
+
// at rest - only the interactive drop area swaps in visually.
|
|
49
|
+
//
|
|
50
|
+
// FilePond and its plugins are optional peerDependencies, loaded on demand
|
|
51
|
+
// via peer-loader.
|
|
52
|
+
export class FileInput extends Component<FileInputOptions> {
|
|
53
|
+
pond: FilePondNS.FilePond | undefined;
|
|
54
|
+
ready: Promise<void>;
|
|
55
|
+
private _fileEl: HTMLInputElement | null;
|
|
56
|
+
private _pathEl: HTMLInputElement | null;
|
|
57
|
+
|
|
58
|
+
constructor(el: HTMLElement, options: Partial<FileInputOptions>) {
|
|
59
|
+
super(el, options, FileInput);
|
|
60
|
+
(this.el as any).M_FileInput = this;
|
|
61
|
+
|
|
62
|
+
const pluginsAttr = this.el.dataset.filePlugins;
|
|
63
|
+
this.options = {
|
|
64
|
+
...FileInput.defaults,
|
|
65
|
+
plugins: pluginsAttr
|
|
66
|
+
? (pluginsAttr.split(',').map((p) => p.trim()) as FilePondPluginName[])
|
|
67
|
+
: FileInput.defaults.plugins,
|
|
68
|
+
...options
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
this._fileEl = this.el.querySelector<HTMLInputElement>('input[type="file"]');
|
|
72
|
+
this._pathEl = this.el.querySelector<HTMLInputElement>('.file-path');
|
|
73
|
+
|
|
74
|
+
this.ready = this._setup();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
static get defaults(): FileInputOptions {
|
|
78
|
+
return _defaults;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
static init(el: HTMLElement, options?: Partial<FileInputOptions>): FileInput;
|
|
82
|
+
static init(els: InitElements<MElement>, options?: Partial<FileInputOptions>): FileInput[];
|
|
83
|
+
static init(
|
|
84
|
+
els: HTMLElement | InitElements<MElement>,
|
|
85
|
+
options: Partial<FileInputOptions> = {}
|
|
86
|
+
): FileInput | FileInput[] {
|
|
87
|
+
return super.init(els, options, FileInput);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
static getInstance(el: HTMLElement): FileInput {
|
|
91
|
+
return (el as any).M_FileInput;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
destroy() {
|
|
95
|
+
this.el.classList.remove('file-field-enhanced');
|
|
96
|
+
this.el.querySelector<HTMLElement>('.btn')?.classList.remove('hide');
|
|
97
|
+
this.el.querySelector<HTMLElement>('.file-path-wrapper')?.classList.remove('hide');
|
|
98
|
+
this.pond?.destroy();
|
|
99
|
+
(this.el as any).M_FileInput = undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Convenience wrapper over the underlying FilePond instance's own getFiles(). */
|
|
103
|
+
getFiles() {
|
|
104
|
+
return this.pond?.getFiles() ?? [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async _setup(): Promise<void> {
|
|
108
|
+
if (!this._fileEl) {
|
|
109
|
+
console.error(Error('.file-field enhanced with data-file-picker="filepond" needs an input[type=file]'));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const FilePond = await loadPeer<typeof FilePondNS>(
|
|
114
|
+
{
|
|
115
|
+
specifier: 'filepond',
|
|
116
|
+
globalName: 'FilePond',
|
|
117
|
+
feature: 'File input (FilePond) enhancement',
|
|
118
|
+
cdnHint:
|
|
119
|
+
'<link rel="stylesheet" href="path/to/filepond.min.css">\n' +
|
|
120
|
+
' <script src="path/to/filepond.min.js"></script>\n' +
|
|
121
|
+
' (self-hosted - copy from node_modules/filepond/dist/, or a CDN of your choice)'
|
|
122
|
+
},
|
|
123
|
+
() => import('filepond')
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const pluginNames = new Set(this.options.plugins);
|
|
127
|
+
// file-validate-type is auto-registered whenever the input restricts
|
|
128
|
+
// `accept`, matching CrazyPHP's own convention, even if the consumer
|
|
129
|
+
// didn't list it explicitly in data-file-plugins.
|
|
130
|
+
if (this._fileEl.accept) pluginNames.add('file-validate-type');
|
|
131
|
+
|
|
132
|
+
for (const name of pluginNames) {
|
|
133
|
+
const loader = PLUGIN_LOADERS[name];
|
|
134
|
+
if (!loader) continue;
|
|
135
|
+
const plugin = await loadPeer(
|
|
136
|
+
{
|
|
137
|
+
specifier: loader.specifier,
|
|
138
|
+
globalName: loader.globalName,
|
|
139
|
+
feature: `File input FilePond plugin "${name}"`,
|
|
140
|
+
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)`
|
|
141
|
+
},
|
|
142
|
+
loader.importer
|
|
143
|
+
);
|
|
144
|
+
FilePond.registerPlugin(plugin as any);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const pondOptions: FilePondNS.FilePondOptions = {};
|
|
148
|
+
const { dataset } = this._fileEl;
|
|
149
|
+
const readBoolean = (value: string | undefined) => value !== 'false';
|
|
150
|
+
|
|
151
|
+
pondOptions.allowMultiple = this._fileEl.multiple;
|
|
152
|
+
pondOptions.disabled = this._fileEl.disabled;
|
|
153
|
+
if (dataset.defaultFile) pondOptions.files = [dataset.defaultFile];
|
|
154
|
+
if (dataset.maxFiles) pondOptions.maxFiles = Number(dataset.maxFiles);
|
|
155
|
+
if (this._fileEl.multiple) pondOptions.allowReorder = readBoolean(dataset.allowReorder);
|
|
156
|
+
if (dataset.labelIdle) pondOptions.labelIdle = dataset.labelIdle;
|
|
157
|
+
if (dataset.instantUpload !== undefined) pondOptions.instantUpload = readBoolean(dataset.instantUpload);
|
|
158
|
+
if (dataset.dropValidation !== undefined) pondOptions.dropValidation = readBoolean(dataset.dropValidation);
|
|
159
|
+
if (dataset.imagePreviewHeight) pondOptions.imagePreviewHeight = Number(dataset.imagePreviewHeight);
|
|
160
|
+
if (dataset.stylePanelLayout) {
|
|
161
|
+
pondOptions.stylePanelLayout = dataset.stylePanelLayout as FilePondNS.FilePondOptions['stylePanelLayout'];
|
|
162
|
+
}
|
|
163
|
+
if (dataset.stylePanelAspectRatio) pondOptions.stylePanelAspectRatio = dataset.stylePanelAspectRatio;
|
|
164
|
+
|
|
165
|
+
this.pond = FilePond.create(this._fileEl, pondOptions);
|
|
166
|
+
|
|
167
|
+
// FilePond inserts its own root as a *sibling* of the input it was
|
|
168
|
+
// given - but that input lives inside .btn (stock .file-field markup
|
|
169
|
+
// is `.btn > input[type=file]`), so the fresh FilePond root lands
|
|
170
|
+
// inside .btn too. Move it back out to be a direct child of .file-field
|
|
171
|
+
// itself before hiding .btn, or hiding .btn would take the whole
|
|
172
|
+
// FilePond widget down with it.
|
|
173
|
+
const pondRoot = this.pond.element;
|
|
174
|
+
if (pondRoot) {
|
|
175
|
+
this.el.appendChild(pondRoot);
|
|
176
|
+
const fileHeight = Number(dataset.fileHeight);
|
|
177
|
+
if (Number.isFinite(fileHeight) && fileHeight > 0) {
|
|
178
|
+
(pondRoot as HTMLElement).style.height = `${fileHeight}px`;
|
|
179
|
+
}
|
|
180
|
+
// Matches CrazyPHP's own File.ts behavior - FilePond's free-tier
|
|
181
|
+
// "Powered by FilePond" credit link doesn't fit a themed, branded UI.
|
|
182
|
+
pondRoot.querySelectorAll('.filepond--credits').forEach((el) => el.remove());
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Everything from the stock .file-field is now redundant - FilePond's
|
|
186
|
+
// own drop zone is the entire upload affordance. Hidden, not removed,
|
|
187
|
+
// so the .file-field structure this component promises to preserve is
|
|
188
|
+
// still there, just not shown alongside FilePond's widget.
|
|
189
|
+
this.el.classList.add('file-field-enhanced');
|
|
190
|
+
this.el.querySelector<HTMLElement>('.btn')?.classList.add('hide');
|
|
191
|
+
this.el.querySelector<HTMLElement>('.file-path-wrapper')?.classList.add('hide');
|
|
192
|
+
this._syncPathInput();
|
|
193
|
+
this.pond.on('updatefiles', this._syncPathInput);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
_syncPathInput = () => {
|
|
197
|
+
if (!this._pathEl || !this.pond) return;
|
|
198
|
+
this._pathEl.value = this.pond
|
|
199
|
+
.getFiles()
|
|
200
|
+
.map((f) => f.filename)
|
|
201
|
+
.join(', ');
|
|
202
|
+
};
|
|
203
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
.kanban-board {
|
|
2
|
+
--kanban-column-width: 20rem;
|
|
3
|
+
--kanban-gap: 1rem;
|
|
4
|
+
display: flex;
|
|
5
|
+
align-items: flex-start;
|
|
6
|
+
gap: var(--kanban-gap);
|
|
7
|
+
width: 100%;
|
|
8
|
+
padding: 0.25rem;
|
|
9
|
+
overflow-x: auto;
|
|
10
|
+
box-sizing: border-box;
|
|
11
|
+
overscroll-behavior-x: contain;
|
|
12
|
+
|
|
13
|
+
&.kanban-wrap {
|
|
14
|
+
flex-wrap: wrap;
|
|
15
|
+
overflow-x: visible;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.kanban-column {
|
|
20
|
+
display: flex;
|
|
21
|
+
flex: 0 0 var(--kanban-column-width);
|
|
22
|
+
flex-direction: column;
|
|
23
|
+
min-height: 15rem;
|
|
24
|
+
padding: 0.75rem;
|
|
25
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
26
|
+
border-radius: 0.875rem;
|
|
27
|
+
background: var(--md-sys-color-surface-variant);
|
|
28
|
+
box-sizing: border-box;
|
|
29
|
+
transition: border-color 160ms ease, background-color 160ms ease;
|
|
30
|
+
|
|
31
|
+
&.is-drag-over {
|
|
32
|
+
border-color: var(--md-sys-color-primary);
|
|
33
|
+
background: color-mix(in srgb, var(--md-sys-color-primary-container) 55%, var(--md-sys-color-surface-variant));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.kanban-column-header {
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
justify-content: space-between;
|
|
41
|
+
gap: 0.5rem;
|
|
42
|
+
padding: 0.25rem 0.25rem 0.75rem;
|
|
43
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.kanban-column-title {
|
|
47
|
+
margin: 0;
|
|
48
|
+
font-size: 1rem;
|
|
49
|
+
font-weight: 600;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.kanban-column-count {
|
|
53
|
+
display: inline-flex;
|
|
54
|
+
min-width: 1.5rem;
|
|
55
|
+
min-height: 1.5rem;
|
|
56
|
+
align-items: center;
|
|
57
|
+
justify-content: center;
|
|
58
|
+
padding: 0 0.375rem;
|
|
59
|
+
border-radius: 999px;
|
|
60
|
+
background: var(--md-sys-color-surface);
|
|
61
|
+
color: var(--md-sys-color-on-surface);
|
|
62
|
+
font-size: 0.75rem;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.kanban-column-body {
|
|
66
|
+
display: flex;
|
|
67
|
+
flex: 1 1 auto;
|
|
68
|
+
flex-direction: column;
|
|
69
|
+
gap: 0.75rem;
|
|
70
|
+
min-height: 8rem;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.kanban-card {
|
|
74
|
+
position: relative;
|
|
75
|
+
display: flex;
|
|
76
|
+
flex-direction: column;
|
|
77
|
+
gap: 0.625rem;
|
|
78
|
+
padding: 1rem;
|
|
79
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
80
|
+
border-radius: 0.75rem;
|
|
81
|
+
background: var(--md-sys-color-surface);
|
|
82
|
+
color: var(--md-sys-color-on-surface);
|
|
83
|
+
box-shadow: var(--md-sys-elevation-level1, 0 1px 2px rgba(0, 0, 0, 0.14));
|
|
84
|
+
cursor: grab;
|
|
85
|
+
user-select: none;
|
|
86
|
+
|
|
87
|
+
&:active,
|
|
88
|
+
&.is-dragging {
|
|
89
|
+
cursor: grabbing;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
&.is-dragging {
|
|
93
|
+
opacity: 0.45;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
&:focus-visible {
|
|
97
|
+
outline: 2px solid var(--md-sys-color-primary);
|
|
98
|
+
outline-offset: 2px;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.kanban-card-title {
|
|
103
|
+
margin: 0;
|
|
104
|
+
font-size: 1rem;
|
|
105
|
+
font-weight: 600;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.kanban-card-description {
|
|
109
|
+
margin: 0;
|
|
110
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
111
|
+
font-size: 0.875rem;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.kanban-card-meta {
|
|
115
|
+
display: flex;
|
|
116
|
+
align-items: center;
|
|
117
|
+
justify-content: space-between;
|
|
118
|
+
gap: 0.5rem;
|
|
119
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
120
|
+
font-size: 0.75rem;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.kanban-card-label {
|
|
124
|
+
display: inline-flex;
|
|
125
|
+
align-items: center;
|
|
126
|
+
align-self: flex-start;
|
|
127
|
+
padding: 0.1875rem 0.5rem;
|
|
128
|
+
border-radius: 999px;
|
|
129
|
+
background: var(--md-sys-color-secondary-container);
|
|
130
|
+
color: var(--md-sys-color-on-secondary-container);
|
|
131
|
+
font-size: 0.75rem;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.kanban-empty {
|
|
135
|
+
display: grid;
|
|
136
|
+
min-height: 5rem;
|
|
137
|
+
place-items: center;
|
|
138
|
+
border: 1px dashed var(--md-sys-color-outline);
|
|
139
|
+
border-radius: 0.625rem;
|
|
140
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
141
|
+
font-size: 0.875rem;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@media (max-width: 600px) {
|
|
145
|
+
.kanban-board {
|
|
146
|
+
--kanban-column-width: min(20rem, calc(100vw - 3rem));
|
|
147
|
+
}
|
|
148
|
+
}
|