bootstrap-time 0.1.5-beta → 0.1.7-beta

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/README.md CHANGED
@@ -33,8 +33,8 @@ Or load the published files from jsDelivr:
33
33
  ```html
34
34
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@6.0.0-alpha.1/dist/css/bootstrap.min.css">
35
35
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.2/font/bootstrap-icons.min.css">
36
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-time@0.1.5-beta/dist/css/bootstrap-time.min.css">
37
- <script src="https://cdn.jsdelivr.net/npm/bootstrap-time@0.1.5-beta/dist/js/bootstrap-time.min.js"></script>
36
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-time@0.1.7-beta/dist/css/bootstrap-time.min.css">
37
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap-time@0.1.7-beta/dist/js/bootstrap-time.min.js"></script>
38
38
  ```
39
39
 
40
40
  Local builds use the same paths under `dist/`:
@@ -44,10 +44,28 @@ Local builds use the same paths under `dist/`:
44
44
  <script src="dist/js/bootstrap-time.min.js"></script>
45
45
  ```
46
46
 
47
- The script initializes the clock when `.clock-picker`, `#clock`, `#time-input`, `#hour-arrow` and `#minute-arrow` are present. The widget overlays the form under the time input. See `examples/bootstrap-time.html`.
47
+ Add `bs-time` to a time field. The script builds the clock and places it under that field, so it works in an existing Bootstrap form.
48
+
49
+ ```html
50
+ <label class="form-label" for="time">Time</label>
51
+ <input type="time" class="form-control bs-time" id="time" step="60">
52
+ ```
53
+
54
+ Minute selection follows the input `step`, including when `step` changes: `60` allows every minute by clicking between the numbers, `900` enables only 00, 15, 30 and 45. See `examples/bootstrap-time.html`.
48
55
 
49
56
  ## Changelog
50
57
 
58
+ ### 0.1.7-beta
59
+
60
+ - Attach the clock to any field with class `bs-time`.
61
+
62
+ ### 0.1.6-beta
63
+
64
+ - The example starts on the current time.
65
+ - Minute selection follows the input `step`, including when it changes.
66
+ - `step="60"` selects any minute by clicking between the numbers.
67
+ - `step="900"` enables only 00, 15, 30 and 45.
68
+
51
69
  ### 0.1.5-beta
52
70
 
53
71
  - Release title uses `<version>-beta`.
@@ -1,6 +1,5 @@
1
1
  .clock-picker {
2
2
  position: relative;
3
- width: 20rem;
4
3
  }
5
4
  .clock-picker input[type=time]::-webkit-calendar-picker-indicator {
6
5
  display: none;
@@ -76,6 +75,8 @@
76
75
  width: 16rem;
77
76
  height: 16rem;
78
77
  border-radius: 50%;
78
+ touch-action: none;
79
+ cursor: pointer;
79
80
  }
80
81
 
