@operato/input 2.0.0-alpha.2 → 2.0.0-alpha.28

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 (33) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/dist/src/ox-input-barcode.d.ts +48 -5
  3. package/dist/src/ox-input-barcode.js +96 -69
  4. package/dist/src/ox-input-barcode.js.map +1 -1
  5. package/dist/src/ox-input-data.d.ts +3 -4
  6. package/dist/src/ox-input-data.js +86 -34
  7. package/dist/src/ox-input-data.js.map +1 -1
  8. package/dist/src/ox-input-mass-fraction.d.ts +1 -0
  9. package/dist/src/ox-input-mass-fraction.js +48 -31
  10. package/dist/src/ox-input-mass-fraction.js.map +1 -1
  11. package/dist/src/ox-input-unit-number.d.ts +2 -1
  12. package/dist/src/ox-input-unit-number.js +24 -10
  13. package/dist/src/ox-input-unit-number.js.map +1 -1
  14. package/dist/stories/ox-input-barcode.stories.js +4 -0
  15. package/dist/stories/ox-input-barcode.stories.js.map +1 -1
  16. package/dist/stories/ox-input-data.stories.d.ts +33 -0
  17. package/dist/stories/ox-input-data.stories.js +40 -0
  18. package/dist/stories/ox-input-data.stories.js.map +1 -0
  19. package/dist/stories/ox-input-mass-fraction.stories.d.ts +4 -0
  20. package/dist/stories/ox-input-mass-fraction.stories.js +9 -2
  21. package/dist/stories/ox-input-mass-fraction.stories.js.map +1 -1
  22. package/dist/stories/ox-input-unit.stories.js +1 -0
  23. package/dist/stories/ox-input-unit.stories.js.map +1 -1
  24. package/dist/tsconfig.tsbuildinfo +1 -1
  25. package/package.json +17 -17
  26. package/src/ox-input-barcode.ts +127 -63
  27. package/src/ox-input-data.ts +95 -36
  28. package/src/ox-input-mass-fraction.ts +46 -32
  29. package/src/ox-input-unit-number.ts +17 -2
  30. package/stories/ox-input-barcode.stories.ts +4 -0
  31. package/stories/ox-input-data.stories.ts +55 -0
  32. package/stories/ox-input-mass-fraction.stories.ts +10 -1
  33. package/stories/ox-input-unit.stories.ts +1 -0
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@operato/input",
3
3
  "description": "Webcomponents for input following open-wc recommendations",
4
4
  "author": "heartyoh@hatiolab.com",
5
- "version": "2.0.0-alpha.2",
5
+ "version": "2.0.0-alpha.28",
6
6
  "main": "dist/src/index.js",
7
7
  "module": "dist/src/index.js",
8
8
  "license": "MIT",
@@ -192,36 +192,36 @@
192
192
  "@lit/localize": "^0.11.2",
193
193
  "@material/mwc-icon": "^0.27.0",
194
194
  "@operato/color-picker": "^2.0.0-alpha.0",
195
- "@operato/i18n": "^2.0.0-alpha.0",
196
- "@operato/popup": "^2.0.0-alpha.2",
197
- "@operato/styles": "^2.0.0-alpha.0",
198
- "@operato/utils": "^2.0.0-alpha.0",
195
+ "@operato/i18n": "^2.0.0-alpha.28",
196
+ "@operato/popup": "^2.0.0-alpha.28",
197
+ "@operato/styles": "^2.0.0-alpha.28",
198
+ "@operato/utils": "^2.0.0-alpha.28",
199
199
  "@polymer/paper-dropdown-menu": "^3.2.0",
200
200
  "@polymer/paper-item": "^3.0.1",
201
201
  "@thebespokepixel/es-tinycolor": "^3.1.0",
202
202
  "@types/codemirror": "^5.60.5",
203
- "@zxing/library": "^0.20.0",
203
+ "@undecaf/zbar-wasm": "^0.10.1",
204
204
  "codemirror": "^6.0.1",
205
205
  "lit": "^2.5.0",
206
206
  "lodash-es": "^4.17.21"
207
207
  },
