material 0.9.21 → 0.9.22

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/index.js CHANGED
@@ -7,6 +7,7 @@ import Badge from './src/badge'
7
7
  import Button from './src/button'
8
8
  import Card from './src/card'
9
9
  import Checkbox from './src/checkbox'
10
+ import ColorPicker from './src/colorpicker'
10
11
  import DateHour from './src/datehour'
11
12
  import Dialog from './src/dialog'
12
13
  import Drawer from './src/drawer'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "material",
3
- "version": "0.9.21",
3
+ "version": "0.9.22",
4
4
  "description": "A lightweight implementation of Material Design Components for the web - ES6",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -0,0 +1,10 @@
1
+ import View from './view'
2
+
3
+ class App extends View {
4
+ static defaults = {
5
+ base: 'app',
6
+ ...View.defaults
7
+ }
8
+ }
9
+
10
+ export default App
@@ -2,33 +2,36 @@ import build from '../module/build'
2
2
  import dom from '../module/dom'
3
3
 
4
4
  class Component {
5
- static base = 'component'
5
+ static defaults = {
6
+ class: 'component',
7
+ mixins: []
8
+ }
6
9
 
7
10
  constructor (options) {
8
11
  this.options = { ...this.constructor.defaults, ...options }
9
12
 
13
+ // Apply any mixins first
10
14
  const mixins = this.options.mixins || []
15
+ mixins.forEach(mixin => Object.assign(this, mixin))
11
16
 
12
- for (let i = 0; i < mixins.length; i++) {
13
- Object.assign(this, mixins[i])
14
- }
15
-
17
+ // Build the component
16
18
  build(this)
17
19
  }
18
20
 
19
- destroy () {
20
- // console.log('destroy')
21
+ async destroy () {
22
+ console.log('destroy')
21
23
  if (this.options.transition && this.element) {
22
24
  this.element.classList.remove('show')
23
-
24
25
  if (this.underlay) {
25
26
  this.underlay.classList.remove('show')
26
27
  }
27
-
28
- setTimeout(() => {
29
- dom.destroy(this.element)
30
- if (this.underlay) dom.destroy(this.underlay)
31
- }, this.options.transition)
28
+ await new Promise(resolve =>
29
+ setTimeout(() => {
30
+ dom.destroy(this.element)
31
+ if (this.underlay) dom.destroy(this.underlay)
32
+ resolve()
33
+ }, this.options.transition)
34
+ )
32
35
  } else {
33
36
  dom.destroy(this.element)
34
37
  if (this.underlay) dom.destroy(this.underlay)
@@ -1,38 +1,46 @@
1
1
  import Component from './component'
2
2
  import events from '../module/events'
3
+ import emitter from '../module/emitter'
3
4
 
4
5
  class Control extends Component {
5
- static base = 'control'
6
+ static defaults = {
7
+ base: 'control',
8
+ ...Component.defaults,
9
+ events: []
10
+ }
6
11
 
7
12
  constructor (options) {
8
13
  super(options)
14
+
15
+ // Initialize event storage
9
16
  this.events = {}
10
17
 
18
+ // Mix in emitter functionality
19
+ Object.assign(this, emitter)
20
+
21
+ // Attach DOM events if specified in options
11
22
  if (this.options.events) {
12
23
  events.attach(this.options.events, this)
13
24
  }
14
25
  }
15
26
 
16
- on (event, listener) {
17
- this.events[event] ??= []
18
- this.events[event].push(listener)
19
-
20
- return this
21
- }
27
+ destroy () {
28
+ // First cleanup event listeners
29
+ if (this.options.events) {
30
+ events.detach(this.options.events, this)
31
+ }
22
32
 
23
- emit (event, ...args) {
24
- this.events?.[event]?.forEach(listener => listener.apply(this, args))
33
+ // Clean up emitter subscriptions
34
+ this.removeAll()
25
35
 
26
- return this
27
- }
36
+ // Emit destroy event before DOM removal
37
+ this.emit('destroy')
28
38
 
29
- destroy () {
30
- // if (this.options.events) {
31
- // events.detach(this.options.events, this)
32
- // }
39
+ // Clear event storage
40
+ this.events = null
33
41
 
42
+ // Let parent handle DOM cleanup
34
43
  super.destroy()
35
- this.emit?.('destroy')
36
44
  }
37
45
  }
38
46
 
package/src/class/form.js CHANGED
@@ -1,20 +1,21 @@
1
1
  import Component from './component'
2
2
  import events from '../module/events'
3
3
  import build from '../form/build'
4
+ import emitter from '../module/emitter'
4
5
 
5
6
  class Form {
6
- static base = 'form'
7
+ static defaults = {
8
+ base: 'form',
9
+ events: []
10
+ }
7
11
 
8
12
  constructor (options) {
9
13
  this.options = { ...this.constructor.defaults, ...options }
10
14
 
11
15
  const mixins = this.options.mixins || []
16
+ mixins.forEach(mixin => Object.assign(this, mixin))
12
17
 
13
- for (let i = 0; i < mixins.length; i++) {
14
- Object.assign(this, mixins[i])
15
- }
16
-
17
- Object.assign(this, build)
18
+ Object.assign(this, build, emitter)
18
19
 
19
20
  this.build(this)
20
21
 
@@ -24,22 +25,6 @@ class Form {
24
25
  events.attach(this.options.events, this)
25
26
  }
26
27
  }
27
-
28
- on (event, listener) {
29
- if (!this.events[event]) {
30
- this.events[event] = []
31
- }
32
- this.events[event].push(listener)
33
-
34
- return this
35
- }
36
-
37
- emit (event, ...args) {
38
- if (this.events[event]) {
39
- this.events[event].forEach(listener => listener.apply(this, args))
40
- }
41
- return this
42
- }
43
28
  }
44
29
 
45
30
  export default Form
package/src/class/view.js CHANGED
@@ -1,29 +1,28 @@
1
- import Component from './component'
2
- import events from '../module/events'
3
-
4
- class View extends Component {
5
- static base = 'view'
1
+ import Control from './control'
2
+ import display from '../mixin/display'
3
+
4
+ class View extends Control {
5
+ static defaults = {
6
+ base: 'view',
7
+ ...Control.defaults,
8
+ display: {}
9
+ }
6
10
 
7
11
  constructor (options) {
12
+ // Let Control handle all the event setup
8
13
  super(options)
9
- this.events = {}
10
-
11
- if (this.options.events) {
12
- events.attach(this.options.events, this)
13
- }
14
- }
15
-
16
- on (event, listener) {
17
- this.events[event] ??= []
18
- this.events[event].push(listener)
19
14
 
20
- return this
15
+ // Add any View-specific initialization here
16
+ // For example, if View needs display functionality:
17
+ Object.assign(this, display)
21
18
  }
22
19
 
23
- emit (event, ...args) {
24
- this.events?.[event]?.forEach(listener => listener.apply(this, args))
20
+ // If View needs any special cleanup:
21
+ destroy () {
22
+ // Do View-specific cleanup first
25
23
 
26
- return this
24
+ // Then let Control handle event cleanup
25
+ super.destroy()
27
26
  }
28
27
  }
29
28
 
@@ -0,0 +1,356 @@
1
+ import {
2
+ Element,
3
+ Control,
4
+ display,
5
+ dataset
6
+ } from '../index'
7
+
8
+ class ColorPicker extends Control {
9
+ static defaults = {
10
+ class: 'colorpicker',
11
+ tag: 'div',
12
+ mixins: [display],
13
+ layout: [
14
+ [Element, 'input', { type: 'text' }],
15
+ [Element, 'preview', { class: 'preview' }],
16
+ [Element, 'panel', { class: 'panel' }, [
17
+ [Element, 'gradient', { class: 'gradient' }],
18
+ [Element, 'hue', { class: 'hue' }],
19
+ [Element, 'hue-slider', { tag: 'input', type: 'range', min: '0', max: '360', step: '1' }],
20
+ [Element, 'hue-marker', { class: 'marker' }],
21
+ [Element, 'alpha', { class: 'alpha' }],
22
+ [Element, 'alpha-slider', { tag: 'input', type: 'range', min: '0', max: '100', step: '1' }],
23
+ [Element, 'alpha-marker', { class: 'marker' }],
24
+ [Element, 'swatches', { class: 'swatches' }]
25
+ ]]
26
+ ],
27
+ format: 'hex',
28
+ alpha: true,
29
+ defaultColor: '#000000',
30
+ swatches: [],
31
+ events: [
32
+ ['ui.input.change', 'onChange'],
33
+ ['ui.preview.click', 'toggle'],
34
+ ['ui.gradient.mousedown', 'startPickingColor'],
35
+ ['ui.gradient.touchstart', 'startPickingColor'],
36
+ ['ui.hue-slider.input', 'setHue'],
37
+ ['ui.alpha-slider.input', 'setAlpha']
38
+ ]
39
+ }
40
+
41
+ constructor (options) {
42
+ super(options)
43
+ this.setup()
44
+ }
45
+
46
+ setup () {
47
+ // Initialize internal state
48
+ this.currentColor = {
49
+ r: 0,
50
+ g: 0,
51
+ b: 0,
52
+ h: 0,
53
+ s: 0,
54
+ v: 0,
55
+ a: 1
56
+ }
57
+
58
+ this.ctx = document.createElement('canvas').getContext('2d')
59
+ this.dragging = false
60
+ this.visible = false
61
+
62
+ // Set up DOM elements
63
+ this.setAttributes()
64
+ this.setupPanelElements()
65
+ this.setupSwatches()
66
+
67
+ // Initialize slider values
68
+ this.ui['hue-slider'].value = 0
69
+ this.ui['alpha-slider'].value = 100
70
+
71
+ // Set initial color
72
+ const defaultColor = this.options.defaultColor || '#000000'
73
+ this.setColorFromStr(defaultColor)
74
+
75
+ // Add document-level event listeners for drag handling
76
+ document.addEventListener('mousemove', this.onDrag.bind(this))
77
+ document.addEventListener('mouseup', this.stopPickingColor.bind(this))
78
+ document.addEventListener('touchmove', this.onDrag.bind(this))
79
+ document.addEventListener('touchend', this.stopPickingColor.bind(this))
80
+ }
81
+
82
+ setAttributes () {
83
+ const { name, value, disabled } = this.options
84
+
85
+ if (name) this.ui.input.setAttribute('name', name)
86
+ if (value) this.setColorFromStr(value)
87
+ if (disabled) this.disable()
88
+ }
89
+
90
+ setupPanelElements () {
91
+ // Set up hue slider
92
+ this.ui['hue-slider'].setAttribute('aria-label', 'Hue')
93
+ this.ui['alpha-slider'].setAttribute('aria-label', 'Opacity')
94
+
95
+ // Set up color marker
96
+ this.marker = document.createElement('div')
97
+ this.marker.className = 'marker'
98
+ this.ui.gradient.appendChild(this.marker)
99
+ }
100
+
101
+ setupSwatches () {
102
+ if (!Array.isArray(this.options.swatches)) return
103
+
104
+ this.options.swatches.forEach(color => {
105
+ const swatch = document.createElement('button')
106
+ swatch.className = 'swatch'
107
+ swatch.style.backgroundColor = color
108
+ swatch.addEventListener('click', () => this.setColorFromStr(color))
109
+ this.ui.swatches.appendChild(swatch)
110
+ })
111
+ }
112
+
113
+ startPickingColor (event) {
114
+ this.dragging = true
115
+ this.onDrag(event)
116
+ }
117
+
118
+ stopPickingColor () {
119
+ this.dragging = false
120
+ }
121
+
122
+ onDrag (event) {
123
+ if (!this.dragging) return
124
+
125
+ event.preventDefault()
126
+
127
+ const rect = this.ui.gradient.getBoundingClientRect()
128
+ const x = (event.touches ? event.touches[0].clientX : event.clientX) - rect.left
129
+ const y = (event.touches ? event.touches[0].clientY : event.clientY) - rect.top
130
+
131
+ this.setColorAtPosition(
132
+ Math.max(0, Math.min(x, rect.width)),
133
+ Math.max(0, Math.min(y, rect.height))
134
+ )
135
+ }
136
+
137
+ setColorAtPosition (x, y) {
138
+ const width = this.ui.gradient.offsetWidth
139
+ const height = this.ui.gradient.offsetHeight
140
+
141
+ const hsva = {
142
+ h: this.ui['hue-slider'].value * 1,
143
+ s: (x / width) * 100,
144
+ v: 100 - ((y / height) * 100),
145
+ a: this.ui['alpha-slider'].value / 100
146
+ }
147
+
148
+ this.marker.style.left = `${x}px`
149
+ this.marker.style.top = `${y}px`
150
+
151
+ const rgba = this.HSVAtoRGBA(hsva)
152
+ this.updateColor(rgba, hsva)
153
+ this.emit('change', this.getValue())
154
+ }
155
+
156
+ setHue () {
157
+ if (!this.ui['hue-slider']) return
158
+
159
+ const hue = parseFloat(this.ui['hue-slider'].value || 0)
160
+
161
+ if (this.ui['hue-marker']) {
162
+ this.ui['hue-marker'].style.left = `${(hue / 360) * 100}%`
163
+ }
164
+
165
+ if (this.ui.gradient) {
166
+ this.ui.gradient.style.color = `hsl(${hue}, 100%, 50%)`
167
+ }
168
+
169
+ // Update color based on current marker position
170
+ if (this.marker && this.ui.gradient) {
171
+ const rect = this.ui.gradient.getBoundingClientRect()
172
+ const x = parseFloat(this.marker.style.left || '0')
173
+ const y = parseFloat(this.marker.style.top || '0')
174
+ this.setColorAtPosition(x, y)
175
+ }
176
+ }
177
+
178
+ setAlpha () {
179
+ if (!this.ui['alpha-slider']) return
180
+
181
+ const alpha = parseFloat(this.ui['alpha-slider'].value || 100) / 100
182
+
183
+ if (this.ui['alpha-marker']) {
184
+ this.ui['alpha-marker'].style.left = `${alpha * 100}%`
185
+ }
186
+
187
+ this.currentColor.a = alpha
188
+ this.updateColor()
189
+ this.emit('change', this.getValue())
190
+ }
191
+
192
+ setColorFromStr (str, silent = false) {
193
+ const rgba = this.strToRGBA(str)
194
+ const hsva = this.RGBAtoHSVA(rgba)
195
+
196
+ this.updateColor(rgba, hsva)
197
+
198
+ // Update UI elements
199
+ this.ui['hue-slider'].value = hsva.h
200
+ this.ui['alpha-slider'].value = hsva.a * 100
201
+ this.setHue()
202
+
203
+ const rect = this.ui.gradient.getBoundingClientRect()
204
+ const x = (hsva.s / 100) * rect.width
205
+ const y = (1 - hsva.v / 100) * rect.height
206
+ this.marker.style.left = `${x}px`
207
+ this.marker.style.top = `${y}px`
208
+ }
209
+
210
+ updateColor (rgba = {}, hsva = {}) {
211
+ Object.assign(this.currentColor, rgba, hsva)
212
+
213
+ const hex = this.RGBAToHex(this.currentColor)
214
+ const color = this.getColorString()
215
+
216
+ this.ui.preview.style.backgroundColor = color
217
+ this.ui.input.value = color
218
+ }
219
+
220
+ getValue () {
221
+ return this.ui.input.value
222
+ }
223
+
224
+ setValue (value, silent = false) {
225
+ if (this.getValue() !== value) {
226
+ this.setColorFromStr(value, silent)
227
+ }
228
+ }
229
+
230
+ // Color conversion utilities
231
+ HSVAtoRGBA (hsva) {
232
+ const saturation = hsva.s / 100
233
+ const value = hsva.v / 100
234
+ let chroma = saturation * value
235
+ const hueBy60 = hsva.h / 60
236
+ let x = chroma * (1 - Math.abs(hueBy60 % 2 - 1))
237
+ const m = value - chroma
238
+
239
+ chroma = (chroma + m)
240
+ x = (x + m)
241
+
242
+ const index = Math.floor(hueBy60) % 6
243
+ const red = [chroma, x, m, m, x, chroma][index]
244
+ const green = [x, chroma, chroma, x, m, m][index]
245
+ const blue = [m, m, x, chroma, chroma, x][index]
246
+
247
+ return {
248
+ r: Math.round(red * 255),
249
+ g: Math.round(green * 255),
250
+ b: Math.round(blue * 255),
251
+ a: hsva.a
252
+ }
253
+ }
254
+
255
+ RGBAtoHSVA (rgba) {
256
+ const red = rgba.r / 255
257
+ const green = rgba.g / 255
258
+ const blue = rgba.b / 255
259
+ const xmax = Math.max(red, green, blue)
260
+ const xmin = Math.min(red, green, blue)
261
+ const chroma = xmax - xmin
262
+ const value = xmax
263
+ let hue = 0
264
+ let saturation = 0
265
+
266
+ if (chroma) {
267
+ if (xmax === red) hue = ((green - blue) / chroma)
268
+ if (xmax === green) hue = 2 + (blue - red) / chroma
269
+ if (xmax === blue) hue = 4 + (red - green) / chroma
270
+ if (xmax) saturation = chroma / xmax
271
+ }
272
+
273
+ hue = Math.floor(hue * 60)
274
+
275
+ return {
276
+ h: hue < 0 ? hue + 360 : hue,
277
+ s: Math.round(saturation * 100),
278
+ v: Math.round(value * 100),
279
+ a: rgba.a
280
+ }
281
+ }
282
+
283
+ strToRGBA (str) {
284
+ const regex = /^((rgba)|rgb)[\D]+([\d.]+)[\D]+([\d.]+)[\D]+([\d.]+)[\D]*?([\d.]+|$)/i
285
+ let match, rgba
286
+
287
+ // Default to black for invalid color strings
288
+ this.ctx.fillStyle = '#000'
289
+
290
+ // Use canvas to convert the string to a valid color string
291
+ this.ctx.fillStyle = str
292
+ match = regex.exec(this.ctx.fillStyle)
293
+
294
+ if (match) {
295
+ rgba = {
296
+ r: match[3] * 1,
297
+ g: match[4] * 1,
298
+ b: match[5] * 1,
299
+ a: match[6] * 1
300
+ }
301
+ } else {
302
+ match = this.ctx.fillStyle.replace('#', '').match(/.{2}/g).map(h => parseInt(h, 16))
303
+ rgba = {
304
+ r: match[0],
305
+ g: match[1],
306
+ b: match[2],
307
+ a: 1
308
+ }
309
+ }
310
+
311
+ return rgba
312
+ }
313
+
314
+ RGBAToHex (rgba) {
315
+ let R = rgba.r.toString(16)
316
+ let G = rgba.g.toString(16)
317
+ let B = rgba.b.toString(16)
318
+ let A = ''
319
+
320
+ if (rgba.r < 16) R = '0' + R
321
+ if (rgba.g < 16) G = '0' + G
322
+ if (rgba.b < 16) B = '0' + B
323
+
324
+ if (this.options.alpha && rgba.a < 1) {
325
+ const alpha = rgba.a * 255 | 0
326
+ A = alpha.toString(16)
327
+ if (alpha < 16) A = '0' + A
328
+ }
329
+
330
+ return '#' + R + G + B + A
331
+ }
332
+
333
+ getColorString () {
334
+ switch (this.options.format) {
335
+ case 'hex':
336
+ return this.RGBAToHex(this.currentColor)
337
+ case 'rgb':
338
+ const { r, g, b, a } = this.currentColor
339
+ return a < 1
340
+ ? `rgba(${r}, ${g}, ${b}, ${a})`
341
+ : `rgb(${r}, ${g}, ${b})`
342
+ default:
343
+ return this.RGBAToHex(this.currentColor)
344
+ }
345
+ }
346
+
347
+ destroy () {
348
+ document.removeEventListener('mousemove', this.onDrag)
349
+ document.removeEventListener('mouseup', this.stopPickingColor)
350
+ document.removeEventListener('touchmove', this.onDrag)
351
+ document.removeEventListener('touchend', this.stopPickingColor)
352
+ super.destroy()
353
+ }
354
+ }
355
+
356
+ export default ColorPicker
@@ -1,7 +1,6 @@
1
1
  export default {
2
2
  cancel (ev) {
3
- // console.log('cancel', this.info, this.mode)
4
- ev.preventDefault()
3
+ if (ev) ev.preventDefault()
5
4
 
6
5
  this.render(this.info)
7
6
 
@@ -6,17 +6,8 @@ export default {
6
6
 
7
7
  this.disableControls()
8
8
 
9
- for (const field in this.field) {
10
- if (this.field.hasOwnProperty(field)) {
11
- this.bindControl(this.field[field])
12
- }
13
- }
14
-
15
- for (const file in this.file) {
16
- if (this.file.hasOwnProperty(file)) {
17
- this.bindControl(this.file[file])
18
- }
19
- }
9
+ Object.keys(this.field).forEach(field => this.bindControl(this.field[field]))
10
+ Object.keys(this.file).forEach(file => this.bindControl(this.file[file]))
20
11
  },
21
12
 
22
13
  setMode (mode) {
@@ -39,7 +30,7 @@ export default {
39
30
  },
40
31
 
41
32
  changeMode (mode) {
42
- // console.log('changeMode', mode)
33
+ // console.log('changeMode', mode, this.options.class)
43
34
 
44
35
  if (mode === 'update' && this.enableControls) {
45
36
  this.enableControls()
@@ -51,19 +42,18 @@ export default {
51
42
  },
52
43
 
53
44
  bindControl (control) {
54
- // console.log('bindControl')
45
+ // console.log('bindControl', control.class)
55
46
  if (control && control.on) {
56
47
  control.on('change', () => {
57
- // console.log('change', control, this.mode)
58
- if (this.mode === 'read') {
59
- this.setMode('update')
60
- }
48
+ if (this.isModified) this.isModified()
49
+ this.setMode('update')
50
+ this.emit('changed', this.options.class)
51
+ console.log('isModified', this.isModified)
61
52
  })
62
53
  }
63
54
  },
64
55
 
65
56
  enableControls () {
66
- // console.log('enableControls')
67
57
  const controls = this.options.controls || ['submit', 'cancel']
68
58
 
69
59
  if (controls === null) return
@@ -73,6 +63,8 @@ export default {
73
63
  this.ui[controls[i]].enable()
74
64
  }
75
65
  }
66
+
67
+ this.emit('enableControls')
76
68
  },
77
69
 
78
70
  disableControls () {