81
82
  .clock-hand {
@@ -118,3 +119,7 @@
118
119
  .clock-number[aria-pressed=true] {
119
120
  color: var(--bs-white, #fff);
120
121
  }
122
+ .clock-number:disabled {
123
+ opacity: 0.35;
124
+ cursor: default;
125
+ }
@@ -1 +1 @@
1
- .clock-picker{position:relative;width:20rem}.clock-picker input[type=time]::-webkit-calendar-picker-indicator{display:none;appearance:none}.clock-toggle{pointer-events:auto;width:2.25rem;height:2.25rem;margin:-0.25rem;border:0;border-radius:50%;background:rgba(0,0,0,0);color:var(--bs-form-adorn-icon-color, var(--bs-secondary-color))}.clock-toggle .bi{font-size:1rem;line-height:1}.clock-toggle:hover,.clock-toggle:focus-visible{background-color:var(--bs-bg-3);color:var(--bs-body-color)}.clock-widget{position:absolute;top:100%;left:0;z-index:5;width:20rem;margin-top:.25rem;padding:.25rem .5rem 1rem;background-color:var(--bs-secondary-bg-subtle);color:var(--bs-secondary-fg)}.clock-arrows{display:flex;justify-content:flex-end;gap:.25rem}.btn.clock-arrow{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;min-height:0;padding:0;border-radius:50%;background-color:rgba(0,0,0,0);color:var(--bs-body-color);text-decoration:none}.btn.clock-arrow svg{width:1.5rem;height:1.5rem;fill:currentColor}.btn.clock-arrow:hover,.btn.clock-arrow:focus-visible{background-color:var(--bs-bg-3);color:var(--bs-body-color)}.btn.clock-arrow:disabled,.btn.clock-arrow:disabled:hover{background-color:rgba(0,0,0,0);color:var(--bs-secondary-color);opacity:.45}.clock{width:16rem;height:16rem;border-radius:50%}.clock-hand{position:absolute;left:50%;bottom:50%;width:2px;height:var(--hand);transform-origin:center bottom;transform:translateX(-50%) rotate(var(--angle));transition:transform .3s ease,height .3s ease;border-radius:1px;pointer-events:none}.clock-knob{position:absolute;left:50%;top:0;width:2.25rem;height:2.25rem;border-radius:50%;transform:translate(-50%, -50%)}.clock-number{position:absolute;left:50%;top:50%;z-index:1;width:2.25rem;height:2.25rem;margin:-1.125rem;border:0;border-radius:50%;background:rgba(0,0,0,0);color:inherit;transform:rotate(var(--angle)) translateY(calc(var(--radius) * -1)) rotate(calc(var(--angle) * -1))}.clock-number[aria-pressed=true]{color:var(--bs-white, #fff)}
1
+ .clock-picker{position:relative}.clock-picker input[type=time]::-webkit-calendar-picker-indicator{display:none;appearance:none}.clock-toggle{pointer-events:auto;width:2.25rem;height:2.25rem;margin:-0.25rem;border:0;border-radius:50%;background:rgba(0,0,0,0);color:var(--bs-form-adorn-icon-color, var(--bs-secondary-color))}.clock-toggle .bi{font-size:1rem;line-height:1}.clock-toggle:hover,.clock-toggle:focus-visible{background-color:var(--bs-bg-3);color:var(--bs-body-color)}.clock-widget{position:absolute;top:100%;left:0;z-index:5;width:20rem;margin-top:.25rem;padding:.25rem .5rem 1rem;background-color:var(--bs-secondary-bg-subtle);color:var(--bs-secondary-fg)}.clock-arrows{display:flex;justify-content:flex-end;gap:.25rem}.btn.clock-arrow{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;min-height:0;padding:0;border-radius:50%;background-color:rgba(0,0,0,0);color:var(--bs-body-color);text-decoration:none}.btn.clock-arrow svg{width:1.5rem;height:1.5rem;fill:currentColor}.btn.clock-arrow:hover,.btn.clock-arrow:focus-visible{background-color:var(--bs-bg-3);color:var(--bs-body-color)}.btn.clock-arrow:disabled,.btn.clock-arrow:disabled:hover{background-color:rgba(0,0,0,0);color:var(--bs-secondary-color);opacity:.45}.clock{width:16rem;height:16rem;border-radius:50%;touch-action:none;cursor:pointer}.clock-hand{position:absolute;left:50%;bottom:50%;width:2px;height:var(--hand);transform-origin:center bottom;transform:translateX(-50%) rotate(var(--angle));transition:transform .3s ease,height .3s ease;border-radius:1px;pointer-events:none}.clock-knob{position:absolute;left:50%;top:0;width:2.25rem;height:2.25rem;border-radius:50%;transform:translate(-50%, -50%)}.clock-number{position:absolute;left:50%;top:50%;z-index:1;width:2.25rem;height:2.25rem;margin:-1.125rem;border:0;border-radius:50%;background:rgba(0,0,0,0);color:inherit;transform:rotate(var(--angle)) translateY(calc(var(--radius) * -1)) rotate(calc(var(--angle) * -1))}.clock-number[aria-pressed=true]{color:var(--bs-white, #fff)}.clock-number:disabled{opacity:.35;cursor:default}
@@ -7,20 +7,72 @@
7
7
 
8
8
  var INNER_RADIUS = "4.6rem";
9
9
  var OUTER_RADIUS = "6.4rem";
10
+ var LEFT = "M15.41 16.59 10.83 12l4.58-4.59L14 6l-6 6 6 6z";
11
+ var RIGHT = "M8.59 16.59 13.17 12 8.59 7.41 10 6l6 6-6 6z";
10
12
 
11
- function init() {
12
- var picker = document.querySelector(".clock-picker");
13
- var clock = document.querySelector("#clock");
14
- var widget = document.querySelector("#clock-widget");
15
- var timeInput = document.querySelector("#time-input");
16
- var hourArrow = document.querySelector("#hour-arrow");
17
- var minuteArrow = document.querySelector("#minute-arrow");
18
- var clockToggle = document.querySelector("#clock-toggle");
19
- if (!clock || !timeInput || !hourArrow || !minuteArrow) return;
20
-
21
- var initial = (timeInput.value || "17:00").split(":");
13
+ function svg(path) {
14
+ return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="' + path + '"></path></svg>';
15
+ }
16
+
17
+ function mount(field) {
18
+ if (field.dataset.bsTime === "ready") return;
19
+ field.dataset.bsTime = "ready";
20
+
21
+ var host = field.closest(".form-control") || field;
22
+ var picker = host.closest(".clock-picker");
23
+ if (!picker) {
24
+ picker = document.createElement("div");
25
+ picker.className = "clock-picker";
26
+ host.parentNode.insertBefore(picker, host);
27
+ picker.append(host);
28
+ }
29
+
30
+ var adorn = field.closest(".form-adorn");
31
+ if (!adorn && field.classList.contains("form-control")) {
32
+ adorn = document.createElement("div");
33
+ adorn.className = "form-control form-adorn form-adorn-end";
34
+ field.parentNode.insertBefore(adorn, field);
35
+ adorn.append(field);
36
+ field.classList.remove("form-control");
37
+ field.classList.add("form-ghost");
38
+ }
39
+
40
+ var toggle = picker.querySelector(".clock-toggle");
41
+ if (!toggle && adorn) {
42
+ toggle = document.createElement("button");
43
+ toggle.type = "button";
44
+ toggle.className = "form-adorn-icon clock-toggle";
45
+ toggle.setAttribute("aria-label", "Klok");
46
+ toggle.setAttribute("aria-pressed", "false");
47
+ toggle.innerHTML = '<i class="bi bi-clock" aria-hidden="true"></i>';
48
+ adorn.insertBefore(toggle, adorn.firstChild);
49
+ }
50
+
51
+ var widget = picker.querySelector(".clock-widget");
52
+ if (!widget) {
53
+ widget = document.createElement("div");
54
+ widget.className = "clock-widget card theme-secondary theme-subtle";
55
+ widget.hidden = true;
56
+ widget.innerHTML =
57
+ '<div class="clock-arrows">' +
58
+ '<button class="btn btn-link clock-arrow clock-hour" type="button" aria-label="Uren">' + svg(LEFT) + "</button>" +
59
+ '<button class="btn btn-link clock-arrow clock-minute" type="button" aria-label="Minuten">' + svg(RIGHT) + "</button>" +
60
+ "</div>" +
61
+ '<div class="clock position-relative mx-auto theme-inverse theme-subtle" role="group" aria-label="Kies een uur"></div>';
62
+ picker.append(widget);
63
+ }
64
+
65
+ var clock = widget.querySelector(".clock");
66
+ var hourArrow = widget.querySelector(".clock-hour");
67
+ var minuteArrow = widget.querySelector(".clock-minute");
68
+ if (!clock || !hourArrow || !minuteArrow) return;
69
+
70
+ var now = new Date();
71
+ var initial = field.value
72
+ ? field.value.split(":")
73
+ : [now.getHours(), now.getMinutes()];
22
74
  var hour = Number(initial[0]) || 0;
23
- var minute = Number(initial[1]) || 0;
75
+ var minute = snapMinute(Number(initial[1]) || 0);
24
76
  var mode = "hour";
25
77
  var handAngle = 150;
26
78
  var hand = document.createElement("div");
@@ -36,6 +88,22 @@
36
88
  return String(value).padStart(2, "0");
37
89
  }
38
90
 
91
+ function minuteStep() {
92
+ var step = Number(field.getAttribute("step") || field.step);
93
+ if (!step || step < 60) step = 60;
94
+ return Math.max(1, Math.round(step / 60));
95
+ }
96
+
97
+ function snapMinute(value) {
98
+ var step = minuteStep();
99
+ var snapped = Math.round(value / step) * step;
100
+ return ((snapped % 60) + 60) % 60;
101
+ }
102
+
103
+ function allowedMinute(value) {
104
+ return value % minuteStep() === 0;
105
+ }
106
+
39
107
  function targetAngle() {
40
108
  return mode === "hour" ? (hour % 12) * 30 : minute * 6;
41
109
  }
@@ -59,7 +127,7 @@
59
127
  }
60
128
 
61
129
  function writeInput() {
62
- timeInput.value = pad(hour) + ":" + pad(minute);
130
+ field.value = pad(hour) + ":" + pad(minute);
63
131
  }
64
132
 
65
133
  function showMode(nextMode) {
@@ -85,14 +153,18 @@
85
153
  var button = document.createElement("button");
86
154
  var inner = mode === "hour" && index >= 12;
87
155
  var angle = (mode === "hour" ? index % 12 : index) * 30;
156
+ var disabled = mode === "minute" && !allowedMinute(value);
88
157
 
89
158
  button.type = "button";
90
159
  button.className = "clock-number";
91
- button.textContent = mode === "hour" && value === 0 ? "00" : String(value);
92
- button.setAttribute("aria-pressed", value === selected ? "true" : "false");
160
+ button.textContent = value === 0 ? "00" : String(value);
161
+ button.dataset.value = String(value);
162
+ button.disabled = disabled;
163
+ button.setAttribute("aria-pressed", !disabled && value === selected ? "true" : "false");
93
164
  button.style.setProperty("--angle", angle + "deg");
94
165
  button.style.setProperty("--radius", inner ? INNER_RADIUS : OUTER_RADIUS);
95
166
  button.addEventListener("click", function () {
167
+ if (button.disabled) return;
96
168
  if (mode === "hour") {
97
169
  hour = value;
98
170
  mode = "minute";
@@ -108,35 +180,56 @@
108
180
  }
109
181
 
110
182
  function showWidget() {
111
- if (!widget) return;
112
183
  widget.hidden = false;
113
- if (clockToggle) clockToggle.setAttribute("aria-pressed", "true");
184
+ if (toggle) toggle.setAttribute("aria-pressed", "true");
114
185
  }
115
186
 
116
187
  function hideWidget() {
117
- if (!widget) return;
118
188
  widget.hidden = true;
119
- if (clockToggle) clockToggle.setAttribute("aria-pressed", "false");
189
+ if (toggle) toggle.setAttribute("aria-pressed", "false");
120
190
  }
121
191
 
122
192
  function toggleWidget() {
123
- if (widget && widget.hidden) showWidget();
193
+ if (widget.hidden) showWidget();
124
194
  else hideWidget();
125
195
  }
126
196
 
197
+ function valueFromPoint(event) {
198
+ var rect = clock.getBoundingClientRect();
199
+ var dx = event.clientX - (rect.left + rect.width / 2);
200
+ var dy = event.clientY - (rect.top + rect.height / 2);
201
+ var angle = (Math.atan2(dx, -dy) * 180 / Math.PI + 360) % 360;
202
+ if (mode === "minute") return snapMinute(Math.round(angle / 6) % 60);
203
+ var hour12 = Math.round(angle / 30) % 12;
204
+ var inner = Math.hypot(dx, dy) < rect.width * 0.32;
205
+ if (inner) return hour12 === 0 ? 0 : hour12 + 12;
206
+ return hour12 === 0 ? 12 : hour12;
207
+ }
208
+
209
+ clock.addEventListener("click", function (event) {
210
+ if (event.target.closest(".clock-number")) return;
211
+ var value = valueFromPoint(event);
212
+ if (mode === "minute") minute = value;
213
+ else {
214
+ hour = value;
215
+ mode = "minute";
216
+ }
217
+ writeInput();
218
+ updateArrows();
219
+ renderClock();
220
+ });
221
+
127
222
  hourArrow.addEventListener("click", function () {
128
223
  showMode("hour");
129
224
  });
130
225
  minuteArrow.addEventListener("click", function () {
131
226
  showMode("minute");
132
227
  });
133
- if (clockToggle) {
134
- clockToggle.addEventListener("click", toggleWidget);
135
- }
136
- timeInput.addEventListener("focus", showWidget);
137
- timeInput.addEventListener("input", function () {
138
- if (!timeInput.value) return;
139
- var parts = timeInput.value.split(":");
228
+ if (toggle) toggle.addEventListener("click", toggleWidget);
229
+ field.addEventListener("focus", showWidget);
230
+ field.addEventListener("input", function () {
231
+ if (!field.value) return;
232
+ var parts = field.value.split(":");
140
233
  var nextHour = Number(parts[0]);
141
234
  var nextMinute = Number(parts[1]);
142
235
  if (nextMinute !== minute) mode = "minute";
@@ -146,24 +239,50 @@
146
239
  updateArrows();
147
240
  renderClock();
148
241
  });
149
- if (picker) {
150
- picker.addEventListener("mousedown", function (event) {
151
- if (event.target === timeInput) return;
152
- if (clockToggle && clockToggle.contains(event.target)) return;
153
- event.preventDefault();
154
- });
155
- }
242
+ picker.addEventListener("mousedown", function (event) {
243
+ if (event.target === field) return;
244
+ if (toggle && toggle.contains(event.target)) return;
245
+ event.preventDefault();
246
+ });
156
247
  document.addEventListener("mousedown", function (event) {
157
- if (!picker || picker.contains(event.target)) return;
248
+ if (picker.contains(event.target)) return;
158
249
  hideWidget();
159
250
  });
160
251
 
252
+ function applyStep() {
253
+ minute = snapMinute(minute);
254
+ field.value = pad(hour) + ":" + pad(minute);
255
+ renderClock();
256
+ }
257
+
258
+ var stepDescriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "step");
259
+ if (stepDescriptor && stepDescriptor.set) {
260
+ Object.defineProperty(field, "step", {
261
+ configurable: true,
262
+ get: function () {
263
+ return stepDescriptor.get.call(field);
264
+ },
265
+ set: function (value) {
266
+ stepDescriptor.set.call(field, value);
267
+ applyStep();
268
+ }
269
+ });
270
+ }
271
+ new MutationObserver(applyStep).observe(field, {
272
+ attributes: true,
273
+ attributeFilter: ["step"]
274
+ });
275
+
161
276
  updateArrows();
162
- writeInput();
277
+ if (!field.value) field.value = pad(hour) + ":" + pad(minute);
163
278
  renderClock();
164
279
  hideWidget();
165
280
  }
166
281
 
282
+ function init() {
283
+ document.querySelectorAll(".bs-time").forEach(mount);
284
+ }
285
+
167
286
  if (document.readyState === "loading") {
168
287
  document.addEventListener("DOMContentLoaded", init);
169
288
  } else {
@@ -1 +1 @@
1
- !function(){"use strict";var e="4.6rem",t="6.4rem";function n(){var n=document.querySelector(".clock-picker"),r=document.querySelector("#clock"),u=document.querySelector("#clock-widget"),o=document.querySelector("#time-input"),c=document.querySelector("#hour-arrow"),i=document.querySelector("#minute-arrow"),a=document.querySelector("#clock-toggle");if(r&&o&&c&&i){var d=(o.value||"17:00").split(":"),s=Number(d[0])||0,l=Number(d[1])||0,m="hour",f=150,p=document.createElement("div"),h=document.createElement("span");p.className="clock-hand bg-primary",h.className="clock-knob bg-primary",h.setAttribute("aria-hidden","true"),p.append(h),r.append(p),c.addEventListener("click",function(){k("hour")}),i.addEventListener("click",function(){k("minute")}),a&&a.addEventListener("click",function(){u&&u.hidden?S():L()}),o.addEventListener("focus",S),o.addEventListener("input",function(){if(o.value){var e=o.value.split(":"),t=Number(e[0]),n=Number(e[1]);n!==l?m="minute":t!==s&&(m="hour"),s=t,l=n,b(),E()}}),n&&n.addEventListener("mousedown",function(e){e.target!==o&&(a&&a.contains(e.target)||e.preventDefault())}),document.addEventListener("mousedown",function(e){n&&!n.contains(e.target)&&L()}),b(),g(),E(),L()}function v(e){return String(e).padStart(2,"0")}function y(){f+=((("hour"===m?s%12*30:6*l)-f)%360+540)%360-180,p.style.setProperty("--angle",f+"deg"),p.style.setProperty("--hand","hour"===m&&(0===s||s>12)?e:t)}function b(){c.disabled="hour"===m,i.disabled="minute"===m}function g(){o.value=v(s)+":"+v(l)}function k(e){m!==e&&(m=e,b(),E())}function E(){r.querySelectorAll(".clock-number").forEach(function(e){e.remove()}),r.setAttribute("aria-label","hour"===m?"Kies een uur":"Kies de minuten");var n="hour"===m?[12,1,2,3,4,5,6,7,8,9,10,11,0,13,14,15,16,17,18,19,20,21,22,23]:[0,5,10,15,20,25,30,35,40,45,50,55],u="hour"===m?s:l;y(),n.forEach(function(n,o){var c=document.createElement("button"),i="hour"===m&&o>=12,a=30*("hour"===m?o%12:o);c.type="button",c.className="clock-number",c.textContent="hour"===m&&0===n?"00":String(n),c.setAttribute("aria-pressed",n===u?"true":"false"),c.style.setProperty("--angle",a+"deg"),c.style.setProperty("--radius",i?e:t),c.addEventListener("click",function(){"hour"===m?(s=n,m="minute"):l=n,g(),b(),E()}),r.append(c)})}function S(){u&&(u.hidden=!1,a&&a.setAttribute("aria-pressed","true"))}function L(){u&&(u.hidden=!0,a&&a.setAttribute("aria-pressed","false"))}}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",n):n()}();
1
+ !function(){"use strict";var e="4.6rem",t="6.4rem";function n(e){return'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="'+e+'"></path></svg>'}function r(r){if("ready"!==r.dataset.bsTime){r.dataset.bsTime="ready";var o=r.closest(".form-control")||r,a=o.closest(".clock-picker");a||((a=document.createElement("div")).className="clock-picker",o.parentNode.insertBefore(a,o),a.append(o));var i=r.closest(".form-adorn");!i&&r.classList.contains("form-control")&&((i=document.createElement("div")).className="form-control form-adorn form-adorn-end",r.parentNode.insertBefore(i,r),i.append(r),r.classList.remove("form-control"),r.classList.add("form-ghost"));var c=a.querySelector(".clock-toggle");!c&&i&&((c=document.createElement("button")).type="button",c.className="form-adorn-icon clock-toggle",c.setAttribute("aria-label","Klok"),c.setAttribute("aria-pressed","false"),c.innerHTML='<i class="bi bi-clock" aria-hidden="true"></i>',i.insertBefore(c,i.firstChild));var u=a.querySelector(".clock-widget");u||((u=document.createElement("div")).className="clock-widget card theme-secondary theme-subtle",u.hidden=!0,u.innerHTML='<div class="clock-arrows"><button class="btn btn-link clock-arrow clock-hour" type="button" aria-label="Uren">'+n("M15.41 16.59 10.83 12l4.58-4.59L14 6l-6 6 6 6z")+'</button><button class="btn btn-link clock-arrow clock-minute" type="button" aria-label="Minuten">'+n("M8.59 16.59 13.17 12 8.59 7.41 10 6l6 6-6 6z")+'</button></div><div class="clock position-relative mx-auto theme-inverse theme-subtle" role="group" aria-label="Kies een uur"></div>',a.append(u));var s=u.querySelector(".clock"),l=u.querySelector(".clock-hour"),d=u.querySelector(".clock-minute");if(s&&l&&d){var m=new Date,f=r.value?r.value.split(":"):[m.getHours(),m.getMinutes()],b=Number(f[0])||0,p=M(Number(f[1])||0),v="hour",h=150,g=document.createElement("div"),k=document.createElement("span");g.className="clock-hand bg-primary",k.className="clock-knob bg-primary",k.setAttribute("aria-hidden","true"),g.append(k),s.append(g),s.addEventListener("click",function(e){if(!e.target.closest(".clock-number")){var t=function(e){var t=s.getBoundingClientRect(),n=e.clientX-(t.left+t.width/2),r=e.clientY-(t.top+t.height/2),o=(180*Math.atan2(n,-r)/Math.PI+360)%360;if("minute"===v)return M(Math.round(o/6)%60);var a=Math.round(o/30)%12;return Math.hypot(n,r)<.32*t.width?0===a?0:a+12:0===a?12:a}(e);"minute"===v?p=t:(b=t,v="minute"),S(),w(),q()}}),l.addEventListener("click",function(){A("hour")}),d.addEventListener("click",function(){A("minute")}),c&&c.addEventListener("click",function(){u.hidden?P():B()}),r.addEventListener("focus",P),r.addEventListener("input",function(){if(r.value){var e=r.value.split(":"),t=Number(e[0]),n=Number(e[1]);n!==p?v="minute":t!==b&&(v="hour"),b=t,p=n,w(),q()}}),a.addEventListener("mousedown",function(e){e.target!==r&&(c&&c.contains(e.target)||e.preventDefault())}),document.addEventListener("mousedown",function(e){a.contains(e.target)||B()});var y=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,"step");y&&y.set&&Object.defineProperty(r,"step",{configurable:!0,get:function(){return y.get.call(r)},set:function(e){y.set.call(r,e),O()}}),new MutationObserver(O).observe(r,{attributes:!0,attributeFilter:["step"]}),w(),r.value||(r.value=E(b)+":"+E(p)),q(),B()}}function E(e){return String(e).padStart(2,"0")}function L(){var e=Number(r.getAttribute("step")||r.step);return(!e||e<60)&&(e=60),Math.max(1,Math.round(e/60))}function M(e){var t=L();return(Math.round(e/t)*t%60+60)%60}function N(){h+=((("hour"===v?b%12*30:6*p)-h)%360+540)%360-180,g.style.setProperty("--angle",h+"deg"),g.style.setProperty("--hand","hour"===v&&(0===b||b>12)?e:t)}function w(){l.disabled="hour"===v,d.disabled="minute"===v}function S(){r.value=E(b)+":"+E(p)}function A(e){v!==e&&(v=e,w(),q())}function q(){s.querySelectorAll(".clock-number").forEach(function(e){e.remove()}),s.setAttribute("aria-label","hour"===v?"Kies een uur":"Kies de minuten");var n="hour"===v?[12,1,2,3,4,5,6,7,8,9,10,11,0,13,14,15,16,17,18,19,20,21,22,23]:[0,5,10,15,20,25,30,35,40,45,50,55],r="hour"===v?b:p;N(),n.forEach(function(n,o){var a=document.createElement("button"),i="hour"===v&&o>=12,c=30*("hour"===v?o%12:o),u="minute"===v&&!function(e){return e%L()===0}(n);a.type="button",a.className="clock-number",a.textContent=0===n?"00":String(n),a.dataset.value=String(n),a.disabled=u,a.setAttribute("aria-pressed",u||n!==r?"false":"true"),a.style.setProperty("--angle",c+"deg"),a.style.setProperty("--radius",i?e:t),a.addEventListener("click",function(){a.disabled||("hour"===v?(b=n,v="minute"):p=n,S(),w(),q())}),s.append(a)})}function P(){u.hidden=!1,c&&c.setAttribute("aria-pressed","true")}function B(){u.hidden=!0,c&&c.setAttribute("aria-pressed","false")}function O(){p=M(p),r.value=E(b)+":"+E(p),q()}}function o(){document.querySelectorAll(".bs-time").forEach(r)}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",o):o()}();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bootstrap-time",
3
- "version": "0.1.5-beta",
3
+ "version": "0.1.7-beta",
4
4
  "description": "Time picker based on Bootstrap.",
5
5
  "author": "M. de Ridder",
6
6
  "license": "MIT",
@@ -7,20 +7,72 @@
7
7
 
8
8
  var INNER_RADIUS = "4.6rem";
9
9
  var OUTER_RADIUS = "6.4rem";
10
+ var LEFT = "M15.41 16.59 10.83 12l4.58-4.59L14 6l-6 6 6 6z";
11
+ var RIGHT = "M8.59 16.59 13.17 12 8.59 7.41 10 6l6 6-6 6z";
10
12
 
11
- function init() {
12
- var picker = document.querySelector(".clock-picker");
13
- var clock = document.querySelector("#clock");
14
- var widget = document.querySelector("#clock-widget");
15
- var timeInput = document.querySelector("#time-input");
16
- var hourArrow = document.querySelector("#hour-arrow");
17
- var minuteArrow = document.querySelector("#minute-arrow");
18
- var clockToggle = document.querySelector("#clock-toggle");
19
- if (!clock || !timeInput || !hourArrow || !minuteArrow) return;
20
-
21
- var initial = (timeInput.value || "17:00").split(":");
13
+ function svg(path) {
14
+ return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="' + path + '"></path></svg>';
15
+ }
16
+
17
+ function mount(field) {
18
+ if (field.dataset.bsTime === "ready") return;
19
+ field.dataset.bsTime = "ready";
20
+
21
+ var host = field.closest(".form-control") || field;
22
+ var picker = host.closest(".clock-picker");
23
+ if (!picker) {
24
+ picker = document.createElement("div");
25
+ picker.className = "clock-picker";
26
+ host.parentNode.insertBefore(picker, host);
27
+ picker.append(host);
28
+ }
29
+
30
+ var adorn = field.closest(".form-adorn");
31
+ if (!adorn && field.classList.contains("form-control")) {
32
+ adorn = document.createElement("div");
33
+ adorn.className = "form-control form-adorn form-adorn-end";
34
+ field.parentNode.insertBefore(adorn, field);
35
+ adorn.append(field);
36
+ field.classList.remove("form-control");
37
+ field.classList.add("form-ghost");
38
+ }
39
+
40
+ var toggle = picker.querySelector(".clock-toggle");
41
+ if (!toggle && adorn) {
42
+ toggle = document.createElement("button");
43
+ toggle.type = "button";
44
+ toggle.className = "form-adorn-icon clock-toggle";
45
+ toggle.setAttribute("aria-label", "Klok");
46
+ toggle.setAttribute("aria-pressed", "false");
47
+ toggle.innerHTML = '<i class="bi bi-clock" aria-hidden="true"></i>';
48
+ adorn.insertBefore(toggle, adorn.firstChild);
49
+ }
50
+
51
+ var widget = picker.querySelector(".clock-widget");
52
+ if (!widget) {
53
+ widget = document.createElement("div");
54
+ widget.className = "clock-widget card theme-secondary theme-subtle";
55
+ widget.hidden = true;
56
+ widget.innerHTML =
57
+ '<div class="clock-arrows">' +
58
+ '<button class="btn btn-link clock-arrow clock-hour" type="button" aria-label="Uren">' + svg(LEFT) + "</button>" +
59
+ '<button class="btn btn-link clock-arrow clock-minute" type="button" aria-label="Minuten">' + svg(RIGHT) + "</button>" +
60
+ "</div>" +
61
+ '<div class="clock position-relative mx-auto theme-inverse theme-subtle" role="group" aria-label="Kies een uur"></div>';
62
+ picker.append(widget);
63
+ }
64
+
65
+ var clock = widget.querySelector(".clock");
66
+ var hourArrow = widget.querySelector(".clock-hour");
67
+ var minuteArrow = widget.querySelector(".clock-minute");
68
+ if (!clock || !hourArrow || !minuteArrow) return;
69
+
70
+ var now = new Date();
71
+ var initial = field.value
72
+ ? field.value.split(":")
73
+ : [now.getHours(), now.getMinutes()];
22
74
  var hour = Number(initial[0]) || 0;
23
- var minute = Number(initial[1]) || 0;
75
+ var minute = snapMinute(Number(initial[1]) || 0);
24
76
  var mode = "hour";
25
77
  var handAngle = 150;
26
78
  var hand = document.createElement("div");
@@ -36,6 +88,22 @@
36
88
  return String(value).padStart(2, "0");
37
89
  }
38
90
 
91
+ function minuteStep() {
92
+ var step = Number(field.getAttribute("step") || field.step);
93
+ if (!step || step < 60) step = 60;
94
+ return Math.max(1, Math.round(step / 60));
95
+ }
96
+
97
+ function snapMinute(value) {
98
+ var step = minuteStep();
99
+ var snapped = Math.round(value / step) * step;
100
+ return ((snapped % 60) + 60) % 60;
101
+ }
102
+
103
+ function allowedMinute(value) {
104
+ return value % minuteStep() === 0;
105
+ }
106
+
39
107
  function targetAngle() {
40
108
  return mode === "hour" ? (hour % 12) * 30 : minute * 6;
41
109
  }
@@ -59,7 +127,7 @@
59
127
  }
60
128
 
61
129
  function writeInput() {
62
- timeInput.value = pad(hour) + ":" + pad(minute);
130
+ field.value = pad(hour) + ":" + pad(minute);
63
131
  }
64
132
 
65
133
  function showMode(nextMode) {
@@ -85,14 +153,18 @@
85
153
  var button = document.createElement("button");
86
154
  var inner = mode === "hour" && index >= 12;
87
155
  var angle = (mode === "hour" ? index % 12 : index) * 30;
156
+ var disabled = mode === "minute" && !allowedMinute(value);
88
157
 
89
158
  button.type = "button";
90
159
  button.className = "clock-number";
91
- button.textContent = mode === "hour" && value === 0 ? "00" : String(value);
92
- button.setAttribute("aria-pressed", value === selected ? "true" : "false");
160
+ button.textContent = value === 0 ? "00" : String(value);
161
+ button.dataset.value = String(value);
162
+ button.disabled = disabled;
163
+ button.setAttribute("aria-pressed", !disabled && value === selected ? "true" : "false");
93
164
  button.style.setProperty("--angle", angle + "deg");
94
165
  button.style.setProperty("--radius", inner ? INNER_RADIUS : OUTER_RADIUS);
95
166
  button.addEventListener("click", function () {
167
+ if (button.disabled) return;
96
168
  if (mode === "hour") {
97
169
  hour = value;
98
170
  mode = "minute";
@@ -108,35 +180,56 @@
108
180
  }
109
181
 
110
182
  function showWidget() {
111
- if (!widget) return;
112
183
  widget.hidden = false;
113
- if (clockToggle) clockToggle.setAttribute("aria-pressed", "true");
184
+ if (toggle) toggle.setAttribute("aria-pressed", "true");
114
185
  }
115
186
 
116
187
  function hideWidget() {
117
- if (!widget) return;
118
188
  widget.hidden = true;
119
- if (clockToggle) clockToggle.setAttribute("aria-pressed", "false");
189
+ if (toggle) toggle.setAttribute("aria-pressed", "false");
120
190
  }
121
191
 
122
192
  function toggleWidget() {
123
- if (widget && widget.hidden) showWidget();
193
+ if (widget.hidden) showWidget();
124
194
  else hideWidget();
125
195
  }
126
196
 
197
+ function valueFromPoint(event) {
198
+ var rect = clock.getBoundingClientRect();
199
+ var dx = event.clientX - (rect.left + rect.width / 2);
200
+ var dy = event.clientY - (rect.top + rect.height / 2);
201
+ var angle = (Math.atan2(dx, -dy) * 180 / Math.PI + 360) % 360;
202
+ if (mode === "minute") return snapMinute(Math.round(angle / 6) % 60);
203
+ var hour12 = Math.round(angle / 30) % 12;
204
+ var inner = Math.hypot(dx, dy) < rect.width * 0.32;
205
+ if (inner) return hour12 === 0 ? 0 : hour12 + 12;
206
+ return hour12 === 0 ? 12 : hour12;
207
+ }
208
+
209
+ clock.addEventListener("click", function (event) {
210
+ if (event.target.closest(".clock-number")) return;
211
+ var value = valueFromPoint(event);
212
+ if (mode === "minute") minute = value;
213
+ else {
214
+ hour = value;
215
+ mode = "minute";
216
+ }
217
+ writeInput();
218
+ updateArrows();
219
+ renderClock();
220
+ });
221
+
127
222
  hourArrow.addEventListener("click", function () {
128
223
  showMode("hour");
129
224
  });
130
225
  minuteArrow.addEventListener("click", function () {
131
226
  showMode("minute");
132
227
  });
133
- if (clockToggle) {
134
- clockToggle.addEventListener("click", toggleWidget);
135
- }
136
- timeInput.addEventListener("focus", showWidget);
137
- timeInput.addEventListener("input", function () {
138
- if (!timeInput.value) return;
139
- var parts = timeInput.value.split(":");
228
+ if (toggle) toggle.addEventListener("click", toggleWidget);
229
+ field.addEventListener("focus", showWidget);
230
+ field.addEventListener("input", function () {
231
+ if (!field.value) return;
232
+ var parts = field.value.split(":");
140
233
  var nextHour = Number(parts[0]);
141
234
  var nextMinute = Number(parts[1]);
142
235
  if (nextMinute !== minute) mode = "minute";
@@ -146,24 +239,50 @@
146
239
  updateArrows();
147
240
  renderClock();
148
241
  });
149
- if (picker) {
150
- picker.addEventListener("mousedown", function (event) {
151
- if (event.target === timeInput) return;
152
- if (clockToggle && clockToggle.contains(event.target)) return;
153
- event.preventDefault();
154
- });
155
- }
242
+ picker.addEventListener("mousedown", function (event) {
243
+ if (event.target === field) return;
244
+ if (toggle && toggle.contains(event.target)) return;
245
+ event.preventDefault();
246
+ });
156
247
  document.addEventListener("mousedown", function (event) {
157
- if (!picker || picker.contains(event.target)) return;
248
+ if (picker.contains(event.target)) return;
158
249
  hideWidget();
159
250
  });
160
251
 
252
+ function applyStep() {
253
+ minute = snapMinute(minute);
254
+ field.value = pad(hour) + ":" + pad(minute);
255
+ renderClock();
256
+ }
257
+
258
+ var stepDescriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "step");
259
+ if (stepDescriptor && stepDescriptor.set) {
260
+ Object.defineProperty(field, "step", {
261
+ configurable: true,
262
+ get: function () {
263
+ return stepDescriptor.get.call(field);
264
+ },
265
+ set: function (value) {
266
+ stepDescriptor.set.call(field, value);
267
+ applyStep();
268
+ }
269
+ });
270
+ }
271
+ new MutationObserver(applyStep).observe(field, {
272
+ attributes: true,
273
+ attributeFilter: ["step"]
274
+ });
275
+
161
276
  updateArrows();
162
- writeInput();
277
+ if (!field.value) field.value = pad(hour) + ":" + pad(minute);
163
278
  renderClock();
164
279
  hideWidget();
165
280
  }
166
281
 
282
+ function init() {
283
+ document.querySelectorAll(".bs-time").forEach(mount);
284
+ }
285
+
167
286
  if (document.readyState === "loading") {
168
287
  document.addEventListener("DOMContentLoaded", init);
169
288
  } else {
@@ -4,7 +4,6 @@
4
4
 
5
5
  .clock-picker {
6
6
  position: relative;
7
- width: 20rem;
8
7
 
9
8
  input[type="time"]::-webkit-calendar-picker-indicator {
10
9
  display: none;
@@ -89,6 +88,8 @@
89
88
  width: 16rem;
90
89
  height: 16rem;
91
90
  border-radius: 50%;
91
+ touch-action: none;
92
+ cursor: pointer;
92
93
  }
93
94
 
94
95
  .clock-hand {
@@ -131,4 +132,9 @@
131
132
  &[aria-pressed="true"] {
132
133
  color: var(--bs-white, #fff);
133
134
  }
135
+
136
+ &:disabled {
137
+ opacity: 0.35;
138
+ cursor: default;
139
+ }
134
140
  }