stalefish 8.0.10 → 8.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.
- package/components/checkbox.mjs +2 -2
- package/components/datePicker.mjs +346 -0
- package/components/dropdown.mjs +32 -10
- package/components/fab.mjs +1 -1
- package/components/selectbox.mjs +5 -4
- package/components/textarea.mjs +3 -2
- package/components/textfield.mjs +20 -14
- package/components/timePicker.mjs +650 -0
- package/components/uploader.mjs +498 -5
- package/example/README.md +28 -0
- package/example/app.mjs +288 -0
- package/example/index.html +62 -0
- package/example/server.mjs +81 -0
- package/example/ssr-browser-stub.js +8 -0
- package/index.mjs +4 -2
- package/package.json +3 -2
- package/components/dateTimePicker.mjs +0 -357
package/components/checkbox.mjs
CHANGED
|
@@ -30,10 +30,10 @@ let styles = css`
|
|
|
30
30
|
`
|
|
31
31
|
|
|
32
32
|
export default ({ wrapperStyle, holdingPen, label, property, required, indeterminate, disabled, darkBackground, onchange }) => {
|
|
33
|
-
let checkboxEl = html`<input data-gramm="false"
|
|
33
|
+
let checkboxEl = html`<input data-gramm="false" ?disabled=${disabled} style="${disabled ? 'cursor: not-allowed;' : ''}" class="${styles.checkbox} ${fieldIsTouched(holdingPen, property) === true ? styles.touched : ''}" value="${holdingPen[property] === true ? 'true' : null}" ?checked=${holdingPen[property] === true} onchange=${e => {
|
|
34
34
|
formField(holdingPen, property)(e)
|
|
35
35
|
onchange && onchange(e)
|
|
36
|
-
}} type="checkbox"
|
|
36
|
+
}} type="checkbox" ?required=${required} />`
|
|
37
37
|
|
|
38
38
|
checkboxEl.indeterminate = indeterminate || false
|
|
39
39
|
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { html, css, formField, fieldIsTouched, rerender } from 'halfcab'
|
|
2
|
+
import calendarIcon from './icons/calendarIcon.mjs'
|
|
3
|
+
import solidDown from './icons/solidDown.mjs'
|
|
4
|
+
|
|
5
|
+
// Styles aligned with timePicker and prior design tweaks
|
|
6
|
+
const styles = css`
|
|
7
|
+
.textfield {
|
|
8
|
+
padding: 10px;
|
|
9
|
+
border: solid 5px #c9c9c9;
|
|
10
|
+
transition: border 0.3s;
|
|
11
|
+
outline: none;
|
|
12
|
+
width: 100%;
|
|
13
|
+
font-size: 18px;
|
|
14
|
+
border-radius: 0;
|
|
15
|
+
box-shadow: none !important;
|
|
16
|
+
font-weight: normal;
|
|
17
|
+
box-sizing: border-box;
|
|
18
|
+
font-family: inherit;
|
|
19
|
+
line-height: 1.4em;
|
|
20
|
+
-webkit-appearance: none;
|
|
21
|
+
-moz-appearance: none;
|
|
22
|
+
appearance: none;
|
|
23
|
+
z-index: 20;
|
|
24
|
+
position: relative;
|
|
25
|
+
height: 55px;
|
|
26
|
+
background-color: #FFF;
|
|
27
|
+
color: #999;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.noType {
|
|
31
|
+
caret-color: transparent;
|
|
32
|
+
user-select: none;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.textfield::placeholder { color: #999; }
|
|
36
|
+
|
|
37
|
+
.label {
|
|
38
|
+
transition: opacity 0.75s;
|
|
39
|
+
border-top-right-radius: 5px;
|
|
40
|
+
border-top-left-radius: 5px;
|
|
41
|
+
user-select: none;
|
|
42
|
+
position: absolute;
|
|
43
|
+
top: -55px;
|
|
44
|
+
z-index: 10;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.icon {
|
|
48
|
+
position: absolute;
|
|
49
|
+
right: 12px;
|
|
50
|
+
top: 17px;
|
|
51
|
+
width: 20px;
|
|
52
|
+
height: 20px;
|
|
53
|
+
opacity: 0.8;
|
|
54
|
+
z-index: 25;
|
|
55
|
+
pointer-events: none;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.withRightIcon { padding-right: 44px; }
|
|
59
|
+
|
|
60
|
+
.clear {
|
|
61
|
+
cursor: pointer;
|
|
62
|
+
position: absolute;
|
|
63
|
+
color: #AAA;
|
|
64
|
+
font-size: 0.8em;
|
|
65
|
+
line-height: 0.9em;
|
|
66
|
+
font-weight: normal;
|
|
67
|
+
box-sizing: border-box;
|
|
68
|
+
right: 42px; /* 10px gap from calendar icon at right:12px, width 20px */
|
|
69
|
+
background-color: #EEE;
|
|
70
|
+
padding: 5px 10px;
|
|
71
|
+
z-index: 30;
|
|
72
|
+
border-radius: 3px;
|
|
73
|
+
top: 17px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.popup { position: absolute; z-index: 9999; top: 55px; left: 0; background: #fff; border: 1px solid #c9c9c9; box-shadow: 0 2px 10px rgba(0,0,0,.15); padding: 4px; color: #999; }
|
|
77
|
+
.calHeader { display: flex; align-items: center; justify-content: space-between; padding: 4px 6px; }
|
|
78
|
+
.navBtn { background: transparent; border: none; cursor: pointer; padding: 4px; color: #ccc; }
|
|
79
|
+
.navBtn:focus { outline: none; }
|
|
80
|
+
.navBtn:focus-visible { outline: 2px solid #c9c9c9; outline-offset: 2px; }
|
|
81
|
+
.monthLabel { position: relative; top: -2px; font-weight: 500; }
|
|
82
|
+
.arrowIcon { display: inline-block; width: 13px; height: 13px; color: #c9c9c9; line-height: 0; }
|
|
83
|
+
.arrowIcon > svg { display: block; width: 13px; height: 13px; }
|
|
84
|
+
.yearWrap { display: inline-flex; align-items: center; gap: 8px; }
|
|
85
|
+
.yearNum { color: #787878; min-width: 3ch; text-align: right; }
|
|
86
|
+
.yearControls { display: inline-flex; flex-direction: column; align-items: center; gap: 0; margin-left: 2px; }
|
|
87
|
+
.yearBtn { background: transparent; border: none; cursor: pointer; padding: 0; line-height: 1; width: 18px; height: 14px; display: flex; align-items: center; justify-content: center; }
|
|
88
|
+
.yearBtn + .yearBtn { margin-top: -2px; }
|
|
89
|
+
.yearBtn:focus { outline: none; }
|
|
90
|
+
.yearBtn:focus-visible { outline: 2px solid #c9c9c9; outline-offset: 2px; }
|
|
91
|
+
.yearArrowIcon { display: block; width: 12px; height: 12px; color: #c9c9c9; line-height: 0; }
|
|
92
|
+
.yearArrowIcon > svg { display: block; width: 12px; height: 12px; }
|
|
93
|
+
.weekHead { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; padding: 2px 6px; font-size: 12px; opacity: 0.8; }
|
|
94
|
+
.grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; padding: 0 6px 6px; }
|
|
95
|
+
`
|
|
96
|
+
|
|
97
|
+
// Internal symbols for per-holdingPen state
|
|
98
|
+
const STATE_SYMBOL = Symbol.for('stalefish.date.state')
|
|
99
|
+
const ID_MAP_SYMBOL = Symbol.for('stalefish.date.idMap')
|
|
100
|
+
|
|
101
|
+
function pad2 (n) { return (n < 10 ? '0' : '') + n }
|
|
102
|
+
|
|
103
|
+
function parseYMD (str) {
|
|
104
|
+
if (!str || typeof str !== 'string') return null
|
|
105
|
+
const m = /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.exec(str)
|
|
106
|
+
if (!m) return null
|
|
107
|
+
const y = parseInt(m[1], 10)
|
|
108
|
+
const mo = parseInt(m[2], 10) - 1
|
|
109
|
+
const d = parseInt(m[3], 10)
|
|
110
|
+
const dt = new Date(y, mo, d)
|
|
111
|
+
if (dt.getFullYear() !== y || dt.getMonth() !== mo || dt.getDate() !== d) return null
|
|
112
|
+
return dt
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function toYMD (date) {
|
|
116
|
+
if (!(date instanceof Date)) return ''
|
|
117
|
+
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Lightweight moment-style formatter for display only
|
|
121
|
+
// Supported tokens: YYYY, YY, MMMM, MMM, MM, M, DD, D
|
|
122
|
+
// This avoids adding a dependency while covering common needs
|
|
123
|
+
const MONTH_NAMES_FULL = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
|
|
124
|
+
const MONTH_NAMES_SHORT = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
|
125
|
+
|
|
126
|
+
function formatDate (date, fmt = 'YYYY-MM-DD') {
|
|
127
|
+
if (!(date instanceof Date)) return ''
|
|
128
|
+
const Y = date.getFullYear()
|
|
129
|
+
const M = date.getMonth() + 1
|
|
130
|
+
const D = date.getDate()
|
|
131
|
+
// Build a token map and replace via a single regex pass to prevent
|
|
132
|
+
// cascading replacements (e.g., replacing D inside Dec from MMM).
|
|
133
|
+
const tokenMap = {
|
|
134
|
+
YYYY: String(Y),
|
|
135
|
+
YY: String(Y).slice(-2),
|
|
136
|
+
MMMM: MONTH_NAMES_FULL[M - 1],
|
|
137
|
+
MMM: MONTH_NAMES_SHORT[M - 1],
|
|
138
|
+
MM: pad2(M),
|
|
139
|
+
M: String(M),
|
|
140
|
+
DD: pad2(D),
|
|
141
|
+
D: String(D)
|
|
142
|
+
}
|
|
143
|
+
// Longest tokens first in alternation to ensure correct matching
|
|
144
|
+
const re = /(YYYY|MMMM|MMM|YY|MM|M|DD|D)/g
|
|
145
|
+
return fmt.replace(re, (match) => tokenMap[match] ?? match)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function ensureState (holdingPen, property) {
|
|
149
|
+
if (!holdingPen) return {}
|
|
150
|
+
if (!holdingPen[STATE_SYMBOL]) {
|
|
151
|
+
Object.defineProperty(holdingPen, STATE_SYMBOL, { value: {}, enumerable: false })
|
|
152
|
+
}
|
|
153
|
+
const map = holdingPen[STATE_SYMBOL]
|
|
154
|
+
if (!map[property]) {
|
|
155
|
+
map[property] = { open: false, viewMonth: null, viewYear: null, tmpDate: null }
|
|
156
|
+
}
|
|
157
|
+
return map[property]
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function ensureId (holdingPen, property, uniqueKey) {
|
|
161
|
+
if (!holdingPen) return `sf-date-${property || Math.random().toString(36).slice(2)}`
|
|
162
|
+
if (!holdingPen[ID_MAP_SYMBOL]) {
|
|
163
|
+
Object.defineProperty(holdingPen, ID_MAP_SYMBOL, { value: {}, enumerable: false })
|
|
164
|
+
}
|
|
165
|
+
const map = holdingPen[ID_MAP_SYMBOL]
|
|
166
|
+
if (!map[property]) {
|
|
167
|
+
map[property] = `sf-date-${property}-${Math.random().toString(36).slice(2)}`
|
|
168
|
+
}
|
|
169
|
+
return `sf-date-${uniqueKey || map[property]}`
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function commitValue ({ holdingPen, property, onchange, valueStr }) {
|
|
173
|
+
const fauxE = { currentTarget: { validity: { valid: true }, value: valueStr } }
|
|
174
|
+
formField(holdingPen, property)(fauxE)
|
|
175
|
+
onchange && onchange(fauxE)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function getMonthMatrix (year, month) {
|
|
179
|
+
const first = new Date(year, month, 1)
|
|
180
|
+
const startDay = first.getDay() // 0-6 Sun-Sat
|
|
181
|
+
const daysInMonth = new Date(year, month + 1, 0).getDate()
|
|
182
|
+
const prevMonthDays = new Date(year, month, 0).getDate()
|
|
183
|
+
const cells = []
|
|
184
|
+
// Fill leading blanks from previous month
|
|
185
|
+
for (let i = 0; i < startDay; i++) {
|
|
186
|
+
cells.push({ d: prevMonthDays - startDay + 1 + i, other: true })
|
|
187
|
+
}
|
|
188
|
+
// Current month days
|
|
189
|
+
for (let d = 1; d <= daysInMonth; d++) cells.push({ d, other: false })
|
|
190
|
+
// Trailing blanks to fill 6 rows of 7 or to end of grid
|
|
191
|
+
while (cells.length % 7 !== 0) cells.push({ d: cells.length, other: true })
|
|
192
|
+
return cells
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function buildCalendarUI ({ state, today, onPickDay, onPrevMonth, onNextMonth, onPrevYear, onNextYear }) {
|
|
196
|
+
const y = state.viewYear
|
|
197
|
+
const m = state.viewMonth
|
|
198
|
+
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
|
|
199
|
+
const weekNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
|
200
|
+
const cells = getMonthMatrix(y, m)
|
|
201
|
+
const sel = state.tmpDate
|
|
202
|
+
const isToday = (d) => today.getFullYear() === y && today.getMonth() === m && today.getDate() === d
|
|
203
|
+
const isSelected = (d) => sel && sel.getFullYear() === y && sel.getMonth() === m && sel.getDate() === d
|
|
204
|
+
|
|
205
|
+
return html`
|
|
206
|
+
<div class="${styles.calHeader}">
|
|
207
|
+
<button type="button" class="${styles.navBtn}" onclick=${onPrevMonth} aria-label="Previous month">
|
|
208
|
+
<span class="${styles.arrowIcon}" style="transform: rotate(90deg);">${solidDown({ colour: '#ccc' })}</span>
|
|
209
|
+
</button>
|
|
210
|
+
<div class="${styles.monthLabel}">
|
|
211
|
+
<span>${monthNames[m]}</span>
|
|
212
|
+
<span class="${styles.yearWrap}" aria-label="Year controls">
|
|
213
|
+
<span class="${styles.yearNum}">${y}</span>
|
|
214
|
+
<span class="${styles.yearControls}">
|
|
215
|
+
<button type="button" class="${styles.yearBtn}" onclick=${onNextYear} aria-label="Next year">
|
|
216
|
+
<span class="${styles.yearArrowIcon}" style="transform: rotate(180deg);">${solidDown({ colour: '#ccc' })}</span>
|
|
217
|
+
</button>
|
|
218
|
+
<button type="button" class="${styles.yearBtn}" onclick=${onPrevYear} aria-label="Previous year">
|
|
219
|
+
<span class="${styles.yearArrowIcon}" style="transform: rotate(0deg);">${solidDown({ colour: '#ccc' })}</span>
|
|
220
|
+
</button>
|
|
221
|
+
</span>
|
|
222
|
+
</span>
|
|
223
|
+
</div>
|
|
224
|
+
<button type="button" class="${styles.navBtn}" onclick=${onNextMonth} aria-label="Next month">
|
|
225
|
+
<span class="${styles.arrowIcon}" style="transform: rotate(270deg);">${solidDown({ colour: '#ccc' })}</span>
|
|
226
|
+
</button>
|
|
227
|
+
</div>
|
|
228
|
+
<div class="${styles.weekHead}">${weekNames.map(w => html`<div style="text-align:center;">${w}</div>`)}</div>
|
|
229
|
+
<div class="${styles.grid}">
|
|
230
|
+
${cells.map((c, idx) => {
|
|
231
|
+
if (c.other) return html`<div></div>`
|
|
232
|
+
const d = c.d
|
|
233
|
+
const selStyle = isSelected(d) ? 'background:#48aaf3; color:#fff;' : 'color:#999;'
|
|
234
|
+
const bgStyle = isSelected(d) ? '' : 'background:#fff;'
|
|
235
|
+
const todayStyle = isToday(d) ? 'box-shadow: inset 0 0 0 2px #c9c9c9;' : ''
|
|
236
|
+
return html`<button type="button" style="padding:6px; ${selStyle} ${bgStyle} ${todayStyle} border:1px solid #c9c9c9; cursor:pointer;" onclick=${() => onPickDay(d)}>${d}</button>`
|
|
237
|
+
})}
|
|
238
|
+
</div>
|
|
239
|
+
`
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export default function datePicker ({
|
|
243
|
+
wrapperStyle = null,
|
|
244
|
+
holdingPen,
|
|
245
|
+
label,
|
|
246
|
+
placeholder,
|
|
247
|
+
property,
|
|
248
|
+
required,
|
|
249
|
+
pattern,
|
|
250
|
+
disabled,
|
|
251
|
+
disableClear = false,
|
|
252
|
+
onchange,
|
|
253
|
+
oninput,
|
|
254
|
+
format = 'YYYY-MM-DD',
|
|
255
|
+
uniqueKey
|
|
256
|
+
} = {}) {
|
|
257
|
+
const wrapperId = ensureId(holdingPen, property, uniqueKey)
|
|
258
|
+
const state = ensureState(holdingPen, property)
|
|
259
|
+
|
|
260
|
+
const currentStr = holdingPen && property ? holdingPen[property] : ''
|
|
261
|
+
const now = new Date()
|
|
262
|
+
if (state.viewMonth === null || state.viewYear === null) {
|
|
263
|
+
const baseDate = parseYMD(currentStr) || now
|
|
264
|
+
state.viewMonth = baseDate.getMonth()
|
|
265
|
+
state.viewYear = baseDate.getFullYear()
|
|
266
|
+
}
|
|
267
|
+
if (!state.tmpDate) {
|
|
268
|
+
state.tmpDate = parseYMD(currentStr) || new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const commit = () => {
|
|
272
|
+
const valueStr = state.tmpDate ? toYMD(state.tmpDate) : ''
|
|
273
|
+
commitValue({ holdingPen, property, onchange, valueStr })
|
|
274
|
+
state.open = false
|
|
275
|
+
rerender()
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const clearValue = (e) => {
|
|
279
|
+
if (e) { e.stopPropagation(); e.preventDefault() }
|
|
280
|
+
commitValue({ holdingPen, property, onchange, valueStr: '' })
|
|
281
|
+
state.open = false
|
|
282
|
+
rerender()
|
|
283
|
+
return false
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// No typing allowed for date input; interaction is via popup only
|
|
287
|
+
|
|
288
|
+
const open = (e) => {
|
|
289
|
+
if (disabled) return
|
|
290
|
+
e && e.stopPropagation()
|
|
291
|
+
// Make opening idempotent to avoid double-toggle on focus+click
|
|
292
|
+
if (state.open) return
|
|
293
|
+
state.open = true
|
|
294
|
+
rerender()
|
|
295
|
+
if (typeof window !== 'undefined' && state.open) {
|
|
296
|
+
const closeOnOutside = (ev) => {
|
|
297
|
+
const wrapperEl = document.getElementById(wrapperId)
|
|
298
|
+
if (!wrapperEl) return
|
|
299
|
+
if (!wrapperEl.contains(ev.target)) {
|
|
300
|
+
state.open = false
|
|
301
|
+
rerender()
|
|
302
|
+
document.removeEventListener('mousedown', closeOnOutside, true)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
setTimeout(() => document.addEventListener('mousedown', closeOnOutside, true), 0)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const popup = state.open
|
|
310
|
+
? html`<div class="${styles.popup}" role="dialog" aria-modal="false">
|
|
311
|
+
${buildCalendarUI({
|
|
312
|
+
state,
|
|
313
|
+
today: new Date(),
|
|
314
|
+
onPickDay: (d) => { state.tmpDate = new Date(state.viewYear, state.viewMonth, d); commit() },
|
|
315
|
+
onPrevMonth: () => { if (state.viewMonth === 0) { state.viewMonth = 11; state.viewYear -= 1 } else { state.viewMonth -= 1 }; rerender() },
|
|
316
|
+
onNextMonth: () => { if (state.viewMonth === 11) { state.viewMonth = 0; state.viewYear += 1 } else { state.viewMonth += 1 }; rerender() },
|
|
317
|
+
onPrevYear: () => { state.viewYear -= 1; rerender() },
|
|
318
|
+
onNextYear: () => { state.viewYear += 1; rerender() }
|
|
319
|
+
})}
|
|
320
|
+
</div>`
|
|
321
|
+
: ''
|
|
322
|
+
|
|
323
|
+
const displayValue = (() => {
|
|
324
|
+
const d = parseYMD(currentStr)
|
|
325
|
+
if (!d) return ''
|
|
326
|
+
return formatDate(d, format || 'YYYY-MM-DD')
|
|
327
|
+
})()
|
|
328
|
+
|
|
329
|
+
return html`
|
|
330
|
+
<div id="${wrapperId}" class="${wrapperStyle}" style="min-height: 55px; display: inline-block; width: calc(100% - 10px); margin: 40px 5px 5px 5px;">
|
|
331
|
+
<div style="display: inline-block; width: 100%; text-align: left; position: relative; padding: 0;" onclick=${e => e.stopPropagation()}>
|
|
332
|
+
${label ? html`<span class="${styles.label}" style="opacity: ${(holdingPen && (holdingPen[property] === 0 || holdingPen[property])) ? 1 : 0}; font-size: 16px; font-weight: normal; color: #999; margin-left: 5px; padding: 9px; background-color: rgba(255,255,255,0.8); position: absolute; top: -36px;">${label}${required ? ' *' : ''}</span>` : ''}
|
|
333
|
+
${!disableClear ? html`<div data-clear class="${styles.clear}" onclick=${clearValue}>clear</div>` : ''}
|
|
334
|
+
<div class="${styles.icon}">${calendarIcon({ colour: '#ccc', width: 20, height: 20 })}</div>
|
|
335
|
+
<input data-gramm="false" ?disabled=${disabled} style="${disabled ? 'cursor: not-allowed; opacity: 0.3;' : 'cursor: pointer;'}" class="${styles.textfield} ${styles.noType} ${styles.withRightIcon} ${fieldIsTouched(holdingPen, property) === true ? styles.touched : ''}" ?required=${required}
|
|
336
|
+
onclick=${open}
|
|
337
|
+
onfocus=${open}
|
|
338
|
+
readonly
|
|
339
|
+
placeholder="${(placeholder || 'Date') + (required ? ' *' : '')}"
|
|
340
|
+
type="text" ${pattern ? { pattern } : ''}
|
|
341
|
+
.value=${displayValue} data-input />
|
|
342
|
+
${popup}
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
`
|
|
346
|
+
}
|
package/components/dropdown.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { html, css } from 'halfcab'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const styles = css`
|
|
4
4
|
|
|
5
5
|
.dropdown {
|
|
6
6
|
position: relative;
|
|
@@ -34,13 +34,35 @@ let styles = css`
|
|
|
34
34
|
}
|
|
35
35
|
`
|
|
36
36
|
|
|
37
|
-
export default ({ menuItems, visible, side, width, margin, backgroundColour }) =>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
export default ({ menuItems = [], options = [], visible = true, side, width, margin, backgroundColour }) => {
|
|
38
|
+
const items = Array.isArray(menuItems) ? menuItems : (Array.isArray(options) ? options : [])
|
|
39
|
+
const filtered = items.filter(item => {
|
|
40
|
+
if (!item) return false
|
|
41
|
+
if (!item.visibleUnder) return true
|
|
42
|
+
if (typeof window === 'undefined') return false
|
|
43
|
+
const limit = parseInt(item.visibleUnder, 10)
|
|
44
|
+
return Number.isFinite(limit) ? window.innerWidth <= limit : true
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
return html`
|
|
48
|
+
<div tabindex="-1" class="${styles.dropdown}" style="position: relative; z-index: 100000; ${!visible ? 'display: none;' : ''}${side === 'right' ? 'float: right;' : ''}">
|
|
49
|
+
<div class="${styles.dropdownContent}" style="background-color: ${backgroundColour || '#f9f9f9'}; ${side === 'right' ? 'right: 0;' : ''} width: ${width || '160px'};${margin ? `margin: ${margin};` : ''}">
|
|
50
|
+
${filtered.map(item => {
|
|
51
|
+
const label = item.text != null ? item.text : (item.name != null ? item.name : '')
|
|
52
|
+
const disabled = item.disabled === true
|
|
53
|
+
const hasAction = typeof item.action === 'function'
|
|
54
|
+
if (item.separator) {
|
|
55
|
+
return html`<hr class="${styles.separator}">`
|
|
56
|
+
}
|
|
57
|
+
if (disabled) {
|
|
58
|
+
return html`<div style="opacity: 0.3; pointer-events: none;">${label}</div>`
|
|
59
|
+
}
|
|
60
|
+
if (hasAction) {
|
|
61
|
+
return html`<div style="cursor: pointer;" onclick=${item.action}>${label}</div>`
|
|
62
|
+
}
|
|
63
|
+
return html`<div>${label}</div>`
|
|
64
|
+
})}
|
|
65
|
+
</div>
|
|
44
66
|
</div>
|
|
45
|
-
|
|
46
|
-
|
|
67
|
+
`
|
|
68
|
+
}
|
package/components/fab.mjs
CHANGED
|
@@ -69,7 +69,7 @@ export default ({ diameter, text, action, colour, disabled, on, icon, menuItems,
|
|
|
69
69
|
`)}</div>` : ''}
|
|
70
70
|
</div>
|
|
71
71
|
<button class="${styles.fab}" onclick=${e => action(e)}
|
|
72
|
-
|
|
72
|
+
?disabled=${disabled}
|
|
73
73
|
style="width: ${diameter}; height: ${diameter};background-color: ${colour};opacity: ${on ? '1' : '0'};bottom: ${on ? '15px' : `calc(-1 * (${diameter} + 10px))`}; ${icon ? 'padding-top: 2px' : ''};">${icon || text}</button>
|
|
74
74
|
</div>
|
|
75
75
|
`
|
package/components/selectbox.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { html, css, formField, fieldIsTouched } from 'halfcab'
|
|
2
2
|
import solidDown from './icons/solidDown.mjs'
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
const styles = css`
|
|
5
5
|
.selectBox {
|
|
6
6
|
-webkit-appearance: none;
|
|
7
7
|
-moz-appearance: none;
|
|
@@ -63,19 +63,20 @@ let styles = css`
|
|
|
63
63
|
`
|
|
64
64
|
|
|
65
65
|
export default ({ wrapperStyle = null, holdingPen, label, property, options, required, disabled, onchange, oninput }) => {
|
|
66
|
-
|
|
66
|
+
const currentOption = options.find(option => {
|
|
67
67
|
if (typeof option === 'object') {
|
|
68
68
|
return option.value === holdingPen[property]
|
|
69
69
|
} else {
|
|
70
70
|
return option === holdingPen[property]
|
|
71
71
|
}
|
|
72
72
|
})
|
|
73
|
+
// text color is always #999 per latest design; placeholder state no longer affects color
|
|
73
74
|
|
|
74
75
|
return html`
|
|
75
|
-
<label style="text-align: left; position: relative; display: inline-block; width: 100%;" ${wrapperStyle ? {
|
|
76
|
+
<label style="text-align: left; position: relative; display: inline-block; width: 100%;" ${wrapperStyle ? { class: wrapperStyle } : ''}>
|
|
76
77
|
<div class="${styles.down}">${solidDown({ colour: '#ccc' })}</div>
|
|
77
78
|
<span class="${styles.label}">${label}${required ? ' *' : ''}</span>
|
|
78
|
-
<select
|
|
79
|
+
<select ?disabled=${disabled} style="${disabled ? 'cursor: not-allowed; opacity: 0.3;' : ''}background-color: ${typeof currentOption === 'object' && currentOption.colour ? `#${currentOption.colour}` : 'white'}; color: #999;" class="${styles.selectBox} ${fieldIsTouched(holdingPen, property) === true ? styles.touched : ''}" oninput=${e => { formField(holdingPen, property)(e); oninput && oninput(e) }} onchange=${e => { formField(holdingPen, property)(e); onchange && onchange(e) }} onblur=${formField(holdingPen, property)}>
|
|
79
80
|
<option value="${required ? 'Select an option' : ''}" ?selected=${!holdingPen[property]} ?disabled=${required} : ''}>${required ? 'Select an option' : ''}</option>
|
|
80
81
|
${options.map(option => {
|
|
81
82
|
let optionValue
|
package/components/textarea.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { html, css, formField, fieldIsTouched } from 'halfcab'
|
|
|
2
2
|
|
|
3
3
|
let styles = css`
|
|
4
4
|
.textarea {
|
|
5
|
+
color: #999;
|
|
5
6
|
padding: 10px;
|
|
6
7
|
border: solid 5px #c9c9c9;
|
|
7
8
|
transition: border 0.3s;
|
|
@@ -56,13 +57,13 @@ function change ({ e, holdingPen, property, label }) {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
export default ({ holdingPen, label, placeholder, property, required, pattern, onkeyup, autofocus, permanentTopPlaceholder = false, permanentTopLabel = false, disabled, darkBackground, onchange, height, oninput, element }) => {
|
|
59
|
-
let input = html`<textarea data-gramm="false" style="${height ? `height: ${height}` : ''}" class="${styles.textarea} ${fieldIsTouched(holdingPen, property) === true ? styles.touched : ''}" onkeyup=${e => onkeyup && onkeyup(e)}
|
|
60
|
+
let input = html`<textarea data-gramm="false" style="${height ? `height: ${height}` : ''}" class="${styles.textarea} ${fieldIsTouched(holdingPen, property) === true ? styles.touched : ''}" onkeyup=${e => onkeyup && onkeyup(e)} ?required=${required} onchange=${e => { change({ e, holdingPen, property, label: styles.label }); onchange && onchange(e) }} oninput=${e => { height = window.getComputedStyle(element.querySelector('textarea')).height; change({ e, holdingPen, property, label: styles.label }); oninput && oninput(e) }} onblur=${formField(holdingPen, property)} placeholder="${placeholder || ''}${required ? ' *' : ''}" ${pattern ? { pattern } : ''}>${holdingPen[property] || ''}</textarea>`
|
|
60
61
|
|
|
61
62
|
if (autofocus) {
|
|
62
63
|
input.autofocus = true
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
return html`
|
|
66
|
-
<label
|
|
67
|
+
<label ?disabled=${disabled} style="${disabled ? 'cursor: not-allowed; opacity: 0.3;' : ''}width: 100%; text-align: left; display: inline-block;"><span class="${styles.label}" style="opacity: ${holdingPen[property] === 0 || holdingPen[property] || (permanentTopPlaceholder || permanentTopLabel) ? 1 : 0}; font-size: 16px; font-weight: normal; color: #999; margin-left: 5px; padding: 9px; background-color: rgba(255,255,255,${darkBackground ? 1 : 0.8}); ">${label}${required ? ' *' : ''}</span>${input}</label>
|
|
67
68
|
`
|
|
68
69
|
}
|
package/components/textfield.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { html, css, formField, fieldIsTouched } from 'halfcab'
|
|
2
2
|
// language=CSS
|
|
3
|
-
|
|
3
|
+
const styles = css`
|
|
4
4
|
.textfield {
|
|
5
5
|
padding: 10px;
|
|
6
6
|
border: solid 5px #c9c9c9;
|
|
@@ -19,6 +19,7 @@ let styles = css`
|
|
|
19
19
|
appearance: none;
|
|
20
20
|
z-index: 20;
|
|
21
21
|
position: relative;
|
|
22
|
+
color: #999;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
.label {
|
|
@@ -63,9 +64,9 @@ let styles = css`
|
|
|
63
64
|
`
|
|
64
65
|
|
|
65
66
|
function change ({ e, holdingPen, property, label }) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
const ff = formField(holdingPen, property)(e)
|
|
68
|
+
const closestLabel = e.target.closest('label')
|
|
69
|
+
const labelEl = closestLabel.querySelector(label.selector)
|
|
69
70
|
if (labelEl) {
|
|
70
71
|
if (holdingPen[property] === 0 || holdingPen[property]) {
|
|
71
72
|
labelEl.style.opacity = 1
|
|
@@ -78,30 +79,35 @@ function change ({ e, holdingPen, property, label }) {
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
function determineType (type) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
// Normalize and guard against undefined/null/non-string values
|
|
83
|
+
const t = (typeof type === 'string' ? type : (type != null ? String(type) : 'text'))
|
|
84
|
+
const tl = t.toLowerCase()
|
|
84
85
|
|
|
85
|
-
|
|
86
|
+
if (tl === 'float' || tl === 'integer') return 'number'
|
|
87
|
+
if (tl === 'string') return 'text'
|
|
88
|
+
if (tl === 'input') return 'text'
|
|
89
|
+
return t || 'text'
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
function determineStep (type) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
+
const tl = typeof type === 'string' ? type.toLowerCase() : ''
|
|
94
|
+
if (tl === 'float' || tl === 'number') return '0.1'
|
|
93
95
|
return '1'
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
export default ({ highlightBorder = false, wrapperStyle = null, holdingPen, label, placeholder, property, required, pattern, type, autofocus, valueContext, permanentTopPlaceholder = false, permanentTopLabel = false, disabled, maxCharacters, maxNumber, minNumber, darkBackground, onkeyup, oninput, onchange }) => {
|
|
97
|
-
|
|
99
|
+
const normalizedType = determineType(type)
|
|
100
|
+
const isColor = normalizedType === 'color'
|
|
101
|
+
const isNumber = normalizedType === 'number'
|
|
102
|
+
|
|
103
|
+
const input = html`<input data-gramm="false" ?disabled=${disabled} ${maxNumber ? { max: maxNumber } : ''} ${minNumber ? { min: minNumber } : ''} ${maxCharacters ? { maxlength: maxCharacters } : ''} style="${isColor ? 'height: 50px; padding: 2px 3px;' : ''}${disabled ? 'cursor: not-allowed; opacity: 0.3;' : ''}" class="${styles.textfield} ${fieldIsTouched(holdingPen, property) === true ? styles.touched : ''} ${highlightBorder ? styles.highlight : ''}" value="${holdingPen[property] !== undefined && holdingPen[property] !== null ? holdingPen[property] : ''}" onkeyup=${e => onkeyup && onkeyup(e)} ?required=${required} onchange=${e => { change({ e, holdingPen, property, label: styles.label }); onchange && onchange(e) }} oninput=${e => { change({ e, holdingPen, property, label: styles.label }); oninput && oninput(e) }} onblur=${formField(holdingPen, property)} placeholder="${placeholder || ''}${required ? ' *' : ''}" type="${normalizedType}" ${pattern ? { pattern } : ''} ${isNumber ? { step: determineStep(normalizedType) } : ''} />`
|
|
98
104
|
|
|
99
105
|
if (autofocus) {
|
|
100
106
|
input.autofocus = true
|
|
101
107
|
}
|
|
102
108
|
|
|
103
109
|
return html`
|
|
104
|
-
<div ${wrapperStyle ? {
|
|
110
|
+
<div ${wrapperStyle ? { class: wrapperStyle } : ''} style="display: inline-block; width: calc(100% - 10px); margin: ${label ? '40' : '5'}px 5px 5px 5px;">
|
|
105
111
|
<label style="width: 100%; text-align: left; position: relative; padding: 0;">
|
|
106
112
|
${valueContext ? html`<div class="${styles.valueContext}">${valueContext}</div>` : ''}
|
|
107
113
|
${label ? html`<span class="${styles.label}" style="opacity: ${holdingPen[property] === 0 || holdingPen[property] || (permanentTopPlaceholder || permanentTopLabel) ? 1 : 0}; font-size: 16px; font-weight: normal; color: #999; margin-left: 5px; padding: 9px; background-color: rgba(255,255,255,${darkBackground ? 1 : 0.8}); ">${label}${required ? ' *' : ''}</span>` : ''}
|