166 lines
14 KiB
JavaScript
166 lines
14 KiB
JavaScript
import { Component, NewState, Util, $ } from '@apigo.cc/state'
|
|
import { AutoForm } from './form.js'
|
|
|
|
/**
|
|
* DatePicker
|
|
* 支持单日期及范围选择 (主字段 + 影子字段模式)
|
|
*/
|
|
Component.register('DatePicker', container => {
|
|
container._thisObj = container;
|
|
container.state = NewState({ start: '', end: '' })
|
|
|
|
container.addEventListener('bind', e => {
|
|
container.state.start = e.detail || ''
|
|
const form = container.closest('AutoForm')
|
|
const name = container.getAttribute('name')
|
|
const item = form?.state?.schema?.find(i => i.name === name)
|
|
const rangeEnd = item?.setting?.rangeEnd || container.rangeEnd
|
|
if (form && rangeEnd) {
|
|
container.state.end = form.data[rangeEnd] || ''
|
|
}
|
|
})
|
|
|
|
Object.defineProperty(container, 'isRange', {
|
|
get: () => {
|
|
const form = container.closest('AutoForm')
|
|
const name = container.getAttribute('name')
|
|
const item = form?.state?.schema?.find(i => i.name === name)
|
|
return !!(item?.setting?.rangeEnd || container.rangeEnd)
|
|
}
|
|
})
|
|
|
|
Object.defineProperty(container, 'value', {
|
|
get: () => container.state.start,
|
|
set: v => { container.state.start = v || ''; }
|
|
})
|
|
|
|
container.updateStart = (val) => {
|
|
container.state.start = val
|
|
container.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: val }))
|
|
}
|
|
|
|
container.updateEnd = (val) => {
|
|
container.state.end = val
|
|
const form = container.closest('AutoForm')
|
|
const name = container.getAttribute('name')
|
|
const item = form?.state?.schema?.find(i => i.name === name)
|
|
const rangeEnd = item?.setting?.rangeEnd || container.rangeEnd
|
|
if (form && rangeEnd) {
|
|
form.data[rangeEnd] = val
|
|
}
|
|
}
|
|
}, Util.makeDom(/*html*/`
|
|
<div class="d-flex align-items-center gap-1 w-100">
|
|
<input type="date" class="form-control h-100" $bind="this.state.start" $onchange="this.updateStart(thisNode.value)">
|
|
<template $if="this.isRange">
|
|
<span class="text-muted mx-1">-</span>
|
|
<input type="date" class="form-control h-100" $bind="this.state.end" $onchange="this.updateEnd(thisNode.value)">
|
|
</template>
|
|
</div>
|
|
`))
|
|
|
|
/**
|
|
* ColorPicker
|
|
* 支持颜色选择与十六进制文本输入
|
|
*/
|
|
Component.register('ColorPicker', container => {
|
|
container._thisObj = container;
|
|
container.state = NewState({ value: '#000000' })
|
|
container.addEventListener('bind', e => {
|
|
container.state.value = e.detail || '#000000'
|
|
})
|
|
Object.defineProperty(container, 'value', {
|
|
get: () => container.state.value,
|
|
set: v => { container.state.value = v || '#000000'; }
|
|
})
|
|
container.updateValue = (val) => {
|
|
container.state.value = val
|
|
container.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: val }))
|
|
}
|
|
}, Util.makeDom(/*html*/`
|
|
<div class="d-flex align-items-center gap-2 w-100 h-100">
|
|
<input type="color" class="form-control form-control-color flex-shrink-0" style="width: 3rem; padding: 0.25rem" $bind="this.state.value" $onchange="this.updateValue(thisNode.value)">
|
|
<input type="text" class="form-control" $bind="this.state.value" $onchange="this.updateValue(thisNode.value)">
|
|
</div>
|
|
`))
|
|
|
|
const BOOTSTRAP_ICONS = ['alarm', 'archive', 'arrow-left', 'arrow-right', 'bag', 'bank', 'basket', 'bell', 'bookmark', 'box', 'briefcase', 'calendar', 'camera', 'cart', 'chat', 'check', 'chevron-down', 'chevron-left', 'chevron-right', 'chevron-up', 'clock', 'cloud', 'code', 'collection', 'command', 'cpu', 'credit-card', 'cup', 'dash', 'database', 'display', 'door-closed', 'download', 'droplet', 'earbuds', 'edit', 'egg', 'eject', 'envelope', 'eraser', 'eye', 'file', 'filter', 'flag', 'folder', 'gear', 'gem', 'gift', 'graph-up', 'grid', 'hammer', 'hand-thumbs-up', 'heart', 'house', 'image', 'inbox', 'info-circle', 'journal', 'key', 'laptop', 'layers', 'layout-text-sidebar-reverse', 'lightbulb', 'link', 'list', 'lock', 'map', 'mic', 'moon', 'mouse', 'music-note', 'newspaper', 'palette', 'paperclip', 'pause', 'pencil', 'person', 'phone', 'pie-chart', 'play', 'plus', 'printer', 'puzzle', 'question-circle', 'reception-4', 'record', 'reply', 'rss', 'save', 'search', 'send', 'server', 'share', 'shield', 'shop', 'shuffle', 'skip-end', 'skip-start', 'slash', 'sliders', 'smartphone', 'speaker', 'speedometer', 'spellcheck', 'square', 'star', 'stickies', 'stop', 'stopwatch', 'suit-heart', 'sun', 'table', 'tag', 'tags', 'telephone', 'terminal', 'text-paragraph', 'thermometer', 'three-dots', 'ticket', 'tools', 'trash', 'trophy', 'truck', 'tv', 'umbrella', 'unlock', 'upload', 'vector-pen', 'wallet', 'watch', 'wifi', 'window', 'wrench', 'x', 'zoom-in', 'zoom-out', 'activity', 'at', 'award', 'backspace', 'badge-3d', 'badge-4k', 'badge-8k', 'badge-ad', 'badge-ar', 'badge-cc', 'badge-hd', 'badge-tm', 'badge-vo', 'badge-vr', 'badge-wc', 'bar-chart', 'battery', 'bicycle', 'binoculars', 'blockquote-left', 'blockquote-right', 'book', 'bookshelf', 'bootstrap', 'border-all', 'border-bottom', 'border-center', 'border-inner', 'border-left', 'border-middle', 'border-outer', 'border-right', 'border-style', 'border-top', 'border-width', 'bounding-box', 'box-arrow-down', 'box-arrow-in-down', 'box-arrow-in-left', 'box-arrow-in-right', 'box-arrow-in-up', 'box-arrow-left', 'box-arrow-right', 'box-arrow-up', 'box-seam', 'brightness-alt-high', 'brightness-alt-low', 'brightness-high', 'brightness-low', 'broadcast', 'brush', 'bucket', 'bug', 'building', 'bullseye', 'calculator', 'calendar-check', 'calendar-date', 'calendar-day', 'calendar-event', 'calendar-minus', 'calendar-month', 'calendar-plus', 'calendar-range', 'calendar-week', 'calendar-x', 'calendar2', 'calendar3', 'calendar4', 'camera-reels', 'camera-video', 'capslock', 'card-checklist', 'card-heading', 'card-image', 'card-list', 'card-text', 'caret-down', 'caret-left', 'caret-right', 'caret-up', 'cart-check', 'cart-dash', 'cart-plus', 'cart-x', 'cash', 'cash-stack', 'cast', 'chat-dots', 'chat-left', 'chat-quote', 'chat-right', 'chat-square', 'chat-text', 'check-all', 'check-circle', 'check-square', 'circle', 'clipboard', 'cloud-arrow-down', 'cloud-arrow-up', 'cloud-check', 'cloud-download', 'cloud-fog', 'cloud-hail', 'cloud-lightning', 'cloud-minus', 'cloud-moon', 'cloud-plus', 'cloud-rain', 'cloud-slash', 'cloud-snow', 'cloud-sun', 'cloud-upload', 'clouds', 'cloudy', 'code-slash', 'code-square', 'collection-play', 'columns', 'columns-gap', 'compass', 'cone', 'cone-striped', 'controller', 'credit-card-2-back', 'credit-card-2-front', 'crop', 'cup-straw', 'cursor', 'dash-circle', 'dash-square', 'diagram-2', 'diagram-3', 'diamond', 'dice-1', 'dice-2', 'dice-3', 'dice-4', 'dice-5', 'dice-6', 'disc', 'discord', 'distribute-horizontal', 'distribute-vertical', 'door-open', 'dot', 'droplet-half', 'easel', 'egg-fried', 'emoji-angry', 'emoji-dizzy', 'emoji-expressionless', 'emoji-frown', 'emoji-heart-eyes', 'emoji-laughing', 'emoji-neutral', 'emoji-smile', 'emoji-sunglasses', 'emoji-wink', 'envelope-open', 'exclamation', 'exclamation-circle', 'exclamation-diamond', 'exclamation-octagon', 'exclamation-square', 'exclamation-triangle', 'eye-slash', 'eyedropper', 'facebook', 'file-arrow-down', 'file-arrow-up', 'file-binary', 'file-break', 'file-check', 'file-code', 'file-diff', 'file-earmark', 'file-excel', 'file-image', 'file-lock', 'file-medical', 'file-minus', 'file-music', 'file-pdf', 'file-person', 'file-play', 'file-plus', 'file-post', 'file-ppt', 'file-richtext', 'file-slides', 'file-spreadsheet', 'file-text', 'file-word', 'file-zip', 'files', 'film', 'filter-circle', 'filter-left', 'filter-right', 'filter-square', 'fingerprint', 'flower1', 'flower2', 'flower3', 'folder-check', 'folder-minus', 'folder-plus', 'folder-symlink', 'folder-x', 'folder2-open', 'fonts', 'forward', 'front', 'fullscreen', 'fullscreen-exit', 'funnel', 'gear-wide', 'gender-female', 'gender-male', 'gender-trans', 'geo', 'geo-alt', 'github', 'globe', 'google', 'graph-down', 'grid-1x2', 'grid-3x2', 'grid-3x3', 'grip-horizontal', 'grip-vertical', 'hand-index', 'hand-thumbs-down', 'handbag', 'hash', 'headphones', 'headset', 'heart-half', 'heptagon', 'hourglass', 'hourglass-bottom', 'hourglass-split', 'hourglass-top', 'house-door', 'hr', 'hurricane', 'image-alt', 'images', 'infinity', 'input-cursor', 'instagram', 'intersect', 'journal-album', 'journal-arrow-down', 'journal-arrow-up', 'journal-bookmark', 'journal-check', 'journal-code', 'journal-medical', 'journal-minus', 'journal-plus', 'journal-richtext', 'journal-text', 'journal-x', 'journals', 'justify', 'kanban', 'keyboard', 'ladder', 'lamp', 'layers-half', 'layout-sidebar', 'layout-split', 'layout-three-columns', 'life-preserver', 'lightbulb-off', 'lightning', 'lightning-charge', 'link-45deg', 'linkedin', 'list-check', 'list-nested', 'list-ol', 'list-stars', 'list-task', 'list-ul', 'mailbox', 'markdown', 'mask', 'mastodon', 'megaphone', 'menu-app', 'menu-button', 'messenger', 'mic-mute', 'minecart', 'minecart-loaded', 'moisture', 'mouse2', 'mouse3', 'music-note-beamed', 'music-note-list', 'music-player', 'node-minus', 'node-plus', 'nut', 'octagon', 'option', 'outlet', 'paint-bucket', 'patch-check', 'patch-exclamation', 'patch-minus', 'patch-plus', 'patch-question', 'pause-btn', 'pause-circle', 'peace', 'pen', 'pencil-square', 'pentagon', 'person-badge', 'person-bounding-box', 'person-circle', 'person-lines-fill', 'phone-landscape', 'phone-vibrate', 'pie-chart-fill', 'pin', 'pin-angle', 'pin-fill', 'pin-map', 'pip', 'play-btn', 'play-circle', 'plug', 'plus-circle', 'plus-square', 'power', 'question', 'question-diamond', 'question-square', 'rainbow', 'receipt', 'receipt-cutoff', 'reception-0', 'reception-1', 'reception-2', 'reception-3', 'record-btn', 'record-circle', 'record2', 'recycle', 'reddit', 'reply-all', 'router', 'rulers', 'safe', 'save2', 'sd-card', 'segmented-nav', 'shield-check', 'shield-exclamation', 'shield-lock', 'shield-shaded', 'shield-slash', 'shift', 'signpost', 'signpost-2', 'signpost-split', 'sim', 'skip-backward', 'skip-forward', 'slack', 'slash-circle', 'slash-square', 'smartwatch', 'snow', 'snow2', 'snow3', 'sort-alpha-down', 'sort-alpha-up', 'sort-numeric-down', 'sort-numeric-up', 'soundwave', 'speedometer2', 'square-half', 'stack', 'star-half', 'stars', 'stop-btn', 'stop-circle', 'suit-club', 'suit-diamond', 'suit-spade', 'sunglasses', 'sunrise', 'sunset', 'symmetry-horizontal', 'symmetry-vertical', 'tablet', 'tablet-landscape', 'telegram', 'telephone-forward', 'telephone-inbound', 'telephone-outbound', 'telephone-plus', 'telephone-x', 'text-center', 'text-indent-left', 'text-indent-right', 'text-left', 'text-right', 'thermometer-half', 'thermometer-high', 'thermometer-low', 'thermometer-snow', 'thermometer-sun', 'three-dots-vertical', 'toggle-off', 'toggle-on', 'toggle2-off', 'toggle2-on', 'tornado', 'translate', 'trash2', 'tree', 'truck-flatbed', 'tsunami', 'type', 'type-bold', 'type-h1', 'type-h2', 'type-h3', 'type-italic', 'type-strikethrough', 'type-underline', 'ui-checks', 'ui-checks-grid', 'ui-radios', 'ui-radios-grid', 'union', 'upc', 'upc-scan', 'view-list', 'view-stacked', 'vinyl', 'voicemail', 'volume-down', 'volume-mute', 'volume-off', 'volume-up', 'vr', 'wallet2', 'water', 'whatsapp', 'wifi-1', 'wifi-2', 'wifi-off', 'wind', 'window-dock', 'window-sidebar', 'x-circle', 'x-diamond', 'x-octagon', 'x-square', 'youtube']
|
|
|
|
/**
|
|
* IconPicker
|
|
* 基于 Bootstrap Icons 的可视化选择控件
|
|
*/
|
|
Component.register('IconPicker', container => {
|
|
container._thisObj = container;
|
|
container.state = NewState({ value: '', search: '', open: false })
|
|
container.addEventListener('bind', e => {
|
|
container.state.value = e.detail || ''
|
|
})
|
|
|
|
Object.defineProperty(container, 'value', {
|
|
get: () => container.state.value,
|
|
set: v => { container.state.value = v || ''; }
|
|
})
|
|
|
|
Object.defineProperty(container, 'filteredIcons', {
|
|
get: () => {
|
|
const s = container.state.search?.toLowerCase() || ''
|
|
return BOOTSTRAP_ICONS.filter(i => i.includes(s))
|
|
}
|
|
})
|
|
|
|
container.selectIcon = (icon) => {
|
|
container.state.value = icon
|
|
container.state.open = false
|
|
container.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: icon }))
|
|
}
|
|
|
|
container.toggle = () => {
|
|
container.state.open = !container.state.open
|
|
if (container.state.open) {
|
|
setTimeout(() => {
|
|
$(container, 'input')?.focus()
|
|
}, 10)
|
|
}
|
|
}
|
|
|
|
const onGlobalClick = (e) => {
|
|
if (!container.contains(e.target)) {
|
|
container.state.open = false
|
|
}
|
|
}
|
|
window.addEventListener('click', onGlobalClick)
|
|
container.addEventListener('remove', () => window.removeEventListener('click', onGlobalClick))
|
|
}, Util.makeDom(/*html*/`
|
|
<div class="dropdown w-100 h-100">
|
|
<button class="btn btn-outline-secondary dropdown-toggle w-100 h-100 d-flex align-items-center justify-content-between px-2" type="button" $onclick="this.toggle()">
|
|
<span class="d-flex align-items-center overflow-hidden">
|
|
<i $if="this.state.value" $class="bi bi-\${this.state.value} me-2"></i>
|
|
<span class="text-truncate" $text="this.state.value || '{#Select Icon#}'"></span>
|
|
</span>
|
|
</button>
|
|
<div $class="dropdown-menu p-2 shadow \${this.state.open ? 'show' : ''}" style="width: 300px; max-height: 350px; overflow-y: hidden; display: flex; flex-direction: column; z-index: 1050">
|
|
<input type="text" class="form-control form-control-sm mb-2" placeholder="Search icons..." $bind="this.state.search" $onclick="event.stopPropagation()">
|
|
<div class="d-flex flex-wrap gap-1 overflow-auto p-1" style="flex: 1" $onclick="event.stopPropagation()">
|
|
<template $each="this.filteredIcons" as="icon">
|
|
<button type="button" class="btn btn-sm btn-outline-light text-dark p-2 d-flex align-items-center justify-content-center" $onclick="this.selectIcon(icon)" $title="icon" style="width: 40px; height: 40px">
|
|
<i $class="bi bi-\${icon}"></i>
|
|
</button>
|
|
</template>
|
|
<div $if="this.filteredIcons.length === 0" class="text-muted p-2 w-100 text-center">No icons found</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`), Util.makeDom(/*html*/`<style>
|
|
IconPicker { display: block; height: 100%; }
|
|
IconPicker .dropdown-menu { left: 0; }
|
|
IconPicker .btn-outline-light:hover { background-color: var(--bs-primary-bg-subtle); border-color: var(--bs-primary); }
|
|
</style>`))
|
|
|
|
// 注册到 AutoForm
|
|
AutoForm.register('DatePicker')
|
|
AutoForm.register('ColorPicker')
|
|
AutoForm.register('IconPicker')
|
|
|
|
export { }
|