208
208
  "devDependencies": {
209
- "@custom-elements-manifest/analyzer": "^0.8.1",
209
+ "@custom-elements-manifest/analyzer": "^0.9.2",
210
210
  "@hatiolab/prettier-config": "^1.0.0",
211
211
  "@lit/localize-tools": "^0.6.3",
212
- "@open-wc/eslint-config": "^10.0.0",
212
+ "@open-wc/eslint-config": "^12.0.3",
213
213
  "@open-wc/testing": "^3.1.6",
214
- "@typescript-eslint/eslint-plugin": "^5.59.1",
215
- "@typescript-eslint/parser": "^5.59.1",
214
+ "@typescript-eslint/eslint-plugin": "^7.0.1",
215
+ "@typescript-eslint/parser": "^7.0.1",
216
216
  "@web/dev-server": "^0.3.0",
217
- "@web/dev-server-storybook": "^0.7.4",
218
- "@web/test-runner": "^0.17.0",
217
+ "@web/dev-server-storybook": "^2.0.1",
218
+ "@web/test-runner": "^0.18.0",
219
219
  "concurrently": "^8.0.1",
220
220
  "eslint": "^8.39.0",
221
- "eslint-config-prettier": "^8.3.0",
222
- "husky": "^8.0.1",
223
- "lint-staged": "^13.2.2",
224
- "prettier": "^2.4.1",
221
+ "eslint-config-prettier": "^9.1.0",
222
+ "husky": "^9.0.11",
223
+ "lint-staged": "^15.2.2",
224
+ "prettier": "^3.2.5",
225
225
  "tslib": "^2.3.1",
226
226
  "typescript": "^5.0.4"
227
227
  },
@@ -239,5 +239,5 @@
239
239
  "prettier --write"
240
240
  ]
241
241
  },
242
- "gitHead": "b940cc5dd44c22458ec03189e6bc8dae7f04f32e"
242
+ "gitHead": "458e804ceb0e7cd5c4aa9d3bb7fe64c1f5bd61a3"
243
243
  }
@@ -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
  }
@@ -4,11 +4,13 @@
4
4
 
5
5
  import './ox-input-code'
6
6
 
7
- import { css, html, PropertyValues } from 'lit'
7
+ import { css, html } from 'lit'
8
8
  import { customElement } from 'lit/decorators.js'
9
+ import { live } from 'lit/directives/live.js'
9
10
 
10
11
  import { OxFormField } from './ox-form-field.js'
11
12
  import { OxInputCode } from './ox-input-code.js'
13
+ import { isEqual } from 'lodash-es'
12
14
 
