@operato/input 2.0.0-alpha.3 → 2.0.0-alpha.8

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.
@@ -6,14 +6,40 @@ import '@operato/popup/ox-popup.js'
6
6
 
7
7
  import { css, html } from 'lit'
8
8
  import { customElement, property, query, state } from 'lit/decorators.js'
9
+ import { scanImageData } from '@undecaf/zbar-wasm'
9
10
 
10
11
  import { OxPopup } from '@operato/popup'
11
- import { BrowserMultiFormatReader } from '@zxing/library'
12
12
 
13
13
  import { OxFormField } from './ox-form-field.js'
14
14
 
15
15
  const barcodeIcon = `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAYBAMAAAAfR1CMAAADKGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDUgNzkuMTYzNDk5LCAyMDE4LzA4LzEzLTE2OjQwOjIyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpFNjM4RURDQkQ1OUExMUU5QkExMkQ4NUY3NkMxNzBFOSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpFNjM4RURDQ0Q1OUExMUU5QkExMkQ4NUY3NkMxNzBFOSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkU2MzhFREM5RDU5QTExRTlCQTEyRDg1Rjc2QzE3MEU5IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkU2MzhFRENBRDU5QTExRTlCQTEyRDg1Rjc2QzE3MEU5Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+55pr/QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAAkUExURQAAAEdwTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEus/7UCWQwAAAALdFJOU9YAg3wKBFBDSz9PnvQNDgAAAE9JREFUGNNjEEQFDKLJSnCOklkgg9QUJFn3RgZhRyS+iCGDEIp2RSBfQICRkRGIgTSQL4jCF6ScvxsZYOFT2T50/6D7Fz080MMLPTzRwhsAHVspfelur08AAAAASUVORK5CYII=`
16
16
 
17
+ /**
18
+ * Custom input component for barcode scanning.
19
+ *
20
+ * This component provides a text input field and a barcode scanning button. Users can input text
21
+ * manually or scan barcodes using the device camera. Supported barcode formats include:
22
+ *
23
+ * - Code-39
24
+ * - Code-93
25
+ * - Code-128
26
+ * - Codabar
27
+ * - Databar/Expanded
28
+ * - EAN/GTIN-5/8/13
29
+ * - ISBN-10/13
30
+ * - ISBN-13+2
31
+ * - ISBN-13+5
32
+ * - ITF (Interleaved 2 of 5)
33
+ * - QR Code
34
+ * - UPC-A/E
35
+ *
36
+ * @fires CustomEvent#change - Dispatched when the input value changes.
37
+ * @fires KeyboardEvent#keydown - Dispatched when the Enter key is pressed (if not withoutEnter).
38
+ *
39
+ * @cssprop {String} --barcodescan-input-button-icon - Icon for the barcode scanning button.
40
+ *
41
+ * @customElement
42
+ */
17
43
  @customElement('ox-input-barcode')
