@per-diem-calculator/vanilla 1.0.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.
- package/.prettierrc +17 -0
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/eslint.config.js +29 -0
- package/index.html +11 -0
- package/package.json +49 -0
- package/public/output.css +2503 -0
- package/src/css/_styles.css +8 -0
- package/src/css/colors.css +45 -0
- package/src/css/fonts.css +9 -0
- package/src/css/rows/_heights.css +6 -0
- package/src/css/rows/_index.css +15 -0
- package/src/css/rows/add.css +18 -0
- package/src/css/rows/animate-btns.css +18 -0
- package/src/css/rows/animate-row-close.css +18 -0
- package/src/css/rows/animate-row-open.css +14 -0
- package/src/css/rows/animate-row-other.css +5 -0
- package/src/css/rows/btn-add-row.css +41 -0
- package/src/css/rows/btn-delete.css +22 -0
- package/src/css/rows/btn-expenses-calculate.css +22 -0
- package/src/css/rows/btn-expenses-category.css +22 -0
- package/src/css/rows/delete.css +10 -0
- package/src/css/rows/details.css +22 -0
- package/src/css/rows/expense.css +18 -0
- package/src/css/rows/location.css +34 -0
- package/src/css/rows/summary.css +22 -0
- package/src/css/tom-select/defaults.css +530 -0
- package/src/css/tom-select/overrides.css +55 -0
- package/src/css/tw-shadow-props.css +50 -0
- package/src/index.ts +1 -0
- package/src/ts/components/Button/Button.ts +50 -0
- package/src/ts/components/Button/template.html +34 -0
- package/src/ts/components/ExpenseRow/ExpenseRow.ts +397 -0
- package/src/ts/components/ExpenseRow/template.html +260 -0
- package/src/ts/components/Label/Label.ts +45 -0
- package/src/ts/components/Label/template.html +1 -0
- package/src/ts/components/LocationCategory/LocationCategory.ts +226 -0
- package/src/ts/components/LocationCategory/template.html +520 -0
- package/src/ts/components/LocationDate/LocationDate.ts +366 -0
- package/src/ts/components/LocationDate/template.html +27 -0
- package/src/ts/components/LocationSelect/LocationSelect.ts +299 -0
- package/src/ts/components/LocationSelect/template.html +45 -0
- package/src/ts/components/index.ts +6 -0
- package/src/ts/controller.ts +193 -0
- package/src/ts/model.ts +163 -0
- package/src/ts/types/config.ts +22 -0
- package/src/ts/types/dates.ts +82 -0
- package/src/ts/types/expenses.ts +73 -0
- package/src/ts/types/locations.ts +25 -0
- package/src/ts/utils/config/configDefault.ts +13 -0
- package/src/ts/utils/config/index.ts +12 -0
- package/src/ts/utils/config/numbers.ts +24 -0
- package/src/ts/utils/config/sanitizeConfig.ts +39 -0
- package/src/ts/utils/dates/INPUT_DATE_MINMAX.ts +5 -0
- package/src/ts/utils/dates/YEAR_REGEX.ts +4 -0
- package/src/ts/utils/dates/getDateSlice.ts +54 -0
- package/src/ts/utils/dates/getValidAPIYear.ts +17 -0
- package/src/ts/utils/dates/index.ts +19 -0
- package/src/ts/utils/dates/isDateRaw.ts +90 -0
- package/src/ts/utils/dates/isShortMonth.ts +24 -0
- package/src/ts/utils/dates/isYYYY.ts +10 -0
- package/src/ts/utils/dates/offsetDateString.ts +17 -0
- package/src/ts/utils/expenses/INTL_MIE_RATES.ts +2125 -0
- package/src/ts/utils/expenses/createExpenseObjs.ts +35 -0
- package/src/ts/utils/expenses/getLodgingRateDomestic.ts +73 -0
- package/src/ts/utils/expenses/getLodgingRateIntl.ts +119 -0
- package/src/ts/utils/expenses/getMieRates.ts +84 -0
- package/src/ts/utils/expenses/index.ts +5 -0
- package/src/ts/utils/expenses/parseIntlLodgingRates.ts +124 -0
- package/src/ts/utils/expenses/returnValidStateExpense.ts +46 -0
- package/src/ts/utils/fetch/fetchJsonGSA.ts +29 -0
- package/src/ts/utils/fetch/fetchXmlDOD.ts +38 -0
- package/src/ts/utils/fetch/index.ts +3 -0
- package/src/ts/utils/fetch/memoize.ts +46 -0
- package/src/ts/utils/fetch/parseXml.ts +19 -0
- package/src/ts/utils/locations/getCitiesDomestic.ts +48 -0
- package/src/ts/utils/locations/getCitiesIntl.ts +63 -0
- package/src/ts/utils/locations/getCountriesDomestic.ts +237 -0
- package/src/ts/utils/locations/getCountriesIntl.ts +34 -0
- package/src/ts/utils/locations/index.ts +6 -0
- package/src/ts/utils/locations/keepUniqueLocations.ts +12 -0
- package/src/ts/utils/locations/locationKeys.ts +10 -0
- package/src/ts/utils/locations/returnValidStateLocation.ts +13 -0
- package/src/ts/utils/locations/sortLocations.ts +19 -0
- package/src/ts/utils/misc/USD.ts +4 -0
- package/src/ts/utils/misc/debounce.ts +22 -0
- package/src/ts/utils/misc/handlePointerDown.ts +3 -0
- package/src/ts/utils/misc/handlePointerUp.ts +22 -0
- package/src/ts/utils/misc/inPrimitiveType.ts +4 -0
- package/src/ts/utils/misc/index.ts +6 -0
- package/src/ts/utils/misc/wait.ts +4 -0
- package/src/ts/utils/styles/applyStyles.ts +19 -0
- package/src/ts/utils/styles/highlightInput.ts +15 -0
- package/src/ts/utils/styles/index.ts +3 -0
- package/src/ts/utils/styles/removeStyles.ts +14 -0
- package/src/ts/views/Expense/Expense.ts +465 -0
- package/src/ts/views/Expense/template.html +176 -0
- package/src/ts/views/Location/Location.ts +763 -0
- package/src/ts/views/Location/template-row.html +146 -0
- package/src/ts/views/Location/template.html +130 -0
- package/src/ts/views/index.ts +2 -0
- package/tsconfig.json +27 -0
- package/vite.config.ts +12 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
// Types
|
|
2
|
+
import type { DateRaw } from '../../types/dates';
|
|
3
|
+
|
|
4
|
+
// Utils
|
|
5
|
+
import { applyStyles, removeStyles } from '../../utils/styles';
|
|
6
|
+
import {
|
|
7
|
+
isDateRawType,
|
|
8
|
+
offsetDateString,
|
|
9
|
+
INPUT_DATE_MIN,
|
|
10
|
+
INPUT_DATE_MAX,
|
|
11
|
+
YEAR_MIN_REGEX,
|
|
12
|
+
YEAR_MAX_REGEX,
|
|
13
|
+
YEAR_INCOMPLETE_REGEX,
|
|
14
|
+
} from '../../utils/dates';
|
|
15
|
+
|
|
16
|
+
// HTML/CSS
|
|
17
|
+
import templateHTML from './template.html?raw';
|
|
18
|
+
|
|
19
|
+
// Template for this Custom Element
|
|
20
|
+
const template = document.createElement('template');
|
|
21
|
+
|
|
22
|
+
// Custom Element
|
|
23
|
+
export class PdcLocationDate extends HTMLElement {
|
|
24
|
+
/* SETUP
|
|
25
|
+
*/
|
|
26
|
+
static observedAttributes = ['bg'];
|
|
27
|
+
#attrName: 'start' | 'end';
|
|
28
|
+
#valid = false;
|
|
29
|
+
#styled = false;
|
|
30
|
+
#enabled = false;
|
|
31
|
+
constructor() {
|
|
32
|
+
super();
|
|
33
|
+
this.attachShadow({ mode: 'open' });
|
|
34
|
+
this.#attrName = this.getAttribute('pdc') === 'start' ? 'start' : 'end';
|
|
35
|
+
this.#styled = this.getAttribute('styled') === 'true';
|
|
36
|
+
|
|
37
|
+
if (this.#styled) {
|
|
38
|
+
template.innerHTML = templateHTML;
|
|
39
|
+
applyStyles(this.#shadowRoot);
|
|
40
|
+
} else template.innerHTML = removeStyles(templateHTML);
|
|
41
|
+
this.#shadowRoot.appendChild(template.content.cloneNode(true));
|
|
42
|
+
|
|
43
|
+
this.#label.setAttribute(
|
|
44
|
+
'text',
|
|
45
|
+
this.#attrName.charAt(0).toUpperCase() + this.#attrName.slice(1),
|
|
46
|
+
);
|
|
47
|
+
if (this.#attrName === 'start') this.restrictStartInput();
|
|
48
|
+
else this.restrictEndInput();
|
|
49
|
+
|
|
50
|
+
this.enableTabIndex(false);
|
|
51
|
+
this.#styleEl();
|
|
52
|
+
this.#addEventListeners();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* EVENTS
|
|
56
|
+
*/
|
|
57
|
+
attributeChangedCallback(
|
|
58
|
+
_name: string,
|
|
59
|
+
_oldValue: string,
|
|
60
|
+
newValue: string,
|
|
61
|
+
) {
|
|
62
|
+
if (!this.#styled) return;
|
|
63
|
+
this.#styleEl(`bg-${newValue === 'white' ? 'white' : 'neutral-50'}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#addEventListeners() {
|
|
67
|
+
this.#input.addEventListener('change', e => {
|
|
68
|
+
const target = e.target;
|
|
69
|
+
if (!(target instanceof HTMLInputElement)) return;
|
|
70
|
+
if (YEAR_INCOMPLETE_REGEX.test(target.value)) return;
|
|
71
|
+
this.handleInputChange(target.value);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* GET ELS
|
|
76
|
+
*/
|
|
77
|
+
get #label() {
|
|
78
|
+
const el = this.#shadowRoot.querySelector('pdc-label');
|
|
79
|
+
if (!el)
|
|
80
|
+
throw new Error(`Failed to render label in Date custom element`);
|
|
81
|
+
return el;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
get #input() {
|
|
85
|
+
const el = this.#shadowRoot.querySelector('input');
|
|
86
|
+
if (!el)
|
|
87
|
+
throw new Error(`Failed to render input in Date custom element`);
|
|
88
|
+
return el;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
get #shadowRoot() {
|
|
92
|
+
if (!this.shadowRoot)
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Failed to render shadowRoot in Category custom element`,
|
|
95
|
+
);
|
|
96
|
+
return this.shadowRoot;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
get #row() {
|
|
100
|
+
const el = this.closest<HTMLElement>('[data-pdc="location-row"]');
|
|
101
|
+
if (!el) throw new Error(`Failed to get row from Date custom element`);
|
|
102
|
+
return el;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
get #prevRow() {
|
|
106
|
+
const el = this.#row.previousElementSibling;
|
|
107
|
+
const startDate =
|
|
108
|
+
el?.querySelector<PdcLocationDate>('[pdc="start"]')?.pdcValue;
|
|
109
|
+
const endEl = el?.querySelector<PdcLocationDate>('[pdc="end"]');
|
|
110
|
+
const endDate = endEl?.pdcValue;
|
|
111
|
+
return { el, startDate, endEl, endDate };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
get #nextRow() {
|
|
115
|
+
const el = this.#row.nextElementSibling;
|
|
116
|
+
const startEl = el?.querySelector<PdcLocationDate>('[pdc="start"]');
|
|
117
|
+
const startDate = startEl?.pdcValue;
|
|
118
|
+
const endDate =
|
|
119
|
+
el?.querySelector<PdcLocationDate>('[pdc="end"]')?.pdcValue;
|
|
120
|
+
return { el, startEl, startDate, endDate };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
get #startEl() {
|
|
124
|
+
const el = this.#row.querySelector<PdcLocationDate>('[pdc="start"]');
|
|
125
|
+
if (!el)
|
|
126
|
+
throw new Error(
|
|
127
|
+
`Failed to get row startDateEl from Date custom element`,
|
|
128
|
+
);
|
|
129
|
+
return el;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
get #endEl() {
|
|
133
|
+
const el = this.#row.querySelector<PdcLocationDate>('[pdc="end"]');
|
|
134
|
+
if (!el)
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Failed to get row endDateEl from Date custom element`,
|
|
137
|
+
);
|
|
138
|
+
return el;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
get #errorEl() {
|
|
142
|
+
const el = this.closest('#locations-container')?.querySelector(
|
|
143
|
+
'#error',
|
|
144
|
+
);
|
|
145
|
+
if (!el)
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Failed to get View's error element from Category custom element`,
|
|
148
|
+
);
|
|
149
|
+
return el;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* VISUAL METHODS
|
|
153
|
+
*/
|
|
154
|
+
#styleEl(bgColor: 'bg-white' | 'bg-neutral-50' | null = null) {
|
|
155
|
+
if (!this.#styled) return;
|
|
156
|
+
const div = this.#shadowRoot.querySelector('div');
|
|
157
|
+
div?.classList.remove(`bg-white`, `bg-neutral-50`);
|
|
158
|
+
div?.classList.add(bgColor ? bgColor : `bg-${this.getAttribute('bg')}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
focusEl() {
|
|
162
|
+
this.#input.focus();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* UPDATE METHODS
|
|
166
|
+
*/
|
|
167
|
+
enableTabIndex(enable: boolean) {
|
|
168
|
+
this.#input.setAttribute('tabindex', enable ? '0' : '-1');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
enable(enable: boolean) {
|
|
172
|
+
if (enable) this.#input.removeAttribute('disabled');
|
|
173
|
+
else this.#input.setAttribute('disabled', 'true');
|
|
174
|
+
this.enableTabIndex(enable);
|
|
175
|
+
this.#enabled = enable;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#setMin(value: DateRaw | null = null) {
|
|
179
|
+
this.#input.setAttribute('min', value ? value : INPUT_DATE_MIN);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#setMax(value: DateRaw | null = null) {
|
|
183
|
+
this.#input.setAttribute('max', value ? value : INPUT_DATE_MAX);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#setAttr(value: DateRaw | null = null) {
|
|
187
|
+
if (value) this.setAttribute(this.#attrName, value);
|
|
188
|
+
else this.removeAttribute(this.#attrName);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
#setInputValue(value: DateRaw | null = null) {
|
|
192
|
+
this.#input.value = value ? value : '';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
#reset() {
|
|
196
|
+
this.#setInputValue();
|
|
197
|
+
this.#setAttr();
|
|
198
|
+
this.focusEl();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#resetEnd() {
|
|
202
|
+
this.#endEl.#setInputValue();
|
|
203
|
+
this.#endEl.#setAttr();
|
|
204
|
+
this.#endEl.restrictEndInput();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async handleInputChange(date: string | null = null) {
|
|
208
|
+
if (this.#styled) this.#input.classList.remove('success');
|
|
209
|
+
if (!this.#isValidInput(date) || !date) return;
|
|
210
|
+
if (!(await this.#isStartBeforeEnd()) || !isDateRawType(date)) return;
|
|
211
|
+
this.#setInputValue(date);
|
|
212
|
+
this.#setAttr(date);
|
|
213
|
+
await this.#startEl.restrictStartInput();
|
|
214
|
+
this.#endEl.restrictEndInput();
|
|
215
|
+
if (this.#styled) {
|
|
216
|
+
this.#input.classList.remove('error');
|
|
217
|
+
this.#input.classList.add('success');
|
|
218
|
+
this.renderError(false);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
#updateStartBasedOnPrevEnd() {
|
|
223
|
+
if (!this.#prevRow.endDate) return;
|
|
224
|
+
const correctStart = offsetDateString(this.#prevRow.endDate, 1);
|
|
225
|
+
if (this.#startDateInputVal === correctStart) return;
|
|
226
|
+
this.enable(false);
|
|
227
|
+
this.#setMin(correctStart);
|
|
228
|
+
this.#setInputValue(correctStart);
|
|
229
|
+
this.#setAttr(correctStart);
|
|
230
|
+
this.#input.classList.add('success');
|
|
231
|
+
this.#prevRow.endEl?.enable(false);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
#updateEndBasedOnNextStart() {
|
|
235
|
+
if (!this.#nextRow.startDate) return;
|
|
236
|
+
this.enable(false);
|
|
237
|
+
const correctEnd = offsetDateString(this.#nextRow.startDate, -1);
|
|
238
|
+
if (this.#endDateInputVal === correctEnd) return;
|
|
239
|
+
this.#setMax(correctEnd);
|
|
240
|
+
this.#setInputValue(correctEnd);
|
|
241
|
+
this.#setAttr(correctEnd);
|
|
242
|
+
this.#input.classList.add('success');
|
|
243
|
+
this.#nextRow.startEl?.enable(false);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/* VALIDATION
|
|
247
|
+
*/
|
|
248
|
+
renderError(
|
|
249
|
+
enable: boolean,
|
|
250
|
+
msg = `Enter a valid ${this.#attrName} date.`,
|
|
251
|
+
) {
|
|
252
|
+
if (enable) {
|
|
253
|
+
if (this.#styled) {
|
|
254
|
+
this.#input.classList.add('error');
|
|
255
|
+
this.#errorEl.classList.add('active');
|
|
256
|
+
}
|
|
257
|
+
this.#errorEl.textContent = msg;
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (!this.#styled) return;
|
|
261
|
+
this.#errorEl.classList.remove('active');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
#isValidInput(date: string | null = null) {
|
|
265
|
+
if (
|
|
266
|
+
!date ||
|
|
267
|
+
YEAR_MIN_REGEX.test(date) ||
|
|
268
|
+
YEAR_MAX_REGEX.test(date) ||
|
|
269
|
+
YEAR_INCOMPLETE_REGEX.test(date) ||
|
|
270
|
+
!isDateRawType(date)
|
|
271
|
+
) {
|
|
272
|
+
this.#reset();
|
|
273
|
+
if (this.#attrName === 'start') this.#resetEnd();
|
|
274
|
+
if (!date) return false;
|
|
275
|
+
if (YEAR_MIN_REGEX.test(date))
|
|
276
|
+
this.renderError(true, `Date must be after 2020`);
|
|
277
|
+
if (YEAR_MAX_REGEX.test(date))
|
|
278
|
+
this.renderError(true, `Date must be before 2041`);
|
|
279
|
+
if (YEAR_INCOMPLETE_REGEX.test(date))
|
|
280
|
+
this.renderError(true, `Enter a valid date.`);
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async #isStartBeforeEnd() {
|
|
287
|
+
if (!this.#startDateInputVal) return false;
|
|
288
|
+
if (!this.#endDateInputVal) return true;
|
|
289
|
+
if (
|
|
290
|
+
Date.parse(this.#startDateInputVal) >
|
|
291
|
+
Date.parse(this.#endDateInputVal)
|
|
292
|
+
) {
|
|
293
|
+
await this.#startEl.restrictStartInput();
|
|
294
|
+
this.#resetEnd();
|
|
295
|
+
this.#endEl.renderError(true, 'End date must be after start date.');
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async restrictStartInput() {
|
|
302
|
+
if (!this.#endEl.pdcValue && !this.#prevRow.el) {
|
|
303
|
+
this.enable(true);
|
|
304
|
+
this.#setMax();
|
|
305
|
+
this.#setMin();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (!this.#prevRow.el) this.enable(true);
|
|
309
|
+
if (this.#endEl.pdcValue) this.#setMax(this.#endEl.pdcValue);
|
|
310
|
+
this.#updateStartBasedOnPrevEnd();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
restrictEndInput() {
|
|
314
|
+
if (!this.#startEl.pdcValue) {
|
|
315
|
+
this.enable(false);
|
|
316
|
+
this.#setMin();
|
|
317
|
+
this.#setMax();
|
|
318
|
+
return;
|
|
319
|
+
} else {
|
|
320
|
+
this.enable(true);
|
|
321
|
+
this.#setMin(this.#startEl.pdcValue);
|
|
322
|
+
}
|
|
323
|
+
this.#updateEndBasedOnNextStart();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
validate() {
|
|
327
|
+
this.#valid = false;
|
|
328
|
+
const attr = this.getAttribute(this.#attrName);
|
|
329
|
+
if (!(attr && isDateRawType(attr))) {
|
|
330
|
+
this.renderError(true);
|
|
331
|
+
return this.#valid;
|
|
332
|
+
}
|
|
333
|
+
this.#valid = true;
|
|
334
|
+
return this.#valid;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/* GET DATA METHODS
|
|
338
|
+
*/
|
|
339
|
+
get inputValue() {
|
|
340
|
+
return this.#input.value;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
get #startDateInputVal() {
|
|
344
|
+
return (
|
|
345
|
+
this.#startEl.inputValue &&
|
|
346
|
+
isDateRawType(this.#startEl.inputValue)
|
|
347
|
+
) ?
|
|
348
|
+
this.#startEl.inputValue
|
|
349
|
+
: null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
get #endDateInputVal() {
|
|
353
|
+
return this.#endEl.inputValue && isDateRawType(this.#endEl.inputValue) ?
|
|
354
|
+
this.#endEl.inputValue
|
|
355
|
+
: null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
get isEnabled() {
|
|
359
|
+
return this.#enabled;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
get pdcValue() {
|
|
363
|
+
const value = this.getAttribute(this.#attrName);
|
|
364
|
+
return !!value && isDateRawType(value) ? value : null;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<div
|
|
2
|
+
class="group flex items-center justify-between border-b border-b-neutral-100
|
|
3
|
+
px-3"
|
|
4
|
+
>
|
|
5
|
+
<pdc-label styled="true"></pdc-label>
|
|
6
|
+
<input
|
|
7
|
+
tabindex="0"
|
|
8
|
+
disabled
|
|
9
|
+
type="date"
|
|
10
|
+
id="date"
|
|
11
|
+
min="2021-01-01"
|
|
12
|
+
max="2040-12-31"
|
|
13
|
+
required
|
|
14
|
+
class="[.error]:border-r-error-400 [.success]:border-r-success-400
|
|
15
|
+
focus-within:after:border-b-primary-800
|
|
16
|
+
webkit-calendar-picker-indicator
|
|
17
|
+
[&::-webkit-calendar-picker-indicator:focus]:border-b-primary-800
|
|
18
|
+
relative my-3 w-[68%] border-r-3 border-r-transparent bg-transparent
|
|
19
|
+
py-3 pr-3 text-neutral-800 opacity-100
|
|
20
|
+
transition-[border-color_opacity] after:absolute after:left-0
|
|
21
|
+
after:mt-1.5 after:w-full after:max-w-[95%] after:border-b-3
|
|
22
|
+
after:border-b-transparent after:transition-colors
|
|
23
|
+
after:content-['_'] focus:outline-none disabled:cursor-not-allowed
|
|
24
|
+
disabled:border-r-transparent disabled:!text-neutral-800/30
|
|
25
|
+
sm:w-[75%]"
|
|
26
|
+
/>
|
|
27
|
+
</div>
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// Types
|
|
2
|
+
import type { Location } from '../../types/locations';
|
|
3
|
+
|
|
4
|
+
// Utils
|
|
5
|
+
import { applyStyles, removeStyles } from '../../utils/styles';
|
|
6
|
+
import { debounce, handlePointerDown, handlePointerUp } from '../../utils/misc';
|
|
7
|
+
import TomSelect from 'tom-select';
|
|
8
|
+
|
|
9
|
+
// HTML/CSS
|
|
10
|
+
import templateHTML from './template.html?raw';
|
|
11
|
+
|
|
12
|
+
// Template for this Custom Element
|
|
13
|
+
const template = document.createElement('template');
|
|
14
|
+
|
|
15
|
+
// Custom Element
|
|
16
|
+
export class PdcLocationSelect extends HTMLElement {
|
|
17
|
+
/* SETUP
|
|
18
|
+
*/
|
|
19
|
+
static observedAttributes = ['bg'];
|
|
20
|
+
#attrName: 'country' | 'city';
|
|
21
|
+
#valid = false;
|
|
22
|
+
#styled = false;
|
|
23
|
+
#enabled = false;
|
|
24
|
+
#tomSelect: TomSelect;
|
|
25
|
+
constructor() {
|
|
26
|
+
super();
|
|
27
|
+
this.attachShadow({ mode: 'open' });
|
|
28
|
+
this.#attrName =
|
|
29
|
+
this.getAttribute('pdc') === 'country' ? 'country' : 'city';
|
|
30
|
+
this.#styled = this.getAttribute('styled') === 'true';
|
|
31
|
+
this.#tomSelect = this.#render();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#render() {
|
|
35
|
+
this.removeAttribute(this.#attrName);
|
|
36
|
+
this.#shadowRoot.innerHTML = '';
|
|
37
|
+
if (this.#styled) {
|
|
38
|
+
template.innerHTML = templateHTML;
|
|
39
|
+
applyStyles(this.#shadowRoot);
|
|
40
|
+
} else template.innerHTML = removeStyles(templateHTML);
|
|
41
|
+
this.#shadowRoot.appendChild(template.content.cloneNode(true));
|
|
42
|
+
this.#label.setAttribute(
|
|
43
|
+
'text',
|
|
44
|
+
this.#attrName === 'country' ? 'State' : 'City',
|
|
45
|
+
);
|
|
46
|
+
return this.#createTomSelect();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* EVENTS
|
|
50
|
+
*/
|
|
51
|
+
attributeChangedCallback(
|
|
52
|
+
_name: string,
|
|
53
|
+
_oldValue: string,
|
|
54
|
+
newValue: string,
|
|
55
|
+
) {
|
|
56
|
+
if (!this.#styled) return;
|
|
57
|
+
this.#styleEl(`bg-${newValue === 'white' ? 'white' : 'neutral-50'}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#addEventListeners() {
|
|
61
|
+
// Mouse, touch events
|
|
62
|
+
let pointerStartX = 0;
|
|
63
|
+
let pointerStartY = 0;
|
|
64
|
+
this.#tomSelect.control.addEventListener(
|
|
65
|
+
'pointerdown',
|
|
66
|
+
(e: PointerEvent) => {
|
|
67
|
+
const result = handlePointerDown(e);
|
|
68
|
+
pointerStartX = result.pointerStartX;
|
|
69
|
+
pointerStartY = result.pointerStartY;
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
this.#container.addEventListener('pointerup', (e: PointerEvent) => {
|
|
73
|
+
const result = handlePointerUp(
|
|
74
|
+
e,
|
|
75
|
+
this.#handleClicks.bind(this),
|
|
76
|
+
pointerStartX,
|
|
77
|
+
pointerStartY,
|
|
78
|
+
);
|
|
79
|
+
pointerStartX = result.pointerStartX;
|
|
80
|
+
pointerStartY = result.pointerStartY;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Keyboard events
|
|
84
|
+
this.#container.addEventListener('keydown', (e: KeyboardEvent) => {
|
|
85
|
+
if (!(e.key === 'Enter' || e.key === ' ')) return;
|
|
86
|
+
this.#handleClicks(e);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
this.#tomSelect.on('change', () => {
|
|
90
|
+
const value = this.#tomSelect.getValue();
|
|
91
|
+
if (Array.isArray(value)) return; // Ensures string value as TomSelect can return string[] if multiple selection enabled
|
|
92
|
+
this.setAttribute(this.#attrName, value);
|
|
93
|
+
|
|
94
|
+
if (this.#styled) {
|
|
95
|
+
this.#renderError(false);
|
|
96
|
+
this.#tomSelect.control.classList.remove('error');
|
|
97
|
+
this.#tomSelect.control.classList.add('success');
|
|
98
|
+
}
|
|
99
|
+
this.#tomSelect.control.setAttribute('tabindex', '-1');
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#handleClicks = (e: Event) => {
|
|
104
|
+
const target = e.target;
|
|
105
|
+
if (
|
|
106
|
+
!(
|
|
107
|
+
target instanceof HTMLElement &&
|
|
108
|
+
(target.closest('.ts-control') || target.closest('button'))
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
return;
|
|
112
|
+
const handler = debounce(() => {
|
|
113
|
+
this.#tomSelect.open();
|
|
114
|
+
});
|
|
115
|
+
handler();
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/* Get Els
|
|
119
|
+
*/
|
|
120
|
+
|
|
121
|
+
get #container() {
|
|
122
|
+
const el = this.#shadowRoot.querySelector('div');
|
|
123
|
+
if (!el)
|
|
124
|
+
throw new Error(
|
|
125
|
+
`Failed to get container for Select custom element`,
|
|
126
|
+
);
|
|
127
|
+
return el;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
get #select() {
|
|
131
|
+
const el = this.#shadowRoot.querySelector('select');
|
|
132
|
+
if (!el)
|
|
133
|
+
throw new Error(`Failed to get select for Select custom element`);
|
|
134
|
+
return el;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
get #button() {
|
|
138
|
+
const el = this.#shadowRoot.querySelector('button');
|
|
139
|
+
if (!el)
|
|
140
|
+
throw new Error(`Failed to get button for Select custom element`);
|
|
141
|
+
return el;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
get #label() {
|
|
145
|
+
const el = this.#shadowRoot.querySelector('pdc-label');
|
|
146
|
+
if (!el)
|
|
147
|
+
throw new Error(`Failed to get label for Select custom element`);
|
|
148
|
+
return el;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
get #loadingSpinner() {
|
|
152
|
+
const el = this.#shadowRoot.querySelector('#loading-spinner');
|
|
153
|
+
if (!el)
|
|
154
|
+
throw new Error(
|
|
155
|
+
`Failed to get loading spinner element for Select custom element`,
|
|
156
|
+
);
|
|
157
|
+
return el;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
get #errorEl() {
|
|
161
|
+
const el = this.closest('#locations-container')?.querySelector(
|
|
162
|
+
'#error',
|
|
163
|
+
);
|
|
164
|
+
if (!el)
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Failed to get View's error element from Category custom element`,
|
|
167
|
+
);
|
|
168
|
+
return el;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
get #shadowRoot() {
|
|
172
|
+
if (!this.shadowRoot)
|
|
173
|
+
throw new Error(
|
|
174
|
+
`Failed to render shadowRoot in Select custom element`,
|
|
175
|
+
);
|
|
176
|
+
return this.shadowRoot;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* VISUAL METHODS
|
|
180
|
+
*/
|
|
181
|
+
#styleEl(bgColor: 'bg-white' | 'bg-neutral-50' | null = null) {
|
|
182
|
+
if (!this.#styled) return;
|
|
183
|
+
const div = this.#shadowRoot.querySelector('div');
|
|
184
|
+
div?.classList.remove(`bg-white`, `bg-neutral-50`);
|
|
185
|
+
div?.classList.add(bgColor ? bgColor : `bg-${this.getAttribute('bg')}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
showLoadingSpinner(enabled: boolean) {
|
|
189
|
+
if (!this.#styled) return;
|
|
190
|
+
if (enabled) this.#loadingSpinner.classList.add('active');
|
|
191
|
+
else this.#loadingSpinner.classList.remove('active');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
focusEl() {
|
|
195
|
+
this.#tomSelect.control.focus();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* UPDATE METHODS
|
|
199
|
+
*/
|
|
200
|
+
enableTabIndex(enable: boolean) {
|
|
201
|
+
this.#button.setAttribute('tabindex', enable ? '0' : '-1');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
enable(enable: boolean) {
|
|
205
|
+
this.#render();
|
|
206
|
+
if (enable) {
|
|
207
|
+
this.#tomSelect.enable();
|
|
208
|
+
this.#container.classList.add('active');
|
|
209
|
+
} else {
|
|
210
|
+
this.#tomSelect.disable();
|
|
211
|
+
this.#container.classList.remove('active');
|
|
212
|
+
}
|
|
213
|
+
this.enableTabIndex(enable);
|
|
214
|
+
this.#enabled = enable;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
setOptions(locations: Location[]) {
|
|
218
|
+
locations.forEach(location => {
|
|
219
|
+
const value =
|
|
220
|
+
this.#attrName === 'country' ? location.country : location.city;
|
|
221
|
+
if (!location.label || !value)
|
|
222
|
+
throw new Error(
|
|
223
|
+
`Failed to get label when creating the options for ${location}.`,
|
|
224
|
+
);
|
|
225
|
+
const option = document.createElement('option');
|
|
226
|
+
option.setAttribute('value', value);
|
|
227
|
+
option.textContent = location.label;
|
|
228
|
+
this.#select.appendChild(option);
|
|
229
|
+
this.#tomSelect.sync();
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#createTomSelect() {
|
|
234
|
+
const noResultsText =
|
|
235
|
+
this.#attrName === 'city' ?
|
|
236
|
+
`No results
|
|
237
|
+
found.<br><br>Choose the first available option.<br><br>E.g. Standard Rate, [OTHER], etc.`
|
|
238
|
+
: `No results found.`;
|
|
239
|
+
const tomSelect = new TomSelect(this.#select, {
|
|
240
|
+
placeholder: `Select ${this.#attrName === 'country' ? 'state' : 'city'}`,
|
|
241
|
+
maxItems: 1,
|
|
242
|
+
plugins: ['dropdown_input'],
|
|
243
|
+
selectOnTab: true,
|
|
244
|
+
openOnFocus: false,
|
|
245
|
+
render: {
|
|
246
|
+
no_results: () => {
|
|
247
|
+
return /* HTML */ ` <div class="no-results">
|
|
248
|
+
${noResultsText}
|
|
249
|
+
</div>`;
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
this.#tomSelect = tomSelect;
|
|
254
|
+
this.#tomSelect.disable();
|
|
255
|
+
this.#tomSelect.tabIndex = -1;
|
|
256
|
+
this.enableTabIndex(false);
|
|
257
|
+
this.#addEventListeners();
|
|
258
|
+
this.#styleEl();
|
|
259
|
+
return tomSelect;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/* VALIDATION
|
|
263
|
+
*/
|
|
264
|
+
#renderError(enable: boolean) {
|
|
265
|
+
if (enable) {
|
|
266
|
+
if (this.#styled) {
|
|
267
|
+
this.#tomSelect.control.classList.add('error');
|
|
268
|
+
this.#errorEl.classList.add('active');
|
|
269
|
+
}
|
|
270
|
+
this.#errorEl.textContent =
|
|
271
|
+
this.#attrName === 'country' ? `Select state.` : `Select city.`;
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (!this.#styled) return;
|
|
275
|
+
this.#tomSelect.control.classList.remove('error');
|
|
276
|
+
this.#errorEl.classList.remove('active');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
validate() {
|
|
280
|
+
this.#valid = false;
|
|
281
|
+
if (!this.hasAttribute(this.#attrName)) {
|
|
282
|
+
if (this.#styled) this.#renderError(true);
|
|
283
|
+
return this.#valid;
|
|
284
|
+
}
|
|
285
|
+
this.#valid = true;
|
|
286
|
+
return this.#valid;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* GET DATA METHODS
|
|
290
|
+
*/
|
|
291
|
+
get isEnabled() {
|
|
292
|
+
return this.#enabled;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
get pdcValue() {
|
|
296
|
+
const value = this.getAttribute(this.#attrName);
|
|
297
|
+
return value ? value : null;
|
|
298
|
+
}
|
|
299
|
+
}
|