@operato/form 8.0.0-beta.0 → 8.0.0-beta.2

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 (41) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/package.json +7 -7
  3. package/.editorconfig +0 -29
  4. package/.storybook/main.js +0 -3
  5. package/.storybook/preview.js +0 -52
  6. package/.storybook/server.mjs +0 -8
  7. package/demo/index.html +0 -110
  8. package/src/components/ox-custom-input.ts +0 -102
  9. package/src/components/ox-custom-select.ts +0 -91
  10. package/src/components/ox-file-uploader.ts +0 -164
  11. package/src/components/ox-form-master.ts +0 -246
  12. package/src/components/ox-form-paginator.ts +0 -60
  13. package/src/components/ox-search-form-paginator.ts +0 -46
  14. package/src/components/ox-search-form.ts +0 -467
  15. package/src/filters/filter-checkbox.ts +0 -34
  16. package/src/filters/filter-input-barcode.ts +0 -35
  17. package/src/filters/filter-input.ts +0 -30
  18. package/src/filters/filter-range-date.ts +0 -81
  19. package/src/filters/filter-range-number.ts +0 -64
  20. package/src/filters/filter-select.ts +0 -56
  21. package/src/filters/filter-styles.ts +0 -127
  22. package/src/filters/index.ts +0 -10
  23. package/src/filters/ox-filter-personalizer.ts +0 -188
  24. package/src/filters/ox-filters-form-base.ts +0 -437
  25. package/src/filters/registry.ts +0 -54
  26. package/src/index.ts +0 -4
  27. package/src/styles/index.ts +0 -3
  28. package/src/styles/multi-column-form-styles.ts +0 -192
  29. package/src/styles/search-form-styles.ts +0 -160
  30. package/src/styles/single-column-form-styles.ts +0 -114
  31. package/src/types.ts +0 -106
  32. package/stories/ox-filters-form-barcode.stories.ts +0 -140
  33. package/stories/ox-filters-form-base-bounded.stories.ts +0 -208
  34. package/stories/ox-filters-form-base-change-handler.stories.ts +0 -220
  35. package/stories/ox-filters-form-base-page-preference.stories.ts +0 -177
  36. package/stories/ox-filters-form-base.stories.ts +0 -179
  37. package/stories/ox-search-form-change-handler.stories.ts +0 -253
  38. package/stories/ox-search-form.stories.ts +0 -258
  39. package/tsconfig.json +0 -24
  40. package/web-dev-server.config.mjs +0 -27
  41. package/web-test-runner.config.mjs +0 -41
