@programmerg/bs-elements 0.1.0

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.
@@ -0,0 +1,152 @@
1
+ import BsElement from "../core/BsElement.js";
2
+
3
+ let csrfToken = '';
4
+ export const setCsrfToken = (token) => {
5
+ csrfToken = token;
6
+ }
7
+
8
+ export async function httpRequest (opts) {
9
+ let url = opts.url,
10
+ method = opts.method ?? 'get',
11
+ contentType = '',
12
+ data = opts.data;
13
+
14
+ switch (opts.contentType) {
15
+ case 'text': contentType = 'text/plain'; break;
16
+ case 'multipart': contentType = 'multipart/form-data'; break;
17
+ case 'urlencoded': contentType = 'application/x-www-form-urlencoded'; break;
18
+ case 'json': default: contentType = 'application/json'; break;
19
+ }
20
+
21
+ // Populate opts from FORM attributes
22
+ if (opts.form && opts.form instanceof HTMLFormElement) {
23
+ url = opts.form.action;
24
+ method = opts.form.method ?? 'get';
25
+ if (opts.form.attributes.enctype) contentType = opts.form.attributes.enctype;
26
+ data = new FormData(opts.form);
27
+ }
28
+
29
+ // Append CSRF Token
30
+ if (!csrfToken) {
31
+ const resp = await fetch(url, {method: 'head'});
32
+ const newToken = resp.headers.get('x-csrf-token');
33
+ if (newToken) setCsrfToken(newToken);
34
+ }
35
+ if (data instanceof FormData) {
36
+ data.append('_token', csrfToken);
37
+ } else {
38
+ data._token = csrfToken;
39
+ }
40
+
41
+ // Convert data
42
+ if (contentType == 'application/json' || contentType == 'text/plain') {
43
+ if (data instanceof FormData) {
44
+ const output = {};
45
+ data.forEach((value, key) => output[key] = value);
46
+ data = output;
47
+ }
48
+ data = JSON.stringify(data);
49
+
50
+ } else { // multipart, urlencoded
51
+ if (!(data instanceof FormData)) {
52
+ const output = new FormData();
53
+ for (let key in item) {
54
+ output.append(key, item[key]);
55
+ }
56
+ data = output;
57
+ }
58
+ }
59
+
60
+ // Assemble the request
61
+ const request = new Request(url, {
62
+ method: method,
63
+ headers: {
64
+ "Accept": "application/json",
65
+ "Content-Type": contentType
66
+ },
67
+ body: data
68
+ });
69
+
70
+ // Append JWT Token
71
+ const jwtToken = localStorage.getItem('token');
72
+ if (jwtToken) request.headers.append("Authorization", "Bearer " + jwtToken);
73
+
74
+ // Get the response
75
+ const response = await fetch(request);
76
+
77
+ // Save new CSRF Token for later
78
+ const newToken = response.headers.get('x-csrf-token');
79
+ if (newToken) setCsrfToken(newToken);
80
+
81
+ if (!response.ok) {
82
+ throw new Error(response.statusText);
83
+ }
84
+
85
+ // const contentType = response.headers.get('content-type');
86
+ // if (contentType && contentType.includes('application/json')) {
87
+ // return response.json();
88
+ // } else {
89
+ // return response.text();
90
+ // }
91
+ return response.json();
92
+ }
93
+
94
+ export default class BsForm extends BsElement {
95
+
96
+ static get properties() {
97
+ return {
98
+ method: {type: String},
99
+ action: {type: String},
100
+ submit: {},
101
+ controls: {},
102
+ _wasChanged: {type: Boolean, state: false}
103
+ }
104
+ }
105
+
106
+ connectedCallback() {
107
+ super.connectedCallback();
108
+ this.addEventListener('change', this.changeListener);
109
+ }
110
+
111
+ disconnectedCallback() {
112
+ this.removeEventListener('change', this.changeListener);
113
+ super.disconnectedCallback();
114
+ }
115
+
116
+ changeListener(e) {
117
+ this._wasChanged = true;
118
+ }
119
+
120
+ handleSubmit(e) {
121
+ const form = e.target;
122
+ form.classList.add('was-validated');
123
+ if (form.checkValidity() === false) {
124
+ e.preventDefault();
125
+ return false;
126
+ }
127
+
128
+ if (this.submit) {
129
+ return this.submit(e);
130
+
131
+ } else {
132
+ httpRequest({form: form});
133
+ e.preventDefault();
134
+ return false;
135
+ }
136
+ }
137
+
138
+ firstUpdated() {
139
+ return html`
140
+ <form
141
+ method=${this.method}
142
+ action=${this.action}
143
+ @submit=${this.handleSubmit}
144
+ class="needs-validation"
145
+ >
146
+ ${this.controls}
147
+ </form>
148
+ `;
149
+ }
150
+ }
151
+
152
+ customElements.define("bs-form", BsForm);
@@ -0,0 +1,209 @@
1
+ import BsElement from '../core/BsElement';
2
+
3
+ export class BsInput extends BsElement {
4
+
5
+ static get properties() {
6
+ return {
7
+ name: {type: String},
8
+ value: {},
9
+ label: {type: String},
10
+ type: {type: String},
11
+ title: {type: String},
12
+ options: {type: Array},
13
+ placeholder: {type: String},
14
+ checked: {type: Boolean},
15
+ readonly: {type: Boolean},
16
+ required: {type: Boolean},
17
+ disabled: {type: Boolean},
18
+ autofocus: {type: Boolean},
19
+ multiple: {type: Boolean},
20
+ variant: {type: String},
21
+ sizing: {type: String},
22
+ size: {type: Number},
23
+ maxlength: {type: Number},
24
+ minlength: {type: Number},
25
+ min: {type: Number},
26
+ max: {type: Number},
27
+ step: {type: Number},
28
+ pattern: {type: String},
29
+ accept: {type: String},
30
+ autocomplete: {type: Boolean},
31
+ content: {},
32
+ }
33
+ }
34
+
35
+ firstUpdated() {
36
+ if (this.variant == 'floating') {
37
+ this.classList.add('form-floating');
38
+ this.classList.add('d-block');
39
+ }
40
+ if (this.variant == 'horizontal') {
41
+ this.classList.add('row');
42
+ }
43
+ const id = "input_" + Math.random().toString(36).substr(2);
44
+
45
+ this.name = this.name ?? "";
46
+ this.value = this.value ?? "";
47
+ this.label = this.label ?? this.name;
48
+ this.options = this.options ?? [];
49
+
50
+ if (!this.type) this.type = typeof this.value;
51
+ if (this.type == 'boolean') this.type = 'checkbox';
52
+ if (this.type == 'number') this.type = 'number';
53
+ if (this.type == 'string') this.type = 'text';
54
+ if (this.type == 'object' && this.value instanceof Date) this.type = 'datetime-local';
55
+
56
+ const label = html`
57
+ <label for=${id} class=${
58
+ (this.variant == '' ? "form-label" : "") +
59
+ (this.variant == 'horizontal' ? "col-sm-3 col-form-label" : "") +
60
+ (this.sizing ? ' col-form-label-' + this.sizing : '')
61
+ }>
62
+ ${(this.type == 'checkbox') ? nothing : this.label}
63
+ </label>
64
+ `;
65
+
66
+ let control;
67
+ switch (this.type) {
68
+ case 'custom':
69
+ control = html`${this.content}`
70
+ break;
71
+
72
+ case 'group':
73
+ control = html`<div class=${"input-group" + (this.sizing ? " input-group-"+this.sizing : "")}>${this.content}</div>`
74
+ break;
75
+
76
+ case 'radio':
77
+ control = this.options.map(option => html`
78
+ <div class="form-check">
79
+ <input id="${id}_${option.value}"
80
+ class="form-check-input"
81
+ type="radio"
82
+ name=${this.name}
83
+ value=${option.value}
84
+ ?checked=${(option.checked || this.value === option.value) ?? false}
85
+ >
86
+ <label for=${id + "_" + option.value} class="form-check-label">${option.text ?? option.value}</label>
87
+ </div>`);
88
+ break;
89
+
90
+ case 'checkbox':
91
+ control = html`
92
+ <div class="form-check">
93
+ <input id=${id}
94
+ class="form-check-input"
95
+ type="checkbox"
96
+ name=${this.name}
97
+ value=${this.value}
98
+ ?checked=${this.checked ?? false}
99
+ >
100
+ <label for=${id} class="form-check-label">${this.label}</label>
101
+ </div>`;
102
+ break;
103
+
104
+ case 'textarea':
105
+ control = html`
106
+ <textarea id=${id}
107
+ class=${"form-control" + (this.sizing ? ' form-control-' + this.sizing : '')}
108
+ name=${this.name}
109
+ .value=${this.value}
110
+ placeholder=${this.placeholder ?? nothing}
111
+ ?readonly=${this.readonly ?? false}
112
+ ?required=${this.required ?? false}
113
+ ?disabled=${this.disabled ?? false}
114
+ ?autofocus=${this.autofocus ?? false}
115
+ cols=${this.size ?? nothing}
116
+ maxlength=${this.maxlength ?? nothing}
117
+ ></textarea>`;
118
+ break;
119
+
120
+ case 'select':
121
+ control = html`
122
+ <select id=${id}
123
+ class=${'form-select' + (this.sizing ? ' form-select-' + this.sizing : '')}
124
+ name=${this.name}
125
+ placeholder=${this.placeholder ?? nothing}
126
+ ?required=${this.required ?? false}
127
+ ?disabled=${this.disabled ?? false}
128
+ ?autofocus=${this.autofocus ?? false}
129
+ ?multiple=${this.multiple ?? false}
130
+ size=${this.size ?? nothing}
131
+ >
132
+ ${this.options.map(option => html`
133
+ <option value=${option.value ?? ""} ?checked=${option.checked || this.value === option.value}>
134
+ ${option.text ?? option.value}
135
+ </option>`
136
+ )}
137
+ </select>`;
138
+ break;
139
+
140
+ case 'text':
141
+ case 'url':
142
+ case 'email':
143
+ case 'password':
144
+ case 'file':
145
+ case 'hidden':
146
+ case 'number':
147
+ case 'range':
148
+ case 'color':
149
+ case 'tel':
150
+ case 'date':
151
+ case' datetime-local':
152
+ case 'week':
153
+ case 'month':
154
+ case 'time':
155
+ default:
156
+ control = html`
157
+ <input id=${id}
158
+ class=${(this.type == 'range' ? 'form-range' : "form-control") +
159
+ (this.type == 'color' ? ' form-control-color' : "") +
160
+ (this.sizing ? ' form-control-' + this.sizing : '')
161
+ }
162
+ type=${this.type}
163
+ name=${this.name}
164
+ .value=${this.value}
165
+ list=${this.options.length > 0 ? id + "_list" : nothing}
166
+ placeholder=${this.placeholder ?? nothing}
167
+ pattern=${this.pattern ?? nothing}
168
+ accept=${this.accept ?? nothing}
169
+ ?readonly=${this.readonly ?? false}
170
+ ?required=${this.required ?? false}
171
+ ?disabled=${this.disabled ?? false}
172
+ ?autofocus=${this.autofocus ?? false}
173
+ ?multiple=${this.multiple ?? false}
174
+ autocomplete=${!!this.autocomplete ? 'off' : nothing}
175
+ size=${this.size ?? nothing}
176
+ min=${this.min ?? nothing}
177
+ max=${this.max ?? nothing}
178
+ step=${this.step ?? nothing}
179
+ maxlength=${this.maxlength ?? nothing}
180
+ minlength=${this.minlength ?? nothing}
181
+ title=${this.title ?? nothing}
182
+ >
183
+ ${this.options.length > 0 ? html`
184
+ <datalist id=${id + "_list"}>
185
+ ${this.options.map(option => html`
186
+ <option value=${option.value ?? ""}></option>
187
+ `)}
188
+ </datalist>` : ''}`;
189
+ break;
190
+ }
191
+
192
+ return (this.variant == 'floating')
193
+ ? html`
194
+ ${control}
195
+ ${label}
196
+ ${this.title ? html`<div class="invalid-feedback">${this.title}</div>` : nothing}
197
+ `
198
+ : html`
199
+ ${label}
200
+ <div class=${this.variant == 'horizontal' ? "col-sm-9" : ""}>
201
+ ${control}
202
+ ${this.title ? html`<div class="invalid-feedback">${this.title}</div>` : nothing}
203
+ </div>
204
+ `
205
+ ;
206
+ }
207
+ }
208
+
209
+ customElements.define('bs-input', BsInput);
@@ -0,0 +1,210 @@
1
+ import BsElement from "../core/BsElement.js";
2
+
3
+ export default class BsTable extends BsElement {
4
+
5
+ static get properties() {
6
+ return {
7
+ tableClass: {type: String},
8
+ editable: {type: Boolean},
9
+ filterable: {type: Boolean},
10
+ src: {type: String},
11
+ data: {type: Array},
12
+
13
+ columns: { type: Array },
14
+ index: { type: Number, state: true },
15
+ unique: { type: Array, state: true },
16
+ filter: { type: Array, state: true },
17
+ hidden: { type: Array, state: true }
18
+ }
19
+ }
20
+
21
+ constructor() {
22
+ super();
23
+ this.tableClass = '';
24
+ this.editable = false;
25
+ this.filterable = false;
26
+ this.src = '';
27
+ this.data = [];
28
+
29
+ this.columns = [];
30
+ this.index = null;
31
+ this.unique = [];
32
+ this.filter = [];
33
+ this.hidden = [];
34
+ }
35
+
36
+ async firstUpdated() {
37
+ if (!this.data) await this.fetchData();
38
+ }
39
+
40
+ // Download the latest json and update it locally
41
+ async fetchData() {
42
+ let _data;
43
+ if (this.src.length > 0) {
44
+ // If a src attribute is set prefer it over any slots
45
+ _data = await fetch(this.src).then((res) => res.json());
46
+ } else {
47
+ // If no src attribute is set then grab the inline json in the slot
48
+ const elem = this.parentElement?.querySelector(
49
+ 'script[type="application/json"]'
50
+ );
51
+ if (elem) _data = JSON.parse(elem.innerHTML);
52
+ }
53
+ this.data = this.transform(_data ?? []);
54
+ this.requestUpdate();
55
+ }
56
+
57
+ transform(data) {
58
+ if (!this.columns) {
59
+ this.columns = Object.keys(this.data[0]).map(key => {
60
+ return {key: key, label: key}
61
+ });
62
+ }
63
+ return data;
64
+ }
65
+
66
+ renderFilter() {
67
+ return html`
68
+ <input type="search">
69
+ <ul>
70
+ <li class=${(this.filter[this.index] === undefined) ? 'disable' : ''} @click=${this.onApply}></li>
71
+ ${this.unique.map(cell => {
72
+ const hidden = this.filter[this.index] !== undefined && this.filter[this.index] !== cell;
73
+ return html`
74
+ <li class=${hidden ? 'disable' : ''} @click=${this.onApply}>${cell}</li>
75
+ `;
76
+ })}
77
+ </ul>
78
+ `;
79
+ }
80
+
81
+ onInput (e) {
82
+ const value = e.target.value;
83
+ const key = row[0];
84
+ const current = this.data[index];
85
+ current[key] = value;
86
+ this.data[index] = current;
87
+ const args = {
88
+ detail: {
89
+ index: index,
90
+ data: current,
91
+ },
92
+ };
93
+ this.requestUpdate();
94
+
95
+ if (this.handleInputCell)
96
+ this.handleInputCell(args);
97
+ else
98
+ this.dispatchEvent(
99
+ new CustomEvent("input-cell", args)
100
+ );
101
+ }
102
+
103
+ renderColumns () {
104
+ return html`
105
+ <tr>
106
+ ${this.columns.map((cell, index) => {
107
+ return html`
108
+ <th data-key=${cell.key} class=${(this.filter[index] !== undefined) ? 'active' : ''}>
109
+ ${cell.label}
110
+ <i class=${this.index === index ? 'arrow_drop_up' : 'arrow_drop_down'} @click=${this.onSelect}></i>
111
+ <div>
112
+ ${this.filterable && this.index === index ? this.renderFilter() : ''}
113
+ </div>
114
+ </th>
115
+ `
116
+ })}
117
+ </tr>
118
+ `;
119
+ }
120
+
121
+ renderRow(entry, index) {
122
+ const matches = entry.every((cell, index) => {
123
+ return this.filter[index] === undefined || this.filter[index] == cell;
124
+ });
125
+ if(matches) {
126
+ this.hidden[index] = true;
127
+ return html`
128
+ <tr>
129
+ ${entry.map(cell => {
130
+ return html`
131
+ <td>
132
+ ${this.editable
133
+ ? html`<input value="${cell}" type="text" @input=${this.onInput} />`
134
+ : html`${cell}`
135
+ }
136
+ </td>
137
+ `;
138
+ })}
139
+ </tr>
140
+ `;
141
+ }
142
+ delete this.hidden[index];
143
+ return '';
144
+ }
145
+
146
+ onSelect(event) {
147
+ if(this.head.includes(event.target)) {
148
+ const index = this.head.indexOf(event.target);
149
+ if(this.index === null || this.index !== index) {
150
+ this.index = this.head.indexOf(event.target);
151
+ let column = this.data.map(row => row[index]);
152
+ // column = column.filter((row, index) => this.hidden[index]);
153
+ this.unique = [...new Set(column)];
154
+ }
155
+ else {
156
+ this.index = null;
157
+ }
158
+ }
159
+ }
160
+
161
+ onApply(event) {
162
+ if(!event.target.classList.contains('disable')) {
163
+ const column = event.target.parentNode.parentNode;
164
+ const index = this.head.indexOf(column);
165
+ const value = event.target.textContent;
166
+ value === '' ? delete this.filter[index] : this.filter[index] = value;
167
+ this.index = null;
168
+ }
169
+ }
170
+
171
+ get table() {
172
+ return this.renderRoot.querySelector('table');
173
+ }
174
+
175
+ get head() {
176
+ return Array.from(this.table.querySelectorAll('thead th'));
177
+ }
178
+
179
+ get body() {
180
+ return Array.from(this.table.querySelectorAll('tbody tr'));
181
+ }
182
+
183
+ firstUpdated() {
184
+ return html`
185
+ <div class="table-responsive">
186
+ <slot name="a"></slot>
187
+ <table class=${"table " + this.tableClass}>
188
+ <thead>
189
+ ${this.renderColumns()}
190
+ </thead>
191
+ <tbody>
192
+ ${!this.data
193
+ ? html`<tr><td colspan=${this.columns.length}>Loading...</td></tr>`
194
+ : nothing
195
+ }
196
+ ${this.data.length === 0
197
+ ? html`<tr><td colspan=${this.columns.length}>No Items Found!</td></tr>`
198
+ : nothing
199
+ }
200
+ ${this.data.map(
201
+ this.renderRow
202
+ )}
203
+ </tbody>
204
+ </table>
205
+ </div>
206
+ `;
207
+ }
208
+ }
209
+
210
+ customElements.define("bs-table", BsTable);
File without changes
File without changes
@@ -0,0 +1,14 @@
1
+ import BsElement from "../core/BsElement.js";
2
+
3
+ export default class BsTree extends BsElement {
4
+
5
+ static get properties() {
6
+ return {}
7
+ }
8
+
9
+ firstUpdated() {
10
+
11
+ }
12
+ }
13
+
14
+ customElements.define("bs-tree", BsTree);