@operato/form 1.0.0-alpha.1

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 (61) hide show
  1. package/.editorconfig +29 -0
  2. package/.storybook/main.js +3 -0
  3. package/.storybook/server.mjs +8 -0
  4. package/CHANGELOG.md +11 -0
  5. package/LICENSE +21 -0
  6. package/README.md +75 -0
  7. package/demo/index.html +33 -0
  8. package/dist/src/components/ox-custom-input.d.ts +23 -0
  9. package/dist/src/components/ox-custom-input.js +114 -0
  10. package/dist/src/components/ox-custom-input.js.map +1 -0
  11. package/dist/src/components/ox-custom-select.d.ts +21 -0
  12. package/dist/src/components/ox-custom-select.js +100 -0
  13. package/dist/src/components/ox-custom-select.js.map +1 -0
  14. package/dist/src/components/ox-file-uploader.d.ts +13 -0
  15. package/dist/src/components/ox-file-uploader.js +166 -0
  16. package/dist/src/components/ox-file-uploader.js.map +1 -0
  17. package/dist/src/components/ox-form-master.d.ts +1 -0
  18. package/dist/src/components/ox-form-master.js +208 -0
  19. package/dist/src/components/ox-form-master.js.map +1 -0
  20. package/dist/src/components/ox-form-paginator.d.ts +9 -0
  21. package/dist/src/components/ox-form-paginator.js +70 -0
  22. package/dist/src/components/ox-form-paginator.js.map +1 -0
  23. package/dist/src/components/ox-search-form-paginator.d.ts +1 -0
  24. package/dist/src/components/ox-search-form-paginator.js +51 -0
  25. package/dist/src/components/ox-search-form-paginator.js.map +1 -0
  26. package/dist/src/components/ox-search-form.d.ts +56 -0
  27. package/dist/src/components/ox-search-form.js +284 -0
  28. package/dist/src/components/ox-search-form.js.map +1 -0
  29. package/dist/src/index.d.ts +3 -0
  30. package/dist/src/index.js +4 -0
  31. package/dist/src/index.js.map +1 -0
  32. package/dist/src/styles/index.d.ts +3 -0
  33. package/dist/src/styles/index.js +4 -0
  34. package/dist/src/styles/index.js.map +1 -0
  35. package/dist/src/styles/multi-column-form-styles.d.ts +1 -0
  36. package/dist/src/styles/multi-column-form-styles.js +192 -0
  37. package/dist/src/styles/multi-column-form-styles.js.map +1 -0
  38. package/dist/src/styles/search-form-styles.d.ts +1 -0
  39. package/dist/src/styles/search-form-styles.js +156 -0
  40. package/dist/src/styles/search-form-styles.js.map +1 -0
  41. package/dist/src/styles/single-column-form-styles.d.ts +1 -0
  42. package/dist/src/styles/single-column-form-styles.js +114 -0
  43. package/dist/src/styles/single-column-form-styles.js.map +1 -0
  44. package/dist/tsconfig.tsbuildinfo +1 -0
  45. package/package.json +77 -0
  46. package/src/components/ox-custom-input.ts +102 -0
  47. package/src/components/ox-custom-select.ts +90 -0
  48. package/src/components/ox-file-uploader.ts +163 -0
  49. package/src/components/ox-form-master.ts +241 -0
  50. package/src/components/ox-form-paginator.ts +60 -0
  51. package/src/components/ox-search-form-paginator.ts +46 -0
  52. package/src/components/ox-search-form.ts +317 -0
  53. package/src/index.ts +3 -0
  54. package/src/styles/index.ts +3 -0
  55. package/src/styles/multi-column-form-styles.ts +192 -0
  56. package/src/styles/search-form-styles.ts +156 -0
  57. package/src/styles/single-column-form-styles.ts +114 -0
  58. package/src/themes/form-theme.css +75 -0
  59. package/tsconfig.json +23 -0
  60. package/web-dev-server.config.mjs +27 -0
  61. package/web-test-runner.config.mjs +41 -0