@@ -1,467 +0,0 @@
1
- import '@material/web/icon/icon.js'
2
- import './ox-custom-input.js'
3
- import './ox-custom-select.js'
4
-
5
- import gql from 'graphql-tag'
6
- import { css, html, LitElement, nothing, PropertyValues } from 'lit'
7
- import { customElement, property, query, queryAll, queryAsync } from 'lit/decorators.js'
8
- import { ifDefined } from 'lit/directives/if-defined.js'
9
- import { until } from 'lit/directives/until.js'
10
- import { styles as MDTypeScaleStyles } from '@material/web/typography/md-typescale-styles'
11
-
12
- import { buildArgs, client } from '@operato/graphql'
13
-
14
- import { SearchFormStyles } from '../styles/search-form-styles.js'
15
- import { SearchFormField, SelectOption, SelectOptionObject } from '../types.js'
16
-
17
- function buildOptions(field: SearchFormField, options: SelectOption[]) {
18
- const selectOptionObjects = options.map(option => {
19
- switch (typeof option) {
20
- case 'string':
21
- return {
22
- name: option,
23
- value: option
24
- }
25
- case 'object':
26
- return {
27
- name: option.name || option.display,
28
- value: option.value
29
- }
30
- default:
31
- return option
32
- }
33
- }) as SelectOptionObject[]
34
-
35
- return html`
36
- ${selectOptionObjects?.map(
37
- ({ value, display, name }) => html`
38
- <option ?selected=${value == field.value} value=${value}>${name || display}</option>
39
- `
40
- )}
41
- `
42
- }
43
-
44
- @customElement('ox-search-form')
45
- export class OxSearchForm extends LitElement {
46
- static styles = [
47
- MDTypeScaleStyles,
48
- SearchFormStyles,
49
- css`
50
- :host {
51
- display: flex;
52
- flex-direction: column;
53
- padding: var(--spacing-small);
54
-
55
- position: relative;
56
- }
57
-
58
- ::placeholder {
59
- text-transform: capitalize;
60
- color: rgba(var(--secondary-color-rgb), 0.5);
61
- }
62
- label {
63
- font-size: var(--md-sys-typescale-label-large-size, 0.875rem);
64
- color: var(--md-sys-color-primary);
65
- }
66
- select,
67
- input {
68
- border: none;
69
- border-bottom: var(--ox-filters-input-border, 1px solid var(--md-sys-color-outline));
70
- font: var(--ox-filters-input-font);
71
- }
72
-
73
- select:focus,
74
- input:focus {
75
- outline: none;
76
- border-bottom: var(--ox-filters-input-focus-border, 1px solid var(--md-sys-color-primary));
77
- color: var(--ox-filters-input-focus-color, var(--md-sys-color-primary));
78
- }
79
- `
80
- ]
81
-
82
- @property({ type: Array }) fields?: SearchFormField[]
83
- @property({ type: Boolean }) hideSearchButton: boolean = false
84
- @property({ type: Boolean, attribute: true }) autofocus: boolean = false
85
-
86
- @queryAsync('form') form!: HTMLFormElement
87
- @query('input[autofocus]') autofocused?: HTMLInputElement
88
- @queryAll('input, select') formFields!: ((HTMLInputElement | HTMLSelectElement) & { field: string; _value: any })[]
89
-
90
- private autoUpdateTargetsOnChange: { [name: string]: string[] } = {}
91
-
92
- render() {
93
- var autofocusConsumed = 0
94
-
95
- return html`
96
- <form
97
- class="search-form md-typescale-body-medium-prominent"
98
- @submit=${(e: Event) => {
99
- e.preventDefault() // input="text" 가 한개만 있을경우에 엔터를 치면 강제 submit되는 증상 방지
100
- }}
101
- @change=${async (e: Event) => {
102
- const field = e.target as HTMLInputElement
103
- const name = field?.getAttribute('name')
104
- const value = field.value
105
-
106
- if (name && this.autoUpdateTargetsOnChange[name]) {
107
- ;(this.autoUpdateTargetsOnChange[name] || []).forEach(name => {
108
- const target = this.renderRoot.querySelector(`[name='${name}']`)
109
- if (target) {
110
- ;(target as HTMLInputElement).value = ''
111
- }
112
- })
113
-
114
- this.requestUpdate()
115
- }
116
-
117
- const { onchange } = this.fields?.find(field => field.name == name) || {}
118
-
119
- const keepGoing = onchange ? await onchange.call(null, value, this) : true
120
-
121
- if (keepGoing) {
122
- this.dispatchEvent(new CustomEvent('change', { detail: value }))
123
- this.submit()
124
- }
125
- }}
126
- >
127
- ${(this.fields || []).map(
128
- field => html`
129
- ${field.type === 'select'
130
- ? this.renderSelect(field)
131
- : field.type === 'checkbox'
132
- ? this.renderCheckbox(field)
133
- : field.type === 'object'
134
- ? this.renderObject(field, autofocusConsumed++)
135
- : this.renderElse(field, autofocusConsumed++)}
136
- `
137
- )}
138
- ${!!this.hideSearchButton
139
- ? nothing
140
- : html`<md-icon @click=${(e: MouseEvent) => this.submit()} search>search</md-icon>`}
141
- </form>
142
- `
143
- }
144
-
145
- renderSelect(field: SearchFormField) {
146
- var { id, options, readonly, label, name, value, placeholder, hidden } = field
147
-
148
- if (typeof options === 'function') {
149
- options = (options as Function).call(field, this)
150
-
151
- if (options instanceof Promise) {
152
- var optionsTemplate = html`${until(options.then(options => buildOptions(field, options)))}`
153
- } else {
154
- var optionsTemplate = buildOptions(field, (options || []) as SelectOption[])
155
- }
156
- } else {
157
- var optionsTemplate = buildOptions(field, (options || []) as SelectOption[])
158
- }
159
-
160
- return html`
161
- <label ?hidden=${hidden}>${label || name || id}</label>
162
- <select
163
- .value=${value}
164
- name=${ifDefined(name)}
165
- placeholder=${ifDefined(placeholder)}
166
- ?hidden=${hidden}
167
- ?readonly=${readonly}
168
- >
169
- ${optionsTemplate!}
170
- </select>
171
- `
172
- }
173
-
174
- renderCheckbox(field: SearchFormField) {
175
- return html`
176
- <input
177
- id="ckbx_${field.name}"
178
- type="checkbox"
179
- name=${ifDefined(field.name)}
180
- ?checked=${field.value}
181
- ?hidden=${field.hidden}
182
- ?readonly=${field.readonly}
183
- @click=${(e: MouseEvent) => {
184
- const checkbox = e.currentTarget as HTMLInputElement & { _value: any }
185
- if (checkbox.hasAttribute('indeterminate')) {
186
- const values = [undefined, 'on', 'off']
187
- const currentValue = checkbox._value
188
- const newValue =
189
- values.indexOf(currentValue) + 1 > values.length - 1
190
- ? values[0]
191
- : values[values.indexOf(currentValue) + 1]
192
-
193
- if (newValue === 'on') {
194
- checkbox.checked = true
195
- } else if (newValue === 'off') {
196
- checkbox.checked = false
197
- } else {
198
- checkbox.indeterminate = true
199
- }
200
- checkbox._value = newValue
201
- }
202
- }}
203
- />
204
- <label for="ckbx_${field.name}" ?hidden=${field.hidden}>${field.label || field.name || field.id}</label>
205
- `
206
- }
207
-
208
- renderObject(field: SearchFormField, autofocusConsumed: number) {
209
- return html`
210
- <label ?hidden=${field.hidden}>${field.label || field.name || field.id}</label>
211
- <input
212
- .type=${field.type}
213
- name=${ifDefined(field.name)}
214
- placeholder=${ifDefined(field.placeholder)}
215
- .value=${field.value || ''}
216
- .field=${field.field}
217
- .queryName=${field.queryName}
218
- ?hidden=${field.hidden}
219
- ?readonly=${field.readonly}
220
- ?autofocus=${!field.hidden && 0 === autofocusConsumed}
221
- />
222
- `
223
- }
224
-
225
- renderElse(field: SearchFormField, autofocusConsumed: number) {
226
- return html`
227
- <label ?hidden=${field.hidden}>${field.label || field.name || field.id}</label>
228
- <input
229
- .type=${field.type}
230
- .value=${field.value || ''}
231
- name=${ifDefined(field.name)}
232
- placeholder=${ifDefined(field.placeholder)}
233
- ?hidden=${field.hidden}
234
- ?readonly=${field.readonly}
235
- ?autofocus=${!field.hidden && 0 === autofocusConsumed++}
236
- />
237
- `
238
- }
239
-
240
- async updated(changes: PropertyValues<this>) {
241
- if (changes.has('fields')) {
242
- this._checkInputValidity()
243
-
244
- var formFields = Array.from(this.formFields)
245
- this.fields &&
246
- this.fields.forEach(field => {
247
- var formField = formFields.find(f => f.name == field.name)
248
- if (field.props instanceof Object && !Array.isArray(field.props)) {
249
- for (let prop in field.props) {
250
- if (field.props[prop]) {
251
- formField!.setAttribute(prop, field.props[prop])
252
- }
253
- }
254
- }
255
-
256
- if (field.attrs && Array.isArray(field.attrs)) {
257
- field.attrs.forEach(attr => {
258
- if (attr === 'indeterminate') {
259
- ;(formField as HTMLInputElement).indeterminate = true
260
- } else {
261
- formField!.setAttribute(attr, '')
262
- }
263
- })
264
- }
265
-
266
- if (field.handlers instanceof Object && !Array.isArray(field.handlers)) {
267
- for (let eventName in field.handlers) {
268
- formField!.addEventListener(eventName, field.handlers[eventName])
269
- }
270
- }
271
- })
272
-
273
- if (this.autofocus) {
274
- await this.requestUpdate()
275
- this.autofocused?.focus()
276
- }
277
-
278
- this.autoUpdateTargetsOnChange = {}
279
- this.fields
280
- ?.filter(filter => filter.boundTo && filter.boundTo.length > 0)
281
- .map(({ name, boundTo }) => {
282
- boundTo!.forEach(to => {
283
- const origin = this.autoUpdateTargetsOnChange[to] || []
284
- if (name && !origin.includes(name)) {
285
- this.autoUpdateTargetsOnChange[to] = [...origin, name]
286
- }
287
- })
288
- })
289
- }
290
- }
291
-
292
- getFieldByName(name: string) {
293
- return this.renderRoot.querySelector(`[name=${name}]`)
294
- }
295
-
296
- _checkInputValidity() {
297
- const names = this.fields?.map(i => i.name) || []
298
- const result = names.every(name => {
299
- return names.indexOf(name) === names.lastIndexOf(name)
300
- })
301
- if (!result) {
302
- throw new Error('Field name is duplicated.')
303
- }
304
- }
305
-
306
- get searchParams() {
307
- let searchParam = new URLSearchParams()
308
- const fields = Array.from(this.formFields)
309
- fields.forEach(field => searchParam.append(field.name, field.value))
310
-
311
- return decodeURI(searchParam.toString())
312
- }
313
-
314
- get queryFilters() {
315
- return Array.from(this.formFields)
316
- .filter(
317
- field =>
318
- (field.type !== 'checkbox' && field.value && field.value !== '') ||
319
- (field.type === 'checkbox' && !field.hasAttribute('indeterminate')) ||
320
- (field.type === 'checkbox' && field.hasAttribute('indeterminate') && field._value)
321
- )
322
- .map(field => {
323
- const operator = field.getAttribute('searchOper') || ''
324
- let value = operator.indexOf('like') >= 0 ? `%${field.value}%` : field.value
325
- return {
326
- name: field.name,
327
- value:
328
- field.type === 'text'
329
- ? value
330
- : field.type === 'checkbox'
331
- ? field.hasAttribute('indeterminate')
332
- ? field._value === 'on'
333
- ? true
334
- : false
335
- : (field as HTMLInputElement).checked
336
- : field.type === 'number'
337
- ? parseFloat(value)
338
- : value,
339
- operator
340
- }
341
- })
342
- }
343
-
344
- public async getQueryFilters() {
345
- return await Promise.all(
346
- Array.from(this.formFields)
347
- .filter(
348
- field =>
349
- (field.type !== 'checkbox' && field.value && field.value !== '') ||
350
- (field.type === 'checkbox' && !field.hasAttribute('indeterminate')) ||
351
- (field.type === 'checkbox' && field.hasAttribute('indeterminate') && field._value)
352
- )
353
- .map(async field => {
354
- const name = field.name
355
- const operator = (field.type === 'text' && field.field ? 'in' : field.getAttribute('searchOper')) || ''
356
- const value = operator.indexOf('like') >= 0 ? `%${field.value}%` : field.value
357
- const filter = {
358
- name,
359
- operator,
360
- value:
361
- field.type === 'text' && field.getAttribute('type') === 'object'
362
- ? await this._getResourceIds(field)
363
- : field.type === 'text'
364
- ? value
365
- : field.type === 'checkbox'
366
- ? field.hasAttribute('indeterminate')
367
- ? field._value === 'on'
368
- ? true
369
- : false
370
- : (field as HTMLInputElement).checked
371
- : field.type === 'number'
372
- ? parseFloat(value)
373
- : value,
374
- relation: field.type === 'text' && field.getAttribute('type') === 'object'
375
- }
376
- return filter
377
- })
378
- )
379
- }
380
-
381
- private async _getResourceIds(inputField: HTMLInputElement | HTMLSelectElement) {
382
- const {
383
- field = 'name',
384
- value,
385
- queryName
386
- } = inputField as (HTMLInputElement | HTMLSelectElement) & { field: string; queryName: string }
387
-
388
- const response = await client.query({
389
- query: gql`
390
- query {
391
- fetch: ${queryName}(${buildArgs({
392
- filters: [
393
- {
394
- name: field,
395
- operator: 'i_like',
396
- value: `%${value}%`
397
- }
398
- ]
399
- })}) {
400
- items {
401
- id
402
- }
403
- }
404
- }
405
- `
406
- })
407
-
408
- if (!response.errors) {
409
- const items = response.data.fetch.items as { id: string }[]
410
- return (items && items.map(item => item.id)) || []
411
- }
412
- }
413
-
414
- serialize() {
415
- let data: { [name: string]: any } = {}
416
-
417
- Array.from(this.formFields).forEach(field => {
418
- const name = field.name
419
- if (field.type === 'number') {
420
- data[name] = parseFloat(field.value)
421
- } else if (field.type === 'checkbox') {
422
- data[name] = (field as HTMLInputElement).checked
423
- } else {
424
- data[name] = field.value
425
- }
426
- })
427
-
428
- return data
429
- }
430
-
431
- reset(value?: string[] | { [field: string]: any }) {
432
- this.form
433
- .then((form: HTMLFormElement) => {
434
- if (value) {
435
- if (value instanceof Array) {
436
- value
437
- .filter(item => typeof item == 'string')
438
- .forEach(item => {
439
- const field = this.getFieldByName(item)
440
- if (field) {
441
- ;(field as HTMLInputElement).value = ''
442
- }
443
- })
444
- } else {
445
- const keys = Object.keys(value)
446
- keys
447
- .filter(key => typeof key == 'string')
448
- .forEach(key => {
449
- const field = this.getFieldByName(key)
450
- if (field) {
451
- ;(field as HTMLInputElement).value = value[key]
452
- }
453
- })
454
- }
455
- } else {
456
- form.reset()
457
- }
458
- })
459
- .catch((error: any) => {
460
- console.error('Error resetting the form:', error)
461
- })
462
- }
463
-
464
- submit() {
465
- this.dispatchEvent(new CustomEvent('submit'))
466
- }
467
- }
@@ -1,34 +0,0 @@
1
- import '@operato/input/ox-checkbox.js'
2
-
3
- import { html } from 'lit-html'
4
-
5
- import { FilterSelectRenderer } from '../types.js'
6
-
7
- export const FilterCheckbox: FilterSelectRenderer = (filter, value, owner) => {
8
- const { name, label, readonly } = filter
9
-
10
- return html`
11
- <ox-checkbox
12
- name=${name}
13
- ?checked=${value}
14
- indeterminatable
15
- ?indeterminate=${value === undefined}
16
- left-label
17
- @change=${(e: CustomEvent) => {
18
- ;(e.target as HTMLElement).dispatchEvent(
19
- new CustomEvent('filter-change', {
20
- bubbles: true,
21
- composed: true,
22
- detail: {
23
- name: name,
24
- operator: filter.operator,
25
- value: (e.target as Element & { checked: boolean | undefined }).checked
26
- }
27
- })
28
- )
29
- }}
30
- ?readonly=${readonly}
31
- ><label><span>${label}</span></label></ox-checkbox
32
- >
33
- `
34
- }
@@ -1,35 +0,0 @@
1
- import '@operato/input/ox-input-barcode.js'
2
-
3
- import { html } from 'lit-html'
4
-
5
- import { FilterSelectRenderer } from '../types.js'
6
-
7
- export const FilterInputBarcode: FilterSelectRenderer = (filter, value, owner) => {
8
- const { name, readonly, options } = filter
9
- const { englishOnly, selectAfterChange } = (options || {}) as { [key: string]: any }
10
-
11
- return html`
12
- <ox-input-barcode
13
- name=${name}
14
- ?english-only=${englishOnly}
15
- ?select-after-change=${selectAfterChange}
16
- .value=${value === undefined ? '' : value}
17
- @change=${(e: CustomEvent) => {
18
- const input = e.target as HTMLInputElement
19
- input.dispatchEvent(
20
- new CustomEvent('filter-change', {
21
- bubbles: true,
22
- composed: true,
23
- detail: {
24
- name,
25
- operator: filter.operator,
26
- value: input.value
27
- }
28
- })
29
- )
30
- }}
31
- filter-input
32
- ?readonly=${readonly}
33
- ></ox-input-barcode>
34
- `
35
- }
@@ -1,30 +0,0 @@
1
- import { html } from 'lit-html'
2
-
3
- import { FilterSelectRenderer } from '../types.js'
4
-
5
- export const FilterInput: FilterSelectRenderer = (filter, value, owner) => {
6
- const { name, type, readonly } = filter
7
-
8
- return html`
9
- <input
10
- type=${type}
11
- name=${name}
12
- .value=${value === undefined ? '' : value}
13
- @change=${(e: CustomEvent) => {
14
- const input = e.target as HTMLInputElement
15
- input.dispatchEvent(
16
- new CustomEvent('filter-change', {
17
- bubbles: true,
18
- composed: true,
19
- detail: {
20
- name: name,
21
- operator: filter.operator,
22
- value: input.value
23
- }
24
- })
25
- )
26
- }}
27
- ?readonly=${readonly}
28
- />
29
- `
30
- }
@@ -1,81 +0,0 @@
1
- import { html } from 'lit-html'
2
-
3
- import { FilterSelectRenderer } from '../types.js'
4
-
5
- function formatDate(type: string, date: Date) {
6
- var year = date.getFullYear()
7
- var month = ('0' + (date.getMonth() + 1)).slice(-2)
8
- var day = ('0' + date.getDate()).slice(-2)
9
- var hours = ('0' + date.getHours()).slice(-2)
10
- var minutes = ('0' + date.getMinutes()).slice(-2)
11
-
12
- var formatDate = year + '-' + month + '-' + day
13
-
14
- return type == 'date' ? formatDate : formatDate + 'T' + hours + ':' + minutes
15
- }
16
-
17
- export const FilterRangeDate: FilterSelectRenderer = (filter, value, owner) => {
18
- var { name, type, operator, readonly } = filter
19
- var [from, to] = value instanceof Array ? value : []
20
-
21
- if (type === 'datetime') {
22
- type = 'datetime-local'
23
- }
24
-
25
- from = formatDate(type, new Date(from))
26
- to = formatDate(type, new Date(to))
27
-
28
- return html`
29
- <input
30
- name=${name}
31
- type=${type as 'date' | 'datetime-local'}
32
- value=${from}
33
- from
34
- @keyup=${(e: KeyboardEvent) => {
35
- if (e.key !== 'Enter') {
36
- return
37
- }
38
- ;(e.target as HTMLElement).dispatchEvent(
39
- new CustomEvent('filter-change', {
40
- bubbles: true,
41
- composed: true,
42
- detail: {
43
- name,
44
- operator,
45
- value: Array.from(
46
- (e.target as HTMLElement).parentElement!.querySelectorAll('input[from], input[to]')
47
- ).map(input => (input as HTMLInputElement).value)
48
- }
49
- })
50
- )
51
- }}
52
- ?readonly=${readonly}
53
- />
54
- ~
55
- <input
56
- name=${name}
57
- type=${type as 'date' | 'datetime-local'}
58
- value=${to}
59
- to
60
- @keyup=${(e: KeyboardEvent) => {
61
- if (e.key !== 'Enter') {
62
- return
63
- }
64
- ;(e.target as HTMLElement).dispatchEvent(
65
- new CustomEvent('filter-change', {
66
- bubbles: true,
67
- composed: true,
68
- detail: {
69
- name,
70
- operator,
71
- value: Array.from(
72
- (e.target as HTMLElement).parentElement!.querySelectorAll('input[from], input[to]')
73
- ).map(input => (input as HTMLInputElement).value)
74
- }
75
- })
76
- )
77
- }}
78
- ?readonly=${readonly}
79
- />
80
- `
81
- }