@operato/input 1.13.14 → 2.0.0-alpha.11

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 = [
@@ -59,46 +85,45 @@ export class OxInputBarcode extends OxFormField {
59
85
  #scan-button[hidden] {
60
86
  display: none;
61
87
  }
62
-
63
- ox-popup {
64
- position: fixed;
65
-
66
- width: 80vw;
67
- height: 80vh;
68
- transform: translate(10%, 10%);
69
- }
70
-
71
- video {
72
- width: 100%;
73
- height: 100%;
74
- }
75
-
76
- @media screen and (max-width: 460px) {
77
- ox-popup {
78
- position: fixed;
79
- left: 0;
80
- top: 0;
81
- width: 100vw;
82
- height: 100vh;
83
- height: 100dvh;
84
- transform: translate(0%, 0%);
85
- }
86
- }
87
88
  `
88
89
  ]
89
90
 
91
+ /**
92
+ * Indicates whether barcode scanning is enabled.
93
+ * @property {Boolean} scannable
94
+ */
90
95
  @property({ type: Boolean }) scannable?: boolean
96
+
97
+ /**
98
+ * If true, the "Enter" key press event is not fired after scanning a barcode.
99
+ * @property {Boolean} withoutEnter
100
+ */
91
101
  @property({ attribute: 'without-enter', type: Boolean }) withoutEnter?: boolean
102
+
103
+ /**
104
+ * The value of the input field.
105
+ * @property {String} declare value
106
+ */
92
107
  @property({ type: String }) declare value?: string
108
+
109
+ /**
110
+ * If true, only English characters are allowed in the input field.
111
+ * @property {Boolean} englishOnly
112
+ */
93
113
  @property({ attribute: 'english-only', type: Boolean }) englishOnly?: boolean
114
+
115
+ /**
116
+ * If true, the input field is automatically selected after a change event.
117
+ * @property {Boolean} selectAfterChange
118
+ */
94
119
  @property({ attribute: 'select-after-change', type: Boolean }) selectAfterChange?: boolean
95
120
 
96
121
  @state() stream?: MediaStream
97
- @state() reader?: BrowserMultiFormatReader
98
122
 
99
123
  @query('input') input!: HTMLInputElement
100
- @query('ox-popup') popup!: OxPopup
101
- @query('video') video!: HTMLVideoElement
124
+
125
+ private popup: OxPopup | null = null
126
+ private video: HTMLVideoElement | null = null
102
127
 
103
128
  connectedCallback() {
104
129
  super.connectedCallback()
@@ -108,7 +133,7 @@ export class OxInputBarcode extends OxFormField {
108
133
  if (navigator.mediaDevices) {
109
134
  ;(async () => {
110
135
  try {
111
- var stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } })
136
+ var stream = await navigator.mediaDevices.getUserMedia({ audio: false, video: { facingMode: 'environment' } })
112
137
  if (stream) {
113
138
  stream.getTracks().forEach(track => track.stop())
114
139
  this.scannable = true
@@ -143,14 +168,6 @@ export class OxInputBarcode extends OxFormField {
143
168
  }}
144
169
  ?disabled=${this.disabled}
145
170
  ></button>
146
-
147
- <ox-popup
148
- @focusout=${() => {
149
- this.stopScan()
150
- }}
151
- >
152
- <video></video>
153
- </ox-popup>
154
171
  `
155
172
  }
156
173
 