@@ -0,0 +1,90 @@
1
+ import { LitElement, PropertyValues, css, html } from 'lit'
2
+ import { customElement, property, query } from 'lit/decorators.js'
3
+
4
+ @customElement('ox-custom-select')
5
+ export class CustomSelect extends LitElement {
6
+ static styles = css`
7
+ select {
8
+ margin: auto 0;
9
+ border-style: var(--form-input-border-style, solid);
10
+ border-width: var(--form-input-border-width, 1px);
11
+ border-color: var(--form-input-border-color, #c4c5c6);
12
+ padding: var(--form-input-padding, 5px);
13
+ outline: var(--form-input-outline, none);
14
+ background-color: var(--form-input-background-color, #fff);
15
+ min-width: calc(
16
+ var(--form-input-width, 300px) + 2 * var(--form-input-padding, 5px) + 2 * var(--form-input-border-width, 1px)
17
+ );
18
+ max-width: calc(
19
+ var(--form-input-width) + 2 * var(--form-input-padding, 5px) + 2 * var(--form-input-border-width, 1px)
20
+ );
21
+ -webkit-appearance: none;
22
+ -webkit-border-radius: 0px;
23
+ }
24
+ `
25
+
26
+ @property({ type: String }) name?: string
27
+ @property({ type: Array }) options?: { name: string; value: any }[]
28
+ @property({ type: Object }) props: any
29
+ @property({ type: Array }) attrs: any
30
+ @property({ type: String }) value: any
31
+ @property({ type: Boolean }) autofocus: boolean = false
32
+
33
+ @query('select') select!: HTMLInputElement
34
+
35
+ render() {
36
+ return html`
37
+ <select
38
+ name=${this.name}
39
+ ?autofocus=${this.autofocus}
40
+ @change=${(e: Event) => (this.value = (e.currentTarget as HTMLSelectElement).value)}
41
+ >
42
+ ${(this.options || []).map(
43
+ option => html`
44
+ <option value=${option.value} ?selected=${this.value === option.value}>${option.name}</option>
45
+ `
46
+ )}
47
+ </select>
48
+ `
49
+ }
50
+
51
+ firstUpdated() {
52
+ this.dispatchEvent(new CustomEvent('load'))
53
+ }
54
+
55
+ updated(changes: PropertyValues<this>) {
56
+ if (changes.has('props') && this._isObject(this.props)) {
57
+ for (let prop in this.props) {
58
+ if (this.props[prop]) {
59
+ this.select.setAttribute(prop, this.props[prop])
60
+ this.setAttribute(prop, this.props[prop])
61
+ }
62
+ }
63
+ }
64
+
65
+ if (changes.has('attrs')) {
66
+ if (this.attrs && Array.isArray(this.attrs)) {
67
+ this.attrs.forEach(attr => {
68
+ this.select.setAttribute(attr, '')
69
+ this.setAttribute(attr, '')
70
+ })
71
+ }
72
+ }
73
+ }
74
+
75
+ _isObject(v: any) {
76
+ return v instanceof Object && !Array.isArray(v)
77
+ }
78
+
79
+ focus() {
80
+ this.select.focus()
81
+ }
82
+
83
+ blur() {
84
+ this.select.blur()
85
+ }
86
+
87
+ checkValidity() {
88
+ return this.select.checkValidity()
89
+ }
90
+ }
@@ -0,0 +1,163 @@
1
+ import { LitElement, css, html } from 'lit'
2
+ import { customElement, property, query } from 'lit/decorators.js'
3
+
4
+ import { FileDropHelper } from '@operato/utils'
5
+
6
+ @customElement('ox-file-uploader')
7
+ export class FileUploader extends LitElement {
8
+ static styles = [
9
+ css`
10
+ :host {
11
+ display: flex;
12
+ flex-direction: column;
13
+ border-radius: var(--border-radius);
14
+ text-align: center;
15
+ padding-bottom: 5px !important;
16
+ text-transform: capitalize;
17
+
18
+ border: var(--file-uploader-border);
19
+ background-color: var(--main-section-background-color);
20
+ font: var(--file-uploader-font) !important;
21
+ color: var(--file-uploader-color);
22
+ }
23
+ :host > mwc-icon {
24
+ color: var(--file-uploader-icon-color);
25
+ }
26
+
27
+ :host(.candrop) {
28
+ background-color: var(--file-uploader-candrop-background-color);
29
+ }
30
+
31
+ #input-file {
32
+ display: none;
33
+ }
34
+
35
+ label {
36
+ position: relative;
37
+ width: auto;
38
+ border: none;
39
+ text-transform: capitalize;
40
+
41
+ padding: var(--file-uploader-label-padding);
42
+ border-radius: var(--file-uploader-label-border-radius);
43
+ background-color: var(--file-uploader-label-background-color);
44
+ color: var(--file-uploader-label-color);
45
+ font: var(--file-uploader-label-font) !important;
46
+ }
47
+
48
+ ul {
49
+ max-width: 500px;
50
+ list-style: none;
51
+ padding: 0;
52
+ }
53
+ li {
54
+ text-align: left;
55
+
56
+ padding: var(--file-uploader-li-padding);
57
+ border-bottom: var(--file-uploader-li-border-bottom);
58
+ }
59
+ li mwc-icon {
60
+ float: right;
61
+ cursor: pointer;
62
+ font: var(--file-uploader-li-icon-font);
63
+ margin: var(--file-uploader-li-icon-margin);
64
+ }
65
+ li mwc-icon:hover,
66
+ li mwc-icon:active {
67
+ color: var(--file-uploader-li-icon-focus-color);
68
+ }
69
+ `
70
+ ]
71
+
72
+ @property({ type: Boolean }) multiple: boolean = false
73
+ @property({ type: String }) accept: string = '*/*'
74
+ @property({ type: String }) label?: string
75
+
76
+ @property({ type: Array }) _files: File[] = []
77
+
78
+ @query('#input-file') fileInput!: HTMLInputElement
79
+
80
+ render() {
81
+ return html`
82
+ <mwc-icon>post_add</mwc-icon>
83
+ <span>add file or drop files here!</span>
84
+ <input
85
+ id="input-file"
86
+ type="file"
87
+ accept=${this.accept}
88
+ ?multiple=${this.multiple}
89
+ hidden
90
+ @change=${(e: Event) => {
91
+ const fileInput = e.currentTarget as HTMLInputElement
92
+ const files = fileInput.files as FileList
93
+ //// Temporary Validation preventing file size more than 10mb to be uploaded
94
+ for (let index = 0; index < files.length; index++) {
95
+ if (files[index].size > 9999999) {
96
+ document.dispatchEvent(
97
+ new CustomEvent('notify', {
98
+ detail: {
99
+ type: 'error',
100
+ message: 'File size must be less than 10 MB.'
101
+ }
102
+ })
103
+ )
104
+ throw new Error('File size must be less than 10 MB.')
105
+ }
106
+ }
107
+
108
+ this._files = [...this._files, ...Array.from(files)]
109
+
110
+ this.dispatchEvent(
111
+ new CustomEvent('files-change', {
112
+ bubbles: true,
113
+ composed: true,
114
+ detail: {
115
+ files: files
116
+ }
117
+ })
118
+ )
119
+
120
+ fileInput.value = ''
121
+ }}
122
+ />
123
+ <label for="input-file">${this.label || 'select file'}</label>
124
+
125
+ ${this._files?.length
126
+ ? html` <ul>
127
+ ${this._files.map(
128
+ file => html`
129
+ <li>
130
+ - ${file.name}
131
+ <mwc-icon
132
+ @click=${(e: Event) => {
133
+ this._files.splice(this._files.indexOf(file), 1)
134
+ this.requestUpdate()
135
+ }}
136
+ >delete_outline</mwc-icon
137
+ >
138
+ </li>
139
+ `
140
+ )}
141
+ </ul>`
142
+ : ''}
143
+ `
144
+ }
145
+
146
+ firstUpdated() {
147
+ FileDropHelper.set(this)
148
+
149
+ this.addEventListener('file-drop', (e: Event) => {
150
+ const de = e as any
151
+ this._files = this.multiple ? de.detail : de.detail[0] ? [de.detail[0]] : []
152
+ })
153
+ }
154
+
155
+ get files() {
156
+ return this._files
157
+ }
158
+
159
+ reset() {
160
+ this.fileInput.value = ''
161
+ this._files = []
162
+ }
163
+ }
@@ -0,0 +1,241 @@
1
+ import { LitElement, PropertyValues, css, html } from 'lit'
2
+ import { customElement, property, query, state } from 'lit/decorators.js'
3
+
4
+ import { CustomInput } from './ox-custom-input.js'
5
+ import { CustomSelect } from './ox-custom-select.js'
6
+ import { FormPaginator } from './ox-form-paginator.js'
7
+
8
+ @customElement('ox-form-master')
9
+ class FormMaster extends LitElement {
10
+ static styles = css`
11
+ form {
12
+ display: grid;
13
+ padding: var(--form-padding, 10px);
14
+ grid-gap: var(--form-grid-gap, 10px);
15
+ grid-template-columns: var(--form-grid-template-columns);
16
+ background-color: var(--form-background-color, #e5e5e5);
17
+ }
18
+ custom-input,
19
+ custom-select {
20
+ justify-self: center;
21
+ }
22
+ `
23
+
24
+ @property({ type: Array }) fields?: {
25
+ type: string
26
+ hidden: boolean
27
+ name: string
28
+ id: string
29
+ label: string
30
+ placeholder: string
31
+ value: any
32
+ field: string
33
+ queryName: string
34
+ props: { [prop: string]: string }
35
+ attrs: string[]
36
+ valueField?: string
37
+ displayField?: string
38
+ options: { name: string; value: string }[]
39
+ }[]
40
+ @property({ type: Number }) maxRowCount: number = 2
41
+ @property({ type: Number }) maxColumnCount: number = 4
42
+ @property({ type: String }) initFocus?: string
43
+
44
+ @state() private pageCount!: number
45
+
46
+ @query('form') form!: HTMLFormElement
47
+
48
+ private loadedCount: number = 0
49
+ private resizeEventHandler?: EventListener
50
+
51
+ connectedCallback(): void {
52
+ super.connectedCallback()
53
+
54
+ this.resizeEventHandler = this._adjustColumnProperty.bind(this)
55
+ window.addEventListener('resize', this.resizeEventHandler)
56
+ }
57
+
58
+ disconnectedCallback(): void {
59
+ super.disconnectedCallback()
60
+
61
+ if (this.resizeEventHandler) {
62
+ window.removeEventListener('resize', this.resizeEventHandler)
63
+ }
64
+ }
65
+
66
+ render() {
67
+ return html`
68
+ <form
69
+ @keypress=${(e: KeyboardEvent) => {
70
+ if (e.keyCode === 13) {
71
+ this.submit()
72
+ }
73
+ }}
74
+ >
75
+ ${(this.fields || []).map(
76
+ field => html`
77
+ ${field.type === 'select'
78
+ ? html`
79
+ <ox-custom-select
80
+ id=${field.id || field.name}
81
+ name=${field.name || field.id}
82
+ .props=${field.props}
83
+ .attrs=${field.attrs}
84
+ .value=${field.value}
85
+ .autofocus=${this.initFocus === field.name}
86
+ @load=${this._onInputLoad.bind(this)}
87
+ .options=${field.options}
88
+ ></ox-custom-select>
89
+ `
90
+ : html`
91
+ <ox-custom-input
92
+ id=${field.id || field.name}
93
+ name=${field.name || field.id}
94
+ .props=${field.props}
95
+ .attrs=${field.attrs}
96
+ .valueField=${field.valueField}
97
+ .displayField=${field.displayField}
98
+ .value=${field.value}
99
+ .autofocus=${this.initFocus === field.name}
100
+ @load=${this._onInputLoad.bind(this)}
101
+ ></ox-custom-input>
102
+ `}
103
+ `
104
+ )}
105
+ </form>
106
+
107
+ <form-paginator .pageCount=${this.pageCount} @pageChange=${this._onPageChangeHandler.bind(this)}></form-paginator>
108
+ `
109
+ }
110
+
111
+ updated(changes: PropertyValues<this>) {
112
+ if (changes.has('fields')) {
113
+ this._checkInputValidity()
114
+ }
115
+ }
116
+
117
+ _checkInputValidity() {
118
+ const names = (this.fields || []).map(i => i.name)
119
+ const result = names.every(name => {
120
+ return names.indexOf(name) === names.lastIndexOf(name)
121
+ })
122
+ if (!result) {
123
+ throw new Error('Field name is duplicated.')
124
+ }
125
+ }
126
+
127
+ _onInputLoad() {
128
+ this.loadedCount++
129
+ if (this.loadedCount === (this.fields || []).length - 1) {
130
+ this._adjustColumnProperty()
131
+ this._initFocus()
132
+ this.dispatchEvent(new CustomEvent('load'))
133
+ }
134
+ }
135
+
136
+ getFields(): (CustomInput | CustomSelect)[] {
137
+ return Array.from(this.form.querySelectorAll('ox-custom-select, ox-custom-input') || [])
138
+ }
139
+
140
+ private currentPage: number = 1
141
+
142
+ _adjustColumnProperty() {
143
+ const inputElements = this.getFields()
144
+
145
+ if (inputElements.length === 0) return
146
+ const inputWidth = inputElements[0].offsetWidth
147
+ const totalWidth = window.innerWidth
148
+ const inputCount = inputElements.length
149
+ let columnCount =
150
+ Math.floor(totalWidth / inputWidth) < this.maxColumnCount
151
+ ? Math.floor(totalWidth / inputWidth)
152
+ : this.maxColumnCount > inputCount
153
+ ? inputCount
154
+ : this.maxColumnCount
155
+
156
+ if (columnCount === 1) {
157
+ this.pageCount = Math.ceil(inputElements.length / this.maxRowCount)
158
+ this._filterActiveFields(this.currentPage)
159
+ } else {
160
+ this.pageCount = 1
161
+ this._activeFields()
162
+ }
163
+
164
+ this.style.setProperty('--form-grid-template-columns', `repeat(${columnCount}, 1fr)`)
165
+ }
166
+
167
+ _initFocus() {
168
+ if (!this.initFocus) this.getFields()[0].focus()
169
+ }
170
+
171
+ getFieldByName(name: string) {
172
+ return this.renderRoot.querySelector(`[name=${name}]`)
173
+ }
174
+
175
+ reset() {
176
+ this.form && this.form.reset()
177
+ }
178
+
179
+ submit() {
180
+ this.dispatchEvent(new CustomEvent('submit'))
181
+ }
182
+
183
+ focusById(id: string) {
184
+ const input = this.renderRoot.querySelector(`form > #${id}`) as CustomInput | CustomSelect
185
+ input.focus()
186
+ }
187
+
188
+ checkValidity() {
189
+ return this.form.checkValidity()
190
+ }
191
+
192
+ serialize() {
193
+ let data: { [name: string]: any } = {}
194
+
195
+ this.getFields().forEach(field => {
196
+ const name = field.name
197
+ if (name) {
198
+ data[name] = field.value
199
+ }
200
+ })
201
+
202
+ return data
203
+ }
204
+
205
+ get searchParams() {
206
+ let searchParam = new URLSearchParams()
207
+ const data = this.serialize()
208
+ const fields = this.getFields()
209
+ fields.forEach(field => searchParam.append(field.name!, field.value))
210
+
211
+ return decodeURI(searchParam.toString())
212
+ }
213
+
214
+ _onPageChangeHandler(event: Event) {
215
+ this.currentPage = (event.currentTarget as FormPaginator).currentPage
216
+ this._filterActiveFields(this.currentPage)
217
+ }
218
+
219
+ _activeFields() {
220
+ this.getFields().forEach(field => (field.hidden = false))
221
+ }
222
+
223
+ _filterActiveFields(currentPage = 1) {
224
+ if (this.pageCount > 1) {
225
+ const fields = this.getFields()
226
+
227
+ const chunkedFields = []
228
+ for (let i = 0, j = fields.length; i < j; i += this.maxRowCount) {
229
+ chunkedFields.push(fields.slice(i, i + this.maxRowCount))
230
+ }
231
+
232
+ chunkedFields.forEach((fields, idx) => {
233
+ if (currentPage === idx + 1) {
234
+ fields.forEach(field => (field.hidden = false))
235
+ } else {
236
+ fields.forEach(field => (field.hidden = true))
237
+ }
238
+ })
239
+ }
240
+ }
241
+ }
@@ -0,0 +1,60 @@
1
+ import { LitElement, PropertyValues, css, html } from 'lit'
2
+ import { customElement, property, state } from 'lit/decorators.js'
3
+
4
+ @customElement('form-paginator')
5
+ export class FormPaginator extends LitElement {
6
+ static styles = css`
7
+ :host {
8
+ display: block;
9
+ background-color: var(--form-background-color, #e5e5e5);
10
+ font-size: 1.5em;
11
+ line-height: 0.5em;
12
+ text-align: center;
13
+ color: #c4c5c6;
14
+ }
15
+
16
+ [selected] {
17
+ color: #6f6f6f;
18
+ }
19
+ `
20
+
21
+ @property({ type: Number }) pageCount: number = 1
22
+ @property({ type: Array }) selectors: number[] = []
23
+
24
+ @state() public currentPage: number = 1
25
+
26
+ render() {
27
+ return html`
28
+ ${this.selectors.map(
29
+ page => html`
30
+ <span
31
+ page=${page}
32
+ ?selected=${page == this.currentPage}
33
+ @click=${(e: MouseEvent) => {
34
+ this.currentPage = page
35
+ this.dispatchEvent(new CustomEvent('pageChange'))
36
+ this.requestUpdate()
37
+ }}
38
+ >&bull;</span
39
+ >
40
+ `
41
+ )}
42
+ `
43
+ }
44
+
45
+ updated(changes: PropertyValues<this>) {
46
+ if (changes.has('pageCount')) {
47
+ if (this.pageCount > 1) {
48
+ let i = 1
49
+ this.selectors = []
50
+
51
+ while (i <= this.pageCount) {
52
+ this.selectors.push(i)
53
+ i++
54
+ }
55
+ } else {
56
+ this.selectors = []
57
+ }
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,46 @@
1
+ import { LitElement, css, html } from 'lit'
2
+ import { customElement, property } from 'lit/decorators.js'
3
+
4
+ @customElement('ox-search-form-paginator')
5
+ class SearchFormPaginator extends LitElement {
6
+ static styles = css`
7
+ :host {
8
+ display: block;
9
+ background-color: var(--form-background-color, #e5e5e5);
10
+ font-size: 1.5em;
11
+ line-height: 0.5em;
12
+ text-align: center;
13
+ color: #c4c5c6;
14
+ }
15
+
16
+ [selected] {
17
+ color: #6f6f6f;
18
+ }
19
+ `
20
+
21
+ @property({ type: Number }) pageCount: number = 0
22
+ @property({ type: Number }) currentPage: number = 0
23
+
24
+ render() {
25
+ return html`
26
+ ${Array(this.pageCount)
27
+ .fill('')
28
+ .map(
29
+ (_, page) => html`
30
+ <span
31
+ ?selected=${page == this.currentPage}
32
+ @click=${(e: MouseEvent) =>
33
+ this.dispatchEvent(
34
+ new CustomEvent('pagechanged', {
35
+ detail: {
36
+ page
37
+ }
38
+ })
39
+ )}
40
+ >&bull;</span
41
+ >
42
+ `
43
+ )}
44
+ `
45
+ }
46
+ }