@maihcx/super-date 0.3.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/LICENSE +21 -0
- package/README.md +191 -0
- package/dist/super-date.css +86 -0
- package/dist/super-date.css.map +1 -0
- package/dist/super-date.esm.js +1083 -0
- package/dist/super-date.esm.js.map +1 -0
- package/dist/super-date.esm.min.js +2 -0
- package/dist/super-date.esm.min.js.br +0 -0
- package/dist/super-date.min.css +1 -0
- package/dist/super-date.min.css.br +0 -0
- package/dist/super-date.min.js +2 -0
- package/dist/super-date.min.js.br +0 -0
- package/dist/super-date.umd.js +1094 -0
- package/dist/super-date.umd.js.map +1 -0
- package/package.json +68 -0
- package/src/css/index.css +84 -0
- package/src/ts/core/format.ts +452 -0
- package/src/ts/core/instance.ts +605 -0
- package/src/ts/core/overlay.ts +127 -0
- package/src/ts/core/registry.ts +141 -0
- package/src/ts/global.ts +33 -0
- package/src/ts/index.ts +26 -0
- package/src/ts/types/bind-entry.type.ts +6 -0
- package/src/ts/types/css.d.ts +1 -0
- package/src/ts/types/date-token.type.ts +9 -0
- package/src/ts/types/segment.type.ts +8 -0
- package/src/ts/types/super-date.type.ts +14 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* overlay.ts — builds and updates the visual overlay that sits on top of
|
|
3
|
+
* the hidden native date/time input.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Segment } from '../types/segment.type';
|
|
7
|
+
import { tokenValue, tokenPlaceholder } from './format';
|
|
8
|
+
import { InputKind } from './format';
|
|
9
|
+
|
|
10
|
+
const CALENDAR_ICON_SVG = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none"
|
|
11
|
+
stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
12
|
+
stroke-linejoin="round" aria-hidden="true">
|
|
13
|
+
<rect x="3" y="4" width="18" height="18" rx="2"/>
|
|
14
|
+
<line x1="16" y1="2" x2="16" y2="6"/>
|
|
15
|
+
<line x1="8" y1="2" x2="8" y2="6"/>
|
|
16
|
+
<line x1="3" y1="10" x2="21" y2="10"/>
|
|
17
|
+
</svg>`;
|
|
18
|
+
|
|
19
|
+
const CLOCK_ICON_SVG = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none"
|
|
20
|
+
stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
21
|
+
stroke-linejoin="round" aria-hidden="true">
|
|
22
|
+
<circle cx="12" cy="12" r="10"/>
|
|
23
|
+
<polyline points="12 6 12 12 16 14"/>
|
|
24
|
+
</svg>`;
|
|
25
|
+
|
|
26
|
+
export interface OverlayElements {
|
|
27
|
+
wrapper: HTMLElement;
|
|
28
|
+
overlay: HTMLElement;
|
|
29
|
+
segEls: HTMLElement[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Wrap `input` in a `.superdate-wrapper`, inject the overlay element and
|
|
34
|
+
* segment spans. Returns references to every created element.
|
|
35
|
+
*/
|
|
36
|
+
export function buildOverlay(
|
|
37
|
+
input: HTMLInputElement,
|
|
38
|
+
segments: Segment[],
|
|
39
|
+
onSegmentClick: (idx: number) => void,
|
|
40
|
+
onIconClick: () => void,
|
|
41
|
+
kind: InputKind = 'date',
|
|
42
|
+
): OverlayElements {
|
|
43
|
+
// ── Wrapper ─────────────────────────────────────────────────────────────────
|
|
44
|
+
const wrapper = document.createElement('div');
|
|
45
|
+
wrapper.className = 'superdate-wrapper';
|
|
46
|
+
wrapper.style.width = input.style.width || '';
|
|
47
|
+
|
|
48
|
+
input.parentNode!.insertBefore(wrapper, input);
|
|
49
|
+
wrapper.appendChild(input);
|
|
50
|
+
|
|
51
|
+
input.classList.add('superdate-input');
|
|
52
|
+
input.style.display = 'block';
|
|
53
|
+
input.style.boxSizing = 'border-box';
|
|
54
|
+
|
|
55
|
+
// ── Overlay ──────────────────────────────────────────────────────────────────
|
|
56
|
+
const overlay = document.createElement('div');
|
|
57
|
+
overlay.className = 'superdate-overlay';
|
|
58
|
+
|
|
59
|
+
const segEls: HTMLElement[] = [];
|
|
60
|
+
|
|
61
|
+
segments.forEach((seg, i) => {
|
|
62
|
+
const el = document.createElement('span');
|
|
63
|
+
el.className = 'superdate-seg';
|
|
64
|
+
if (seg.token) {
|
|
65
|
+
el.dataset.token = seg.token;
|
|
66
|
+
el.dataset.idx = String(i);
|
|
67
|
+
el.setAttribute('tabindex', '-1');
|
|
68
|
+
el.addEventListener('click', (e) => {
|
|
69
|
+
e.stopPropagation();
|
|
70
|
+
onSegmentClick(i);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
overlay.appendChild(el);
|
|
74
|
+
segEls.push(el);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ── Icon (calendar for date/datetime-local, clock for time) ─────────────────
|
|
78
|
+
const icon = document.createElement('span');
|
|
79
|
+
icon.className = 'superdate-icon';
|
|
80
|
+
icon.innerHTML = kind === 'time' ? CLOCK_ICON_SVG : CALENDAR_ICON_SVG;
|
|
81
|
+
icon.addEventListener('click', (e) => {
|
|
82
|
+
e.stopPropagation();
|
|
83
|
+
onIconClick();
|
|
84
|
+
});
|
|
85
|
+
overlay.appendChild(icon);
|
|
86
|
+
|
|
87
|
+
wrapper.appendChild(overlay);
|
|
88
|
+
|
|
89
|
+
return { wrapper, overlay, segEls };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Re-render all segment spans from the current date/time value.
|
|
94
|
+
*/
|
|
95
|
+
export function renderSegments(
|
|
96
|
+
segments: Segment[],
|
|
97
|
+
segEls: HTMLElement[],
|
|
98
|
+
date: Date | null,
|
|
99
|
+
): void {
|
|
100
|
+
segments.forEach((seg, i) => {
|
|
101
|
+
const el = segEls[i];
|
|
102
|
+
if (!seg.token) {
|
|
103
|
+
el.textContent = seg.text;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const val = tokenValue(seg.token, date);
|
|
107
|
+
if (val) {
|
|
108
|
+
el.textContent = val;
|
|
109
|
+
el.classList.remove('empty');
|
|
110
|
+
} else {
|
|
111
|
+
el.textContent = tokenPlaceholder(seg.token);
|
|
112
|
+
el.classList.add('empty');
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Mark one segment element as active (highlighted), deactivate all others.
|
|
119
|
+
*/
|
|
120
|
+
export function activateSegmentEl(segEls: HTMLElement[], idx: number): void {
|
|
121
|
+
segEls.forEach((el, i) => el.classList.toggle('active', i === idx));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Remove active highlight from all segment elements. */
|
|
125
|
+
export function deactivateAll(segEls: HTMLElement[]): void {
|
|
126
|
+
segEls.forEach(el => el.classList.remove('active'));
|
|
127
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* registry.ts — SuperDateRegistry tracks bound selectors and uses a single
|
|
3
|
+
* MutationObserver to auto-initialise matching inputs added to the DOM later.
|
|
4
|
+
* Supports input[type="date"], input[type="time"], and input[type="datetime-local"].
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { BindEntry } from '../types/bind-entry.type';
|
|
8
|
+
import { SuperDateOptions } from '../types/super-date.type';
|
|
9
|
+
import { SuperDateInstance, INSTANCE_KEY } from './instance';
|
|
10
|
+
|
|
11
|
+
let observer: MutationObserver | null = null;
|
|
12
|
+
let bindings: BindEntry[] = [];
|
|
13
|
+
|
|
14
|
+
const DESTROYED_ATTR = 'data-superdate-destroyed';
|
|
15
|
+
const SUPPORTED_TYPES = new Set(['date', 'time', 'datetime-local']);
|
|
16
|
+
|
|
17
|
+
// ── Destroyed-marker helpers ──────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
function isDestroyed(el: HTMLInputElement): boolean {
|
|
20
|
+
return el.hasAttribute(DESTROYED_ATTR);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function markDestroyed(el: HTMLInputElement): void {
|
|
24
|
+
el.setAttribute(DESTROYED_ATTR, '');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function clearDestroyedBySelector(selector: string): void {
|
|
28
|
+
document.querySelectorAll<HTMLInputElement>(selector).forEach(el => {
|
|
29
|
+
el.removeAttribute(DESTROYED_ATTR);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ── Options normalisation ─────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
function defaultOpts(options: SuperDateOptions = {}): Required<SuperDateOptions> {
|
|
36
|
+
return {
|
|
37
|
+
format: options.format ?? 'dd/MM/yyyy',
|
|
38
|
+
timeFormat: options.timeFormat ?? 'HH:mm',
|
|
39
|
+
dateTimeDelimiter: options.dateTimeDelimiter ?? ' ',
|
|
40
|
+
locale: options.locale ?? (typeof navigator !== 'undefined' ? navigator.language : 'en'),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Initialisation helpers ────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
function createInstance(el: HTMLInputElement, opts: Required<SuperDateOptions>): void {
|
|
47
|
+
el[INSTANCE_KEY] = new SuperDateInstance(
|
|
48
|
+
el,
|
|
49
|
+
opts.format,
|
|
50
|
+
opts.timeFormat,
|
|
51
|
+
opts.dateTimeDelimiter,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function canInit(el: HTMLInputElement): boolean {
|
|
56
|
+
return SUPPORTED_TYPES.has(el.type) && !el[INSTANCE_KEY] && !isDestroyed(el);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function initAll(selector: string, opts: Required<SuperDateOptions>): void {
|
|
60
|
+
document.querySelectorAll<HTMLInputElement>(selector).forEach(el => {
|
|
61
|
+
if (canInit(el)) createInstance(el, opts);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Try to initialise any input in `node` (or node itself) against all
|
|
67
|
+
* registered bindings. Called from the MutationObserver callback.
|
|
68
|
+
*/
|
|
69
|
+
function tryInit(node: Element): void {
|
|
70
|
+
for (const binding of bindings) {
|
|
71
|
+
// Direct match
|
|
72
|
+
if (node instanceof HTMLInputElement && node.matches(binding.selector) && canInit(node)) {
|
|
73
|
+
createInstance(node, binding.options);
|
|
74
|
+
}
|
|
75
|
+
// Descendant matches
|
|
76
|
+
node.querySelectorAll<HTMLInputElement>(binding.selector).forEach(el => {
|
|
77
|
+
if (canInit(el)) createInstance(el, binding.options);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function startObserver(): void {
|
|
83
|
+
observer = new MutationObserver(mutations => {
|
|
84
|
+
for (const mutation of mutations) {
|
|
85
|
+
for (const node of Array.from(mutation.addedNodes)) {
|
|
86
|
+
if (node instanceof Element) tryInit(node);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Public registry ───────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
export class SuperDateRegistry {
|
|
96
|
+
|
|
97
|
+
public version: string = '';
|
|
98
|
+
public name: string = '';
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Bind SuperDate to all current **and future** matching inputs.
|
|
102
|
+
* Safe to call multiple times with different selectors.
|
|
103
|
+
*/
|
|
104
|
+
bind(selector: string, options: SuperDateOptions = {}): this {
|
|
105
|
+
const opts = defaultOpts(options);
|
|
106
|
+
bindings.push({ selector, options: opts });
|
|
107
|
+
|
|
108
|
+
clearDestroyedBySelector(selector);
|
|
109
|
+
initAll(selector, opts);
|
|
110
|
+
|
|
111
|
+
if (!observer) startObserver();
|
|
112
|
+
|
|
113
|
+
return this;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Manually enhance a single element.
|
|
118
|
+
* Returns the existing instance if one is already attached.
|
|
119
|
+
*/
|
|
120
|
+
init(el: HTMLInputElement, options: SuperDateOptions = {}): SuperDateInstance {
|
|
121
|
+
if (el[INSTANCE_KEY]) return el[INSTANCE_KEY]!;
|
|
122
|
+
|
|
123
|
+
el.removeAttribute(DESTROYED_ATTR);
|
|
124
|
+
const opts = defaultOpts(options);
|
|
125
|
+
const instance = new SuperDateInstance(el, opts.format, opts.timeFormat, opts.dateTimeDelimiter);
|
|
126
|
+
el[INSTANCE_KEY] = instance;
|
|
127
|
+
return instance;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Remove the enhancement from a single element and mark it with
|
|
132
|
+
* [data-superdate-destroyed] so the MutationObserver won't re-bind it.
|
|
133
|
+
*/
|
|
134
|
+
destroy(el: HTMLInputElement): void {
|
|
135
|
+
if (el[INSTANCE_KEY]) {
|
|
136
|
+
el[INSTANCE_KEY]!.destroy();
|
|
137
|
+
delete el[INSTANCE_KEY];
|
|
138
|
+
}
|
|
139
|
+
markDestroyed(el);
|
|
140
|
+
}
|
|
141
|
+
}
|
package/src/ts/global.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SuperDate — lightweight TypeScript date-input enhancer.
|
|
3
|
+
*
|
|
4
|
+
* Hides the native browser chrome, renders a fully custom overlay,
|
|
5
|
+
* and supports keyboard editing, copy/paste, and custom date formats.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import SuperDate from 'superdate';
|
|
9
|
+
* SuperDate.bind('.date-field');
|
|
10
|
+
* SuperDate.bind('[data-datepicker]', { format: 'MM/dd/yyyy' });
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import "../css/index.css"
|
|
14
|
+
|
|
15
|
+
import { SuperDateRegistry } from './core/registry';
|
|
16
|
+
|
|
17
|
+
declare global {
|
|
18
|
+
var GLOBAL_SDATE: SuperDateRegistry;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Singleton registry — the default export used in most projects. */
|
|
22
|
+
if (typeof globalThis.GLOBAL_SDATE == "undefined") {
|
|
23
|
+
globalThis.GLOBAL_SDATE = new SuperDateRegistry();
|
|
24
|
+
}
|
|
25
|
+
var SuperDate = globalThis.GLOBAL_SDATE;
|
|
26
|
+
|
|
27
|
+
const __LIB_VERSION__ = "LIB_VERSION";
|
|
28
|
+
const __LIB_NAME__ = "LIB_NAME";
|
|
29
|
+
|
|
30
|
+
SuperDate.version = __LIB_VERSION__;
|
|
31
|
+
SuperDate.name = __LIB_NAME__;
|
|
32
|
+
|
|
33
|
+
export default SuperDate;
|
package/src/ts/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SuperDate — lightweight TypeScript date-input enhancer.
|
|
3
|
+
*
|
|
4
|
+
* Hides the native browser chrome, renders a fully custom overlay,
|
|
5
|
+
* and supports keyboard editing, copy/paste, and custom date formats.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import SuperDate from 'superdate';
|
|
9
|
+
* SuperDate.bind('.date-field');
|
|
10
|
+
* SuperDate.bind('[data-datepicker]', { format: 'MM/dd/yyyy' });
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import "../css/index.css"
|
|
14
|
+
|
|
15
|
+
import { SuperDateRegistry } from './core/registry';
|
|
16
|
+
|
|
17
|
+
/** Singleton registry — the default export used in most projects. */
|
|
18
|
+
const SuperDate = new SuperDateRegistry();
|
|
19
|
+
|
|
20
|
+
const __LIB_VERSION__ = "LIB_VERSION";
|
|
21
|
+
const __LIB_NAME__ = "LIB_NAME";
|
|
22
|
+
|
|
23
|
+
SuperDate.version = __LIB_VERSION__;
|
|
24
|
+
SuperDate.name = __LIB_NAME__;
|
|
25
|
+
|
|
26
|
+
export default SuperDate;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module "*.css";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** A single recognized date/time token in a format string. */
|
|
2
|
+
export type DateToken =
|
|
3
|
+
// Date tokens
|
|
4
|
+
| 'dd' | 'MM' | 'yyyy' | 'yy' | 'd' | 'M'
|
|
5
|
+
// Time tokens
|
|
6
|
+
| 'HH' | 'H' // 24h hours (padded / unpadded)
|
|
7
|
+
| 'hh' | 'h' // 12h hours (padded / unpadded)
|
|
8
|
+
| 'mm' // minutes (padded) — NOTE: lowercase mm = minutes, uppercase MM = months
|
|
9
|
+
| 'ss'; // seconds (padded)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface SuperDateOptions {
|
|
2
|
+
/** Default display format for date inputs, e.g. "dd/MM/yyyy". Can be overridden per-element
|
|
3
|
+
* via the `data-date-format` attribute. */
|
|
4
|
+
format?: string;
|
|
5
|
+
/** Default display format for time tokens, e.g. "HH:mm" or "HH:mm:ss".
|
|
6
|
+
* Can be overridden per-element via `data-time-format` attribute. */
|
|
7
|
+
timeFormat?: string;
|
|
8
|
+
/** Delimiter between date part and time part in datetime-local inputs.
|
|
9
|
+
* Can be overridden per-element via `data-date-time-delimiter` attribute.
|
|
10
|
+
* Defaults to " " (space). */
|
|
11
|
+
dateTimeDelimiter?: string;
|
|
12
|
+
/** Locale used for month/weekday names. Defaults to navigator.language. */
|
|
13
|
+
locale?: string;
|
|
14
|
+
}
|