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.
Files changed (42) hide show
  1. package/README.md +4 -4
  2. package/components/air-datepicker/_air-datepicker.scss +137 -0
  3. package/components/air-datepicker/air-datepicker.ts +192 -0
  4. package/components/alert/_alerts.scss +130 -0
  5. package/components/alert/alert.ts +73 -0
  6. package/components/appbar/appbar.css +301 -4
  7. package/components/button/_buttons.scss +39 -1
  8. package/components/card/_cards.scss +9 -1
  9. package/components/checkbox/_checkboxes.scss +120 -229
  10. package/components/collapsible/_collapsible.scss +50 -0
  11. package/components/color-input/_color-input.scss +124 -0
  12. package/components/color-input/color-input.ts +203 -0
  13. package/components/dropdown/dropdown.ts +1 -1
  14. package/components/file-input/_file-input-enhanced.scss +61 -0
  15. package/components/file-input/file-input.ts +203 -0
  16. package/components/kanban/_kanban.scss +148 -0
  17. package/components/kanban/kanban.ts +133 -0
  18. package/components/list/_collection.scss +10 -2
  19. package/components/number-input/number-input.ts +103 -0
  20. package/components/password-input/password-input.ts +72 -0
  21. package/components/slider/_range.scss +5 -1
  22. package/components/switch/_switches.scss +5 -0
  23. package/components/textfield/_input-fields.scss +4 -4
  24. package/components/textfield/_select.scss +54 -0
  25. package/components/tom-select/_tom-select-field.scss +256 -0
  26. package/components/tom-select/tom-select-field.ts +175 -0
  27. package/components/toolbar/_toolbar.scss +324 -0
  28. package/components/toolbar/toolbar.ts +152 -0
  29. package/dist/css/materialize.colors.min.css +2 -2
  30. package/dist/css/materialize.css +10575 -711
  31. package/dist/css/materialize.min.css +2 -2
  32. package/dist/css/materialize.min.css.map +1 -1
  33. package/dist/js/materialize.cjs.js +1136 -45
  34. package/dist/js/materialize.d.ts +227 -2
  35. package/dist/js/materialize.js +1136 -45
  36. package/dist/js/materialize.min.js +2 -2
  37. package/dist/js/materialize.mjs +1128 -46
  38. package/package.json +45 -1
  39. package/sass/_colors.scss +191 -5
  40. package/sass/_global.scss +240 -1
  41. package/sass/materialize.scss +14 -1
  42. package/sass/mixins.module.scss +6 -6