18
44
  export class OxInputBarcode extends OxFormField {
19
45
  static styles = [
@@ -87,14 +113,37 @@ export class OxInputBarcode extends OxFormField {
87
113
  `
88
114
  ]
89
115
 
116
+ /**
117
+ * Indicates whether barcode scanning is enabled.
118
+ * @property {Boolean} scannable
119
+ */
90
120
  @property({ type: Boolean }) scannable?: boolean
121
+
122
+ /**
123
+ * If true, the "Enter" key press event is not fired after scanning a barcode.
124
+ * @property {Boolean} withoutEnter
125
+ */
91
126
  @property({ attribute: 'without-enter', type: Boolean }) withoutEnter?: boolean
127
+
128
+ /**
129
+ * The value of the input field.
130
+ * @property {String} declare value
131
+ */
92
132
  @property({ type: String }) declare value?: string
133
+
134
+ /**
135
+ * If true, only English characters are allowed in the input field.
136
+ * @property {Boolean} englishOnly
137
+ */
93
138
  @property({ attribute: 'english-only', type: Boolean }) englishOnly?: boolean
139
+
140
+ /**
141
+ * If true, the input field is automatically selected after a change event.
142
+ * @property {Boolean} selectAfterChange
143
+ */
94
144
  @property({ attribute: 'select-after-change', type: Boolean }) selectAfterChange?: boolean
95
145
 
96
146
  @state() stream?: MediaStream
97
- @state() reader?: BrowserMultiFormatReader
98
147
 
99
148
  @query('input') input!: HTMLInputElement
100
149
  @query('ox-popup') popup!: OxPopup
@@ -108,7 +157,7 @@ export class OxInputBarcode extends OxFormField {
108
157
  if (navigator.mediaDevices) {
109
158
  ;(async () => {
110
159
  try {
111
- var stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } })
160
+ var stream = await navigator.mediaDevices.getUserMedia({ audio: false, video: { facingMode: 'environment' } })
112
161
  if (stream) {
113
162
  stream.getTracks().forEach(track => track.stop())
114
163
  this.scannable = true
@@ -206,22 +255,52 @@ export class OxInputBarcode extends OxFormField {
206
255
  /* template.video가 생성된 후에 접근하기 위해서, 한 프레임을 강제로 건너뛴다. */
207
256
  await this.updateComplete
208
257
 
209
- var constraints = { video: { facingMode: 'environment' } } /* backside camera first */
258
+ var constraints = { audio: false, video: { facingMode: 'environment' } } /* backside camera first */
210
259
  this.stream = await navigator.mediaDevices.getUserMedia(constraints)
211
-
212
- this.reader = new BrowserMultiFormatReader()
213
- if (getComputedStyle(this.popup).display !== 'none' /* popup not hidden */ && this.stream) {
214
- var result = await this.reader.decodeOnceFromStream(this.stream, this.video)
215
- var input = this.input
216
- input.focus()
217
- this.value = input.value = String(result)
218
-
219
- if (!this.withoutEnter) {
220
- input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }))
260
+ this.video.srcObject = this.stream
261
+ this.video.play()
262
+
263
+ this.video.onloadedmetadata = async e => {
264
+ var canvas = new OffscreenCanvas(
265
+ this.video.videoWidth || this.video.width,
266
+ this.video.videoHeight || this.video.height
267
+ )
268
+
269
+ var context = canvas.getContext('2d', {
270
+ willReadFrequently: true
271
+ })
272
+
273
+ const detect = async () => {
274
+ try {
275
+ if (!this.stream?.active) {
276
+ return
277
+ }
278
+
279
+ context!.drawImage(this.video, 0, 0, canvas.width, canvas.height)
280
+ const imageData = context!.getImageData(0, 0, canvas.width, canvas.height)
281
+ const symbols = await scanImageData(imageData)
282
+ const result = symbols[0]?.decode()
283
+
284
+ if (result) {
285
+ this.stopScan()
286
+
287
+ var input = this.input
288
+ input.focus()
289
+ this.value = input.value = String(result)
290
+
291
+ if (!this.withoutEnter) {
292
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }))
293
+ }
294
+ } else {
295
+ requestAnimationFrame(async () => await detect())
296
+ }
297
+ } catch (e) {
298
+ console.warn(e)
299
+ this.stopScan()
300
+ }
221
301
  }
222
- } else {
223
- /* popup이 비동기 진행 중에 close된 경우라면, stopScan()을 처리하지 못하게 되므로, 다시한번 clear해준다. */
224
- this.stopScan()
302
+
303
+ await detect()
225
304
  }
226
305
  } catch (err) {
227
306
  /*
@@ -229,20 +308,15 @@ export class OxInputBarcode extends OxFormField {
229
308
  * 2. 뒤로가기 등으로 popup이 종료된 경우에도 NotFoundException: Video stream has ended before any code could be detected. 이 발생한다.
230
309
  */
231
310
  console.warn(err)
232
- } finally {
233
- this.popup.close()
234
-
235
- this.stopScan()
236
311
  }
237
312
  }
238
313
 
239
314
  stopScan() {
240
- this.video?.pause()
315
+ if (this.video) {
316
+ this.video.pause()
317
+ this.video.srcObject = null
318
+ }
241
319
 
242
320
  this.stream?.getTracks().forEach(track => track.stop())
243
- this.reader?.reset()
244
-
245
- delete this.stream
246
- delete this.reader
247
321
  }
248
322
  }
@@ -131,11 +131,16 @@ export class OxInputMassFraction extends OxFormField {
131
131
  display: block;
132
132
  text-align: center;
133
133
  }
134
+
135
+ [right-end] {
136
+ margin-left: auto;
137
+ }
134
138
  `
135
139
  ]
136
140
 
137
141
  @property({ type: Object }) defaultValue: MassFraction = {}
138
142
  @property({ type: Object }) value: MassFraction = {}
143
+ @property({ type: Boolean, attribute: true }) composable: boolean = false
139
144
 
140
145
  @queryAll('[data-record]') records!: NodeListOf<HTMLElement>
141
146
 
@@ -168,14 +173,18 @@ export class OxInputMassFraction extends OxFormField {
168
173
  list="value-template"
169
174
  ?disabled=${this.disabled}
170
175
  />
171
- <button
172
- class="record-action"
173
- @click=${(e: MouseEvent) => this._delete(e)}
174
- tabindex="-1"
175
- ?disabled=${this.disabled}
176
- >
177
- <mwc-icon>remove</mwc-icon>
178
- </button>
176
+ ${this.composable
177
+ ? html`
178
+ <button
179
+ class="record-action"
180
+ @click=${(e: MouseEvent) => this._delete(e)}
181
+ tabindex="-1"
182
+ ?disabled=${this.disabled}
183
+ >
184
+ <mwc-icon>remove</mwc-icon>
185
+ </button>
186
+ `
187
+ : nothing}
179
188
  <button
180
189
  class="record-action"
181
190
  @click=${(e: MouseEvent) => this._up(e)}
@@ -202,36 +211,41 @@ export class OxInputMassFraction extends OxFormField {
202
211
  ? nothing
203
212
  : html`
204
213
  <div data-record-new>
205
- <ox-select
206
- data-key
207
- placeholder="Fluid"
208
- .value=${live('')}
209
- @change=${(e: Event) => {
210
- e.stopPropagation()
211
- }}
212
- >
213
- <ox-popup-list with-search> ${this.options} </ox-popup-list>
214
- </ox-select>
215
-
216
- <input
217
- type="number"
218
- data-value
219
- placeholder="proportion"
220
- min="0"
221
- max="1"
222
- step="0.01"
223
- value=""
224
- list="value-template"
225
- />
226
- <button class="record-action" @click=${(e: MouseEvent) => this._add()} tabindex="-1">
227
- <mwc-icon>add</mwc-icon>
228
- </button>
214
+ ${this.composable
215
+ ? html`
216
+ <ox-select
217
+ data-key
218
+ placeholder="Fluid"
219
+ .value=${live('')}
220
+ @change=${(e: Event) => {
221
+ e.stopPropagation()
222
+ }}
223
+ >
224
+ <ox-popup-list with-search> ${this.options} </ox-popup-list>
225
+ </ox-select>
226
+
227
+ <input
228
+ type="number"
229
+ data-value
230
+ placeholder="proportion"
231
+ min="0"
232
+ max="1"
233
+ step="0.01"
234
+ value=""
235
+ list="value-template"
236
+ />
237
+ <button class="record-action" @click=${(e: MouseEvent) => this._add()} tabindex="-1">
238
+ <mwc-icon>add</mwc-icon>
239
+ </button>
240
+ `
241
+ : nothing}
229
242
  <button
230
243
  title="fill with the values suggested"
231
244
  @click=${() => {
232
245
  this.value = { ...this.defaultValue }
233
246
  this.dispatchChangeEvent()
234
247
  }}
248
+ right-end
235
249
  >
236
250
  <mwc-icon>settings_suggest</mwc-icon>
237
251
  </button>
@@ -43,6 +43,10 @@ const Template: Story<ArgTypes> = ({
43
43
  <link href="/themes/app-theme.css" rel="stylesheet" />
44
44
  <link href="https://fonts.googleapis.com/css?family=Material+Icons&display=block" rel="stylesheet" />
45
45
  <style>
46
+ #root {
47
+ height: 500px;
48
+ }
49
+
46
50
  ox-input-barcode {
47
51
  font-size: 80px;
48
52
  --input-font: initial;
@@ -9,6 +9,7 @@ export default {
9
9
  name: { control: 'text' },
10
10
  value: { control: 'object' },
11
11
  defaultValue: { control: 'object' },
12
+ composable: { control: 'boolean' },
12
13
  disabled: { control: 'boolean' }
13
14
  }
14
15
  }
@@ -23,6 +24,7 @@ interface ArgTypes {
23
24
  name?: string
24
25
  value?: object
25
26
  defaultValue?: object
27
+ composable?: boolean
26
28
  disabled?: boolean
27
29
  }
28
30
 
@@ -30,6 +32,7 @@ const Template: Story<ArgTypes> = ({
30
32
  name = 'mass-fraction',
31
33
  value = {},
32
34
  defaultValue = {},
35
+ composable = true,
33
36
  disabled
34
37
  }: ArgTypes) => html`
35
38
  <link href="/themes/app-theme.css" rel="stylesheet" />
@@ -46,6 +49,7 @@ const Template: Story<ArgTypes> = ({
46
49
  name=${name}
47
50
  .value=${value}
48
51
  .defaultValue=${defaultValue}
52
+ ?composable=${composable}
49
53
  ?disabled=${disabled}
50
54
  >
51
55
  </ox-input-mass-fraction>
@@ -54,7 +58,12 @@ const Template: Story<ArgTypes> = ({
54
58
  export const Regular = Template.bind({})
55
59
  Regular.args = {
56
60
  name: 'mass-fraction',
57
- value: {},
61
+ value: {
62
+ H2O: 0.8,
63
+ N2O: 0.1,
64
+ CO2: 0.1
65
+ },
66
+ composable: true,
58
67
  defaultValue: {
59
68
  H2O: 0.8,
60
69
  N2O: 0.1,