13
15
  /**
14
16
  WEB Component for code-mirror based data editor.
@@ -49,38 +51,56 @@ export class OxInputData extends OxFormField {
49
51
  ]
50
52
 
51
53
  render() {
54
+ const valueType = typeof this.value
55
+
52
56
  return html`
53
57
  <div datatype>
54
58
  <input
59
+ id="string"
55
60
  type="radio"
56
61
  name="data-type"
57
62
  data-value="string"
58
- .checked=${typeof this.value == 'string'}
63
+ .checked=${live(valueType == 'string')}
59
64
  @click=${() => this._setDataType('string')}
60
65
  ?disabled=${this.disabled}
61
- />string
66
+ />
67
+ <label for="string">string</label>
62
68
 
63
69
  <input
70
+ id="number"
64
71
  type="radio"
65
72
  name="data-type"
66
73
  data-value="number"
67
- .checked=${typeof this.value == 'number'}
74
+ .checked=${live(valueType == 'number')}
68
75
  @click=${() => this._setDataType('number')}
69
76
  ?disabled=${this.disabled}
70
- />number
77
+ />
78
+ <label for="number">number</label>
71
79
 
72
80
  <input
81
+ id="object"
73
82
  type="radio"
74
83
  name="data-type"
75
84
  data-value="object"
76
- .checked=${typeof this.value == 'object'}
85
+ .checked=${live(valueType == 'object')}
77
86
  @click=${() => this._setDataType('object')}
78
87
  ?disabled=${this.disabled}
79
- />object
88
+ />
89
+ <label for="object">object</label>
90
+
80
91
  <mwc-icon @click=${() => this._clearData()} title="delete">delete_forever</mwc-icon>
81
92
  </div>
82
93
 
83
- <ox-input-code .value=${this._getData(this.value)} language="javascript" editor ?disabled=${this.disabled}>
94
+ <ox-input-code
95
+ .value=${this._getStringData(this.value)}
96
+ language="text"
97
+ editor
98
+ ?disabled=${this.disabled}
99
+ @change=${(e: CustomEvent) => {
100
+ e.stopPropagation()
101
+ this._setDataTypeAndValue(valueType, (e.target as any).value)
102
+ }}
103
+ >
84
104
  </ox-input-code>
85
105
  `
86
106
  }
@@ -88,45 +108,75 @@ export class OxInputData extends OxFormField {
88
108
  firstUpdated() {
89
109
  this.renderRoot.addEventListener('change', e => {
90
110
  e.stopPropagation()
111
+
91
112
  const target = e.target as OxInputCode
92
113
  if (target.hasAttribute('editor')) {
114
+ if (this.value === undefined && target.value == '') {
115
+ return
116
+ }
93
117
  this.value = target.value
94
118
  }
95
-
96
- const type = this.renderRoot.querySelector('input[name=data-type]:checked')?.getAttribute('data-value')
97
- this._setDataType(type)
98
119
  })
99
120
  }
100
121
 
101
- udpated(changes: PropertyValues<this>) {
102
- if (changes.has('value')) {
103
- this.value = this._getData(this.value)
122
+ _setDataTypeAndValue(type: string | undefined | null, value: any) {
123
+ /* value must be a string */
124
+ try {
125
+ switch (type) {
126
+ case 'number':
127
+ if (!isNaN(Number(value))) {
128
+ value = Number(value)
129
+ }
130
+ break
131
+ case 'object':
132
+ value = eval('(' + value + ')')
133
+ break
134
+ }
135
+ } catch (e) {}
136
+
137
+ if (isEqual(this.value, value)) {
138
+ return
104
139
  }
140
+
141
+ this.value = value
142
+
143
+ this.requestUpdate()
144
+ this._onAfterValueChange()
105
145
  }
106
146
 
107
147
  _setDataType(type: string | undefined | null) {
108
- if (typeof this.value !== type) {
109
- var value = this.value
110
-
111
- try {
112
- switch (type) {
113
- case 'string':
114
- this.value = String(value || '')
115
- break
116
- case 'number':
117
- if (!isNaN(value)) {
118
- this.value = Number(value)
119
- }
120
- break
121
- case 'object':
122
- this.value = eval('(' + value + ')')
123
- break
124
- }
125
- } catch (e) {
126
- console.log(e)
148
+ if (typeof this.value == type) {
149
+ return
150
+ }
151
+
152
+ var value = this.value
153
+
154
+ try {
155
+ switch (type) {
156
+ case 'string':
157
+ value = this._getStringData(value)
158
+ break
159
+ case 'number':
160
+ if (!isNaN(value)) {
161
+ value = Number(value)
162
+ }
163
+ break
164
+ case 'object':
165
+ value = eval('(' + value + ')')
166
+ break
127
167
  }
168
+ } catch (e) {
169
+ console.log(e)
128
170
  }
129
171
 
172
+ if (isEqual(this.value, value)) {
173
+ this.requestUpdate()
174
+ return
175
+ }
176
+
177
+ this.value = value
178
+
179
+ this.requestUpdate()
130
180
  this._onAfterValueChange()
131
181
  }
132
182
 
@@ -135,11 +185,20 @@ export class OxInputData extends OxFormField {
135
185
  this._onAfterValueChange()
136
186
  }
137
187
 
138
- _getData(data: any) {
139
- return typeof data !== 'object' ? data || '' : JSON.stringify(data, null, 1)
188
+ _getStringData(data: any) {
189
+ const type = typeof data
190
+
191
+ switch (type) {
192
+ case 'object':
193
+ return JSON.stringify(data, null, 1)
194
+ case 'undefined':
195
+ return ''
196
+ default:
197
+ return String(data) || ''
198
+ }
140
199
  }
141
200
 
142
- _onAfterValueChange() {
201
+ async _onAfterValueChange() {
143
202
  this.dispatchEvent(
144
203
  new CustomEvent('change', {
145
204
  bubbles: true,
@@ -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>