@@ -0,0 +1,133 @@
1
+ import { BaseOptions, Component, InitElements, MElement } from '../../src/component';
2
+
3
+ export interface KanbanMoveDetail {
4
+ card: HTMLElement;
5
+ from: HTMLElement;
6
+ to: HTMLElement;
7
+ }
8
+
9
+ export interface KanbanOptions extends BaseOptions {
10
+ /** Enable native drag-and-drop interactions. */
11
+ draggable: boolean;
12
+ /** Called after a card changes column or order. */
13
+ onMove?: (detail: KanbanMoveDetail) => void;
14
+ }
15
+
16
+ const _defaults: KanbanOptions = {
17
+ draggable: true
18
+ };
19
+
20
+ /** A lightweight, dependency-free board for columns of draggable cards. */
21
+ export class Kanban extends Component<KanbanOptions> {
22
+ private _draggedCard: HTMLElement | null = null;
23
+ private _dragSourceColumn: HTMLElement | null = null;
24
+
25
+ private _onDragStart = (event: DragEvent) => {
26
+ const card = (event.target as HTMLElement | null)?.closest<HTMLElement>('.kanban-card');
27
+ if (!card || !this.options.draggable) return;
28
+ this._draggedCard = card;
29
+ this._dragSourceColumn = card.closest<HTMLElement>('.kanban-column');
30
+ card.classList.add('is-dragging');
31
+ event.dataTransfer?.setData('text/plain', card.dataset.kanbanCard || '');
32
+ if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move';
33
+ };
34
+
35
+ private _onDragOver = (event: DragEvent) => {
36
+ if (!this._draggedCard) return;
37
+ const column = (event.target as HTMLElement | null)?.closest<HTMLElement>('.kanban-column');
38
+ if (!column || !this.el.contains(column)) return;
39
+ event.preventDefault();
40
+ if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';
41
+ this.el.querySelectorAll('.kanban-column.is-drag-over').forEach((item) => item.classList.remove('is-drag-over'));
42
+ column.classList.add('is-drag-over');
43
+ };
44
+
45
+ private _onDrop = (event: DragEvent) => {
46
+ if (!this._draggedCard) return;
47
+ const column = (event.target as HTMLElement | null)?.closest<HTMLElement>('.kanban-column');
48
+ if (!column || !this.el.contains(column)) return;
49
+ event.preventDefault();
50
+ const body = column.querySelector<HTMLElement>('.kanban-column-body') || column;
51
+ const targetCard = (event.target as HTMLElement | null)?.closest<HTMLElement>('.kanban-card');
52
+ const source = this._dragSourceColumn;
53
+ if (targetCard && targetCard !== this._draggedCard && targetCard.parentElement === body) {
54
+ const box = targetCard.getBoundingClientRect();
55
+ body.insertBefore(this._draggedCard, event.clientY < box.top + box.height / 2 ? targetCard : targetCard.nextSibling);
56
+ } else {
57
+ body.appendChild(this._draggedCard);
58
+ }
59
+ this._clearDragState();
60
+ this._updateCounts();
61
+ if (source && source !== column) this.options.onMove?.({ card: this._draggedCard, from: source, to: column });
62
+ };
63
+
64
+ private _onDragEnd = () => this._clearDragState();
65
+
66
+ constructor(el: HTMLElement, options: Partial<KanbanOptions>) {
67
+ super(el, options, Kanban);
68
+ this.options = { ...Kanban.defaults, ...options };
69
+ this.el['M_Kanban'] = this;
70
+ this._prepareMarkup();
71
+ if (this.options.draggable) this._bindEvents();
72
+ }
73
+
74
+ static get defaults(): KanbanOptions {
75
+ return _defaults;
76
+ }
77
+
78
+ static init(el: HTMLElement, options?: Partial<KanbanOptions>): Kanban;
79
+ static init(els: InitElements<MElement>, options?: Partial<KanbanOptions>): Kanban[];
80
+ static init(els: HTMLElement | InitElements<MElement>, options: Partial<KanbanOptions> = {}): Kanban | Kanban[] {
81
+ return super.init(els, options, Kanban);
82
+ }
83
+
84
+ static getInstance(el: HTMLElement): Kanban {
85
+ return el['M_Kanban'];
86
+ }
87
+
88
+ private _prepareMarkup() {
89
+ this.el.setAttribute('role', 'region');
90
+ this.el.querySelectorAll<HTMLElement>('.kanban-column').forEach((column) => {
91
+ column.setAttribute('role', 'group');
92
+ const body = column.querySelector<HTMLElement>('.kanban-column-body');
93
+ if (body) body.setAttribute('role', 'list');
94
+ column.querySelectorAll<HTMLElement>('.kanban-card').forEach((card) => {
95
+ card.setAttribute('role', 'listitem');
96
+ card.setAttribute('tabindex', '0');
97
+ if (this.options.draggable) card.draggable = true;
98
+ });
99
+ });
100
+ this._updateCounts();
101
+ }
102
+
103
+ private _bindEvents() {
104
+ this.el.addEventListener('dragstart', this._onDragStart);
105
+ this.el.addEventListener('dragover', this._onDragOver);
106
+ this.el.addEventListener('drop', this._onDrop);
107
+ this.el.addEventListener('dragend', this._onDragEnd);
108
+ }
109
+
110
+ private _clearDragState() {
111
+ this._draggedCard?.classList.remove('is-dragging');
112
+ this.el.querySelectorAll('.kanban-column.is-drag-over').forEach((item) => item.classList.remove('is-drag-over'));
113
+ this._draggedCard = null;
114
+ this._dragSourceColumn = null;
115
+ }
116
+
117
+ private _updateCounts() {
118
+ this.el.querySelectorAll<HTMLElement>('.kanban-column').forEach((column) => {
119
+ const count = column.querySelector<HTMLElement>('.kanban-column-count');
120
+ const body = column.querySelector<HTMLElement>('.kanban-column-body');
121
+ if (count && body) count.textContent = String(body.querySelectorAll(':scope > .kanban-card').length);
122
+ });
123
+ }
124
+
125
+ destroy() {
126
+ this._clearDragState();
127
+ this.el.removeEventListener('dragstart', this._onDragStart);
128
+ this.el.removeEventListener('dragover', this._onDragOver);
129
+ this.el.removeEventListener('drop', this._onDrop);
130
+ this.el.removeEventListener('dragend', this._onDragEnd);
131
+ delete this.el['M_Kanban'];
132
+ }
133
+ }
@@ -3,9 +3,13 @@
3
3
  .collection {
4
4
  padding-left: 0;
5
5
  list-style-type: none;
6
+ background-color: var(--md-sys-color-surface);
6
7
  margin: $element-top-margin 0 $element-bottom-margin 0;
7
8
  border: 1px solid var(--md-sys-color-outline-variant);
8
- border-radius: 2px;
9
+ // @implement /Users/kzarshenas/Sites/RodeoFx/rodeo_toolkit_2/app/Front/style/scss/component_enhance.scss
10
+ // Matches the 12px corner radius used across cards, dropdowns and
11
+ // collapsibles instead of the near-square 2px default.
12
+ border-radius: 12px;
9
13
  overflow: hidden;
10
14
  position: relative;
11
15
 
@@ -89,7 +93,11 @@
89
93
 
90
94
  &.with-header {
91
95
  .collection-header {
92
- background-color: transparent;
96
+ // @implement /Users/kzarshenas/Sites/RodeoFx/rodeo_toolkit_2/app/Front/style/scss/component_enhance.scss
97
+ // A tinted header reads as its own row instead of blending into the
98
+ // list; it also isn't text, so it shouldn't be selectable.
99
+ background-color: var(--md-sys-color-surface-variant);
100
+ user-select: none;
93
101
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
94
102
  padding: 10px 20px;
95
103
  }
@@ -0,0 +1,103 @@
1
+ import type IMaskCtor from 'imask';
2
+ import type { MaskedNumberOptions, InputMask } from 'imask';
3
+ import { Component, BaseOptions, InitElements, MElement } from '../../src/component';
4
+ import { loadPeer } from '../../src/peer-loader';
5
+
6
+ export interface NumberInputOptions extends BaseOptions {
7
+ thousandsSeparator: string;
8
+ radix: string;
9
+ mapToRadix: string[];
10
+ min?: number;
11
+ max?: number;
12
+ scale?: number;
13
+ }
14
+
15
+ const _defaults: NumberInputOptions = {
16
+ thousandsSeparator: ' ',
17
+ radix: '.',
18
+ mapToRadix: [',']
19
+ };
20
+
21
+ // @implement /Users/kzarshenas/Sites/CrazyProject/CrazyPHP/src/Front/Library/Utility/Form/Number.ts
22
+ // Masked/formatted numeric input via IMask (thousands separator, decimal
23
+ // scale, min/max) - deliberately opts in on `type="text" data-type="number"`
24
+ // rather than `type="number"`, so IMask can format the display value
25
+ // (thousands separators etc.) that a native number input wouldn't allow.
26
+ //
27
+ // IMask is an optional peerDependency, loaded on demand via peer-loader -
28
+ // see that file for the bundler/no-bundler resolution strategy.
29
+ export class NumberInput extends Component<NumberInputOptions> {
30
+ declare el: HTMLInputElement;
31
+ mask: InputMask<MaskedNumberOptions> | undefined;
32
+ ready: Promise<void>;
33
+
34
+ constructor(el: HTMLInputElement, options: Partial<NumberInputOptions>) {
35
+ super(el, options, NumberInput);
36
+ (this.el as any).M_NumberInput = this;
37
+
38
+ this.options = {
39
+ ...NumberInput.defaults,
40
+ ...options
41
+ };
42
+
43
+ this.ready = this._setup();
44
+ }
45
+
46
+ static get defaults(): NumberInputOptions {
47
+ return _defaults;
48
+ }
49
+
50
+ static init(el: HTMLInputElement, options?: Partial<NumberInputOptions>): NumberInput;
51
+ static init(
52
+ els: InitElements<HTMLInputElement | MElement>,
53
+ options?: Partial<NumberInputOptions>
54
+ ): NumberInput[];
55
+ static init(
56
+ els: HTMLInputElement | InitElements<HTMLInputElement | MElement>,
57
+ options: Partial<NumberInputOptions> = {}
58
+ ): NumberInput | NumberInput[] {
59
+ return super.init(els, options, NumberInput);
60
+ }
61
+
62
+ static getInstance(el: HTMLInputElement): NumberInput {
63
+ return (el as any).M_NumberInput;
64
+ }
65
+
66
+ destroy() {
67
+ this.mask?.destroy();
68
+ (this.el as any).M_NumberInput = undefined;
69
+ }
70
+
71
+ async _setup(): Promise<void> {
72
+ const IMask = await loadPeer<typeof IMaskCtor>(
73
+ {
74
+ specifier: 'imask',
75
+ globalName: 'IMask',
76
+ feature: 'Number input (IMask) enhancement',
77
+ cdnHint: '<script src="path/to/imask.min.js"></script> (self-hosted - copy from node_modules/imask/dist/imask.min.js, or a CDN of your choice)'
78
+ },
79
+ () => import('imask')
80
+ );
81
+
82
+ const maskOptions: MaskedNumberOptions = {
83
+ mask: Number,
84
+ skipInvalid: true,
85
+ thousandsSeparator: this.options.thousandsSeparator,
86
+ radix: this.options.radix,
87
+ mapToRadix: this.options.mapToRadix,
88
+ autofix: true
89
+ };
90
+
91
+ const max = this.options.max ?? (this.el.hasAttribute('max') ? Number(this.el.getAttribute('max')) : undefined);
92
+ if (max !== undefined && !Number.isNaN(max)) maskOptions.max = max;
93
+
94
+ const min = this.options.min ?? (this.el.hasAttribute('min') ? Number(this.el.getAttribute('min')) : undefined);
95
+ if (min !== undefined && !Number.isNaN(min)) maskOptions.min = min;
96
+
97
+ const step = this.el.getAttribute('step');
98
+ const scale = this.options.scale ?? (step?.includes('.') ? step.split('.').at(-1)?.length : undefined);
99
+ if (scale !== undefined) maskOptions.scale = scale;
100
+
101
+ this.mask = IMask(this.el, maskOptions) as InputMask<MaskedNumberOptions>;
102
+ }
103
+ }
@@ -0,0 +1,72 @@
1
+ import { Component, BaseOptions, InitElements, MElement } from '../../src/component';
2
+
3
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
4
+ export interface PasswordInputOptions extends BaseOptions {}
5
+
6
+ const _defaults: PasswordInputOptions = {};
7
+
8
+ // @implement /Users/kzarshenas/Sites/CrazyProject/CrazyPHP/src/Front/Library/Utility/Form/Password.ts
9
+ // Show/hide toggle for a password input - no third-party dependency, just
10
+ // Materialize's own .prefix/.suffix icon-slot convention (see
11
+ // components/textfield/_input-fields.scss), so unlike the other new form
12
+ // enhancements in this batch this one needs no peer-loader/dynamic import.
13
+ export class PasswordInput extends Component<PasswordInputOptions> {
14
+ declare el: HTMLInputElement;
15
+ private _suffixEl: HTMLElement | null;
16
+
17
+ constructor(el: HTMLInputElement, options: Partial<PasswordInputOptions>) {
18
+ super(el, options, PasswordInput);
19
+ (this.el as any).M_PasswordInput = this;
20
+
21
+ this.options = {
22
+ ...PasswordInput.defaults,
23
+ ...options
24
+ };
25
+
26
+ this._suffixEl = this.el.parentElement?.querySelector<HTMLElement>('[data-password-toggle-icon]') ?? null;
27
+ if (!this.el.dataset.passwordVisible) this.el.dataset.passwordVisible = '0';
28
+ this._setupEventHandlers();
29
+ }
30
+
31
+ static get defaults(): PasswordInputOptions {
32
+ return _defaults;
33
+ }
34
+
35
+ static init(el: HTMLInputElement, options?: Partial<PasswordInputOptions>): PasswordInput;
36
+ static init(
37
+ els: InitElements<HTMLInputElement | MElement>,
38
+ options?: Partial<PasswordInputOptions>
39
+ ): PasswordInput[];
40
+ static init(
41
+ els: HTMLInputElement | InitElements<HTMLInputElement | MElement>,
42
+ options: Partial<PasswordInputOptions> = {}
43
+ ): PasswordInput | PasswordInput[] {
44
+ return super.init(els, options, PasswordInput);
45
+ }
46
+
47
+ static getInstance(el: HTMLInputElement): PasswordInput {
48
+ return (el as any).M_PasswordInput;
49
+ }
50
+
51
+ destroy() {
52
+ this._removeEventHandlers();
53
+ (this.el as any).M_PasswordInput = undefined;
54
+ }
55
+
56
+ _setupEventHandlers() {
57
+ this._suffixEl?.addEventListener('click', this._handleToggleClick);
58
+ }
59
+
60
+ _removeEventHandlers() {
61
+ this._suffixEl?.removeEventListener('click', this._handleToggleClick);
62
+ }
63
+
64
+ _handleToggleClick = () => {
65
+ const visible = this.el.dataset.passwordVisible === '1';
66
+ this.el.type = visible ? 'password' : 'text';
67
+ this.el.dataset.passwordVisible = visible ? '0' : '1';
68
+
69
+ const iconEl = this._suffixEl?.querySelector('i');
70
+ if (iconEl) iconEl.textContent = visible ? 'visibility' : 'visibility_off';
71
+ };
72
+ }
@@ -114,7 +114,11 @@ input[type=range]::-moz-focus-inner {
114
114
 
115
115
  input[type=range]::-moz-range-thumb {
116
116
  @include range-thumb;
117
- margin-top: -5px;
117
+ // No margin-top here, unlike the -webkit-slider-thumb rule above:
118
+ // Firefox already vertically centers ::-moz-range-thumb on the track
119
+ // by default. Copying webkit's -5px correction (needed there because
120
+ // Chrome/Safari top-align the thumb by default) over-corrects in
121
+ // Firefox, pushing an already-centered thumb noticeably too high.
118
122
  }
119
123
 
120
124
  // hide the outline behind the border
@@ -23,6 +23,11 @@
23
23
  opacity: 0;
24
24
  width: 0;
25
25
  height: 0;
26
+ // @implement /Users/kzarshenas/Sites/RodeoFx/rodeo_toolkit_2/app/Front/style/scss/component_enhance.scss
27
+ // Without this, an inherited padding can push the (invisible, 0x0)
28
+ // checkbox out of alignment with the lever it's meant to sit under.
29
+ box-sizing: border-box;
30
+ padding: 0 !important;
26
31
 
27
32
  // CHECKED
28
33
  // Track
@@ -50,10 +50,10 @@ textarea.materialize-textarea {
50
50
 
51
51
  // Default
52
52
 
53
- input, textarea {
53
+ input:not([type=checkbox]):not([type=radio]), textarea {
54
54
  box-sizing: border-box; /* https://stackoverflow.com/questions/1377719/padding-within-inputs-breaks-width-100*/
55
55
  padding: 0 16px;
56
- padding-top: 20px;
56
+ // padding-top: 20px;
57
57
  //background-color: transparent; //background-color: var(--md-sys-color-surface);
58
58
  border: none; // reset
59
59
  border-radius: 4px; // md.sys.shape.corner.extra-small.top
@@ -180,7 +180,7 @@ textarea.materialize-textarea {
180
180
 
181
181
  &.outlined {
182
182
 
183
- input, textarea {
183
+ input:not([type=checkbox]):not([type=radio]), textarea {
184
184
  padding-top: 0;
185
185
  background-color: transparent; //(--md-sys-color-background);
186
186
  border: 1px solid var(--md-sys-color-on-surface-variant);
@@ -225,7 +225,7 @@ textarea.materialize-textarea {
225
225
  // border/label/text below should pick up the error color.
226
226
  background-color: transparent;
227
227
 
228
- input, textarea {
228
+ input:not([type=checkbox]):not([type=radio]), textarea {
229
229
  border-color: var(--md-sys-color-error);
230
230
  }
231
231
  input:focus:not([readonly]), textarea:focus:not([readonly]) {
@@ -89,6 +89,60 @@ select:disabled {
89
89
  }
90
90
  }
91
91
 
92
+ // Keep enhanced FormSelect labels in sync with TomSelectField.
93
+ // FormSelect moves the original label inside its generated .select-wrapper.
94
+ .input-field:has(> .select-wrapper) {
95
+ margin-top: 16px;
96
+ }
97
+
98
+ // State 1: a genuinely empty select with no visible placeholder.
99
+ .input-field > .select-wrapper > label {
100
+ position: absolute !important;
101
+ top: 16px !important;
102
+ left: 16px !important;
103
+ z-index: 2;
104
+ margin: 0 !important;
105
+ padding: 0 !important;
106
+ color: var(--md-sys-color-on-surface-variant);
107
+ font-size: 16px !important;
108
+ line-height: 24px !important;
109
+ text-align: left !important;
110
+ background: transparent;
111
+ transform: none !important;
112
+ transform-origin: top left !important;
113
+ transition: top 0.2s ease-out, transform 0.2s ease-out, color 0.2s ease-out;
114
+ }
115
+
116
+ // State 2: the generated input displays a placeholder/selection, the native
117
+ // select declares a placeholder, or the component currently has focus.
118
+ .input-field > .select-wrapper:has(> input.select-dropdown:not(:placeholder-shown)) > label,
119
+ .input-field > .select-wrapper:has(.hide-select > select[placeholder]:not([placeholder=""])) > label,
120
+ .input-field > .select-wrapper:focus-within > label {
121
+ position: absolute !important;
122
+ top: -12px !important;
123
+ left: 16px !important;
124
+ padding: 0 4px !important;
125
+ font-size: 16px !important;
126
+ background: var(--md-sys-color-surface);
127
+ transform: scale(0.75) !important;
128
+ }
129
+
130
+ .input-field > .select-wrapper:focus-within > label {
131
+ color: var(--input-color);
132
+ }
133
+
134
+ // Outlined FormSelect uses the same state logic with the label in its notch.
135
+ .input-field.outlined > .select-wrapper:has(> input.select-dropdown:not(:placeholder-shown)) > label,
136
+ .input-field.outlined > .select-wrapper:has(.hide-select > select[placeholder]:not([placeholder=""])) > label,
137
+ .input-field.outlined > .select-wrapper:focus-within > label {
138
+ top: -8px !important;
139
+ left: 12px !important;
140
+ padding: 0 4px !important;
141
+ line-height: 16px !important;
142
+ background: var(--md-sys-color-surface);
143
+ transform: scale(0.75) !important;
144
+ }
145
+
92
146
  // Optgroup styles
93
147
  .select-dropdown li.optgroup {
94
148
  border-top: 1px solid rgba(0, 0, 0, 0.04);