@@ -201,27 +218,74 @@ export class OxInputBarcode extends OxFormField {
201
218
 
202
219
  async scan(e: MouseEvent) {
203
220
  try {
204
- this.popup.open({})
221
+ if (this.popup) {
222
+ this.stopScan()
223
+ }
224
+
225
+ this.popup = OxPopup.open({
226
+ template: html`
227
+ <video></video>
228
+ <mwc-icon
229
+ style="position: fixed; right: 0; top: 0; color: red; tabindex: 0"
230
+ @click=${() => {
231
+ this.stopScan()
232
+ }}
233
+ >close</mwc-icon
234
+ >
235
+ `,
236
+ width: '100vw',
237
+ height: '100dvh'
238
+ })
205
239
 
206
- /* template.video 생성된 후에 접근하기 위해서, 한 프레임을 강제로 건너뛴다. */
207
- await this.updateComplete
240
+ this.video! = this.popup.querySelector('video') as HTMLVideoElement
208
241
 
209
- var constraints = { video: { facingMode: 'environment' } } /* backside camera first */
242
+ var constraints = { audio: false, video: { facingMode: 'environment' } } /* backside camera first */
210
243
  this.stream = await navigator.mediaDevices.getUserMedia(constraints)
211
244
 
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' }))
245
+ this.video.srcObject = this.stream
246
+ this.video.play()
247
+
248
+ this.video.onloadedmetadata = async e => {
249
+ var canvas = new OffscreenCanvas(
250
+ this.video!.videoWidth || this.video!.width,
251
+ this.video!.videoHeight || this.video!.height
252
+ )
253
+
254
+ var context = canvas.getContext('2d', {
255
+ willReadFrequently: true
256
+ })
257
+
258
+ const detect = async () => {
259
+ try {
260
+ if (!this.stream?.active) {
261
+ return
262
+ }
263
+
264
+ context!.drawImage(this.video!, 0, 0, canvas.width, canvas.height)
265
+ const imageData = context!.getImageData(0, 0, canvas.width, canvas.height)
266
+ const symbols = await scanImageData(imageData)
267
+ const result = symbols[0]?.decode()
268
+
269
+ if (result) {
270
+ this.stopScan()
271
+
272
+ var input = this.input
273
+ input.focus()
274
+ this.value = input.value = String(result)
275
+
276
+ if (!this.withoutEnter) {
277
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }))
278
+ }
279
+ } else {
280
+ requestAnimationFrame(async () => await detect())
281
+ }
282
+ } catch (e) {
283
+ console.warn(e)
284
+ this.stopScan()
285
+ }
221
286
  }
222
- } else {
223
- /* popup이 비동기 진행 중에 close된 경우라면, stopScan()을 처리하지 못하게 되므로, 다시한번 clear해준다. */
224
- this.stopScan()
287
+
288
+ await detect()
225
289
  }
226
290
  } catch (err) {
227
291
  /*
@@ -229,20 +293,20 @@ export class OxInputBarcode extends OxFormField {
229
293
  * 2. 뒤로가기 등으로 popup이 종료된 경우에도 NotFoundException: Video stream has ended before any code could be detected. 이 발생한다.
230
294
  */
231
295
  console.warn(err)
232
- } finally {
233
- this.popup.close()
234
-
235
- this.stopScan()
236
296
  }
237
297
  }
238
298
 
239
299
  stopScan() {
240
- this.video?.pause()
300
+ if (this.video) {
301
+ this.video.pause()
302
+ this.video.srcObject = null
303
+ }
241
304
 
242
- this.stream?.getTracks().forEach(track => track.stop())
243
- this.reader?.reset()
305
+ if (this.popup) {
306
+ this.popup.close()
307
+ this.popup = null
308
+ }
244
309
 
245
- delete this.stream
246
- delete this.reader
310
+ this.stream?.getTracks().forEach(track => track.stop())
247
311
  }
248
312
  }
@@ -90,7 +90,7 @@ export class OxInputData extends OxFormField {
90
90
  <mwc-icon @click=${() => this._clearData()} title="delete">delete_forever</mwc-icon>
91
91
  </div>
92
92
 
93
- <ox-input-code .value=${this._getStringData(this.value)} language="javascript" editor ?disabled=${this.disabled}>
93
+ <ox-input-code .value=${this._getData(this.value)} language="javascript" editor ?disabled=${this.disabled}>
94
94
  </ox-input-code>
95
95
  `
96
96
  }
@@ -98,14 +98,13 @@ export class OxInputData extends OxFormField {
98
98
  firstUpdated() {
99
99
  this.renderRoot.addEventListener('change', e => {
100
100
  e.stopPropagation()
101
-
102
101
  const target = e.target as OxInputCode
103
102
  if (target.hasAttribute('editor')) {
104
- if (this.value === undefined && target.value == '') {
105
- return
106
- }
107
103
  this.value = target.value
108
104
  }
105
+
106
+ const type = this.renderRoot.querySelector('input[name=data-type]:checked')?.getAttribute('data-value')
107
+ this._setDataType(type)
109
108
  })
110
109
  }
111
110
 
@@ -116,7 +115,7 @@ export class OxInputData extends OxFormField {
116
115
  try {
117
116
  switch (type) {
118
117
  case 'string':
119
- this.value = this._getStringData(value)
118
+ this.value = this._getData(value)
120
119
  break
121
120
  case 'number':
122
121
  if (!isNaN(value)) {
@@ -141,17 +140,8 @@ export class OxInputData extends OxFormField {
141
140
  this._onAfterValueChange()
142
141
  }
143
142
 
144
- _getStringData(data: any) {
145
- const type = typeof data
146
-
147
- switch (type) {
148
- case 'object':
149
- return JSON.stringify(data, null, 1)
150
- case 'undefined':
151
- return ''
152
- default:
153
- return String(data) || ''
154
- }
143
+ _getData(data: any) {
144
+ return typeof data !== 'object' ? data || '' : JSON.stringify(data, null, 1)
155
145
  }
156
146
 
157
147
  async _onAfterValueChange() {
@@ -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;