(function($) { 'use strict'; let _defaults = { dialRadius: 135, outerRadius: 105, innerRadius: 70, tickRadius: 20, duration: 350, container: null, defaultTime: 'now', // default time, 'now' or '13:14' e.g. fromNow: 0, // Millisecond offset from the defaultTime showClearBtn: false, // internationalization i18n: { cancel: 'Cancel', clear: 'Clear', done: 'Ok' }, autoClose: false, // auto close when minute is selected twelveHour: true, // change to 12 hour AM/PM clock from 24 hour vibrate: true, // vibrate the device when dragging clock hand // Callbacks onOpenStart: null, onOpenEnd: null, onCloseStart: null, onCloseEnd: null, onSelect: null }; /** * @class * */ class Timepicker extends Component { constructor(el, options) { super(Timepicker, el, options); this.el.M_Timepicker = this; this.options = $.extend({}, Timepicker.defaults, options); this.id = M.guid(); this._insertHTMLIntoDOM(); this._setupModal(); this._setupVariables(); this._setupEventHandlers(); this._clockSetup(); this._pickerSetup(); } static get defaults() { return _defaults; } static init(els, options) { return super.init(this, els, options); } static _addLeadingZero(num) { return (num < 10 ? '0' : '') + num; } static _createSVGEl(name) { let svgNS = 'http://www.w3.org/2000/svg'; return document.createElementNS(svgNS, name); } /** * @typedef {Object} Point * @property {number} x The X Coordinate * @property {number} y The Y Coordinate */ /** * Get x position of mouse or touch event * @param {Event} e * @return {Point} x and y location */ static _Pos(e) { if (e.targetTouches && e.targetTouches.length >= 1) { return { x: e.targetTouches[0].clientX, y: e.targetTouches[0].clientY }; } // mouse event return { x: e.clientX, y: e.clientY }; } /** * Get Instance */ static getInstance(el) { let domElem = !!el.jquery ? el[0] : el; return domElem.M_Timepicker; } /** * Teardown component */ destroy() { this._removeEventHandlers(); this.modal.destroy(); $(this.modalEl).remove(); this.el.M_Timepicker = undefined; } /** * Setup Event Handlers */ _setupEventHandlers() { this._handleInputKeydownBound = this._handleInputKeydown.bind(this); this._handleInputClickBound = this._handleInputClick.bind(this); this._handleClockClickStartBound = this._handleClockClickStart.bind(this); this._handleDocumentClickMoveBound = this._handleDocumentClickMove.bind(this); this._handleDocumentClickEndBound = this._handleDocumentClickEnd.bind(this); this.el.addEventListener('click', this._handleInputClickBound); this.el.addEventListener('keydown', this._handleInputKeydownBound); this.plate.addEventListener('mousedown', this._handleClockClickStartBound); this.plate.addEventListener('touchstart', this._handleClockClickStartBound); $(this.spanHours).on('click', this.showView.bind(this, 'hours')); $(this.spanMinutes).on('click', this.showView.bind(this, 'minutes')); } _removeEventHandlers() { this.el.removeEventListener('click', this._handleInputClickBound); this.el.removeEventListener('keydown', this._handleInputKeydownBound); } _handleInputClick() { this.open(); } _handleInputKeydown(e) { if (e.which === M.keys.ENTER) { e.preventDefault(); this.open(); } } _handleClockClickStart(e) { e.preventDefault(); let clockPlateBR = this.plate.getBoundingClientRect(); let offset = { x: clockPlateBR.left, y: clockPlateBR.top }; this.x0 = offset.x + this.options.dialRadius; this.y0 = offset.y + this.options.dialRadius; this.moved = false; let clickPos = Timepicker._Pos(e); this.dx = clickPos.x - this.x0; this.dy = clickPos.y - this.y0; // Set clock hands this.setHand(this.dx, this.dy, false); // Mousemove on document document.addEventListener('mousemove', this._handleDocumentClickMoveBound); document.addEventListener('touchmove', this._handleDocumentClickMoveBound); // Mouseup on document document.addEventListener('mouseup', this._handleDocumentClickEndBound); document.addEventListener('touchend', this._handleDocumentClickEndBound); } _handleDocumentClickMove(e) { e.preventDefault(); let clickPos = Timepicker._Pos(e); let x = clickPos.x - this.x0; let y = clickPos.y - this.y0; this.moved = true; this.setHand(x, y, false, true); } _handleDocumentClickEnd(e) { e.preventDefault(); document.removeEventListener('mouseup', this._handleDocumentClickEndBound); document.removeEventListener('touchend', this._handleDocumentClickEndBound); let clickPos = Timepicker._Pos(e); let x = clickPos.x - this.x0; let y = clickPos.y - this.y0; if (this.moved && x === this.dx && y === this.dy) { this.setHand(x, y); } if (this.currentView === 'hours') { this.showView('minutes', this.options.duration / 2); } else if (this.options.autoClose) { $(this.minutesView).addClass('timepicker-dial-out'); setTimeout(() => { this.done(); }, this.options.duration / 2); } if (typeof this.options.onSelect === 'function') { this.options.onSelect.call(this, this.hours, this.minutes); } // Unbind mousemove event document.removeEventListener('mousemove', this._handleDocumentClickMoveBound); document.removeEventListener('touchmove', this._handleDocumentClickMoveBound); } _insertHTMLIntoDOM() { this.$modalEl = $(Timepicker._template); this.modalEl = this.$modalEl[0]; this.modalEl.id = 'modal-' + this.id; // Append popover to input by default let containerEl = document.querySelector(this.options.container); if (this.options.container && !!containerEl) { this.$modalEl.appendTo(containerEl); } else { this.$modalEl.insertBefore(this.el); } } _setupModal() { this.modal = M.Modal.init(this.modalEl, { onOpenStart: this.options.onOpenStart, onOpenEnd: this.options.onOpenEnd, onCloseStart: this.options.onCloseStart, onCloseEnd: () => { if (typeof this.options.onCloseEnd === 'function') { this.options.onCloseEnd.call(this); } this.isOpen = false; } }); } _setupVariables() { this.currentView = 'hours'; this.vibrate = navigator.vibrate ? 'vibrate' : navigator.webkitVibrate ? 'webkitVibrate' : null; this._canvas = this.modalEl.querySelector('.timepicker-canvas'); this.plate = this.modalEl.querySelector('.timepicker-plate'); this.hoursView = this.modalEl.querySelector('.timepicker-hours'); this.minutesView = this.modalEl.querySelector('.timepicker-minutes'); this.spanHours = this.modalEl.querySelector('.timepicker-span-hours'); this.spanMinutes = this.modalEl.querySelector('.timepicker-span-minutes'); this.spanAmPm = this.modalEl.querySelector('.timepicker-span-am-pm'); this.footer = this.modalEl.querySelector('.timepicker-footer'); this.amOrPm = 'PM'; } _pickerSetup() { let $clearBtn = $( `` ) .appendTo(this.footer) .on('click', this.clear.bind(this)); if (this.options.showClearBtn) { $clearBtn.css({ visibility: '' }); } let confirmationBtnsContainer = $('
'); $( '' ) .appendTo(confirmationBtnsContainer) .on('click', this.close.bind(this)); $( '' ) .appendTo(confirmationBtnsContainer) .on('click', this.done.bind(this)); confirmationBtnsContainer.appendTo(this.footer); } _clockSetup() { if (this.options.twelveHour) { this.$amBtn = $('
AM
'); this.$pmBtn = $('
PM
'); this.$amBtn.on('click', this._handleAmPmClick.bind(this)).appendTo(this.spanAmPm); this.$pmBtn.on('click', this._handleAmPmClick.bind(this)).appendTo(this.spanAmPm); } this._buildHoursView(); this._buildMinutesView(); this._buildSVGClock(); } _buildSVGClock() { // Draw clock hands and others let dialRadius = this.options.dialRadius; let tickRadius = this.options.tickRadius; let diameter = dialRadius * 2; let svg = Timepicker._createSVGEl('svg'); svg.setAttribute('class', 'timepicker-svg'); svg.setAttribute('width', diameter); svg.setAttribute('height', diameter); let g = Timepicker._createSVGEl('g'); g.setAttribute('transform', 'translate(' + dialRadius + ',' + dialRadius + ')'); let bearing = Timepicker._createSVGEl('circle'); bearing.setAttribute('class', 'timepicker-canvas-bearing'); bearing.setAttribute('cx', 0); bearing.setAttribute('cy', 0); bearing.setAttribute('r', 4); let hand = Timepicker._createSVGEl('line'); hand.setAttribute('x1', 0); hand.setAttribute('y1', 0); let bg = Timepicker._createSVGEl('circle'); bg.setAttribute('class', 'timepicker-canvas-bg'); bg.setAttribute('r', tickRadius); g.appendChild(hand); g.appendChild(bg); g.appendChild(bearing); svg.appendChild(g); this._canvas.appendChild(svg); this.hand = hand; this.bg = bg; this.bearing = bearing; this.g = g; } _buildHoursView() { let $tick = $('
'); // Hours view if (this.options.twelveHour) { for (let i = 1; i < 13; i += 1) { let tick = $tick.clone(); let radian = i / 6 * Math.PI; let radius = this.options.outerRadius; tick.css({ left: this.options.dialRadius + Math.sin(radian) * radius - this.options.tickRadius + 'px', top: this.options.dialRadius - Math.cos(radian) * radius - this.options.tickRadius + 'px' }); tick.html(i === 0 ? '00' : i); this.hoursView.appendChild(tick[0]); // tick.on(mousedownEvent, mousedown); } } else { for (let i = 0; i < 24; i += 1) { let tick = $tick.clone(); let radian = i / 6 * Math.PI; let inner = i > 0 && i < 13; let radius = inner ? this.options.innerRadius : this.options.outerRadius; tick.css({ left: this.options.dialRadius + Math.sin(radian) * radius - this.options.tickRadius + 'px', top: this.options.dialRadius - Math.cos(radian) * radius - this.options.tickRadius + 'px' }); tick.html(i === 0 ? '00' : i); this.hoursView.appendChild(tick[0]); // tick.on(mousedownEvent, mousedown); } } } _buildMinutesView() { let $tick = $('
'); // Minutes view for (let i = 0; i < 60; i += 5) { let tick = $tick.clone(); let radian = i / 30 * Math.PI; tick.css({ left: this.options.dialRadius + Math.sin(radian) * this.options.outerRadius - this.options.tickRadius + 'px', top: this.options.dialRadius - Math.cos(radian) * this.options.outerRadius - this.options.tickRadius + 'px' }); tick.html(Timepicker._addLeadingZero(i)); this.minutesView.appendChild(tick[0]); } } _handleAmPmClick(e) { let $btnClicked = $(e.target); this.amOrPm = $btnClicked.hasClass('am-btn') ? 'AM' : 'PM'; this._updateAmPmView(); } _updateAmPmView() { if (this.options.twelveHour) { this.$amBtn.toggleClass('text-primary', this.amOrPm === 'AM'); this.$pmBtn.toggleClass('text-primary', this.amOrPm === 'PM'); } } _updateTimeFromInput() { // Get the time let value = ((this.el.value || this.options.defaultTime || '') + '').split(':'); if (this.options.twelveHour && !(typeof value[1] === 'undefined')) { if (value[1].toUpperCase().indexOf('AM') > 0) { this.amOrPm = 'AM'; } else { this.amOrPm = 'PM'; } value[1] = value[1].replace('AM', '').replace('PM', ''); } if (value[0] === 'now') { let now = new Date(+new Date() + this.options.fromNow); value = [now.getHours(), now.getMinutes()]; if (this.options.twelveHour) { this.amOrPm = value[0] >= 12 && value[0] < 24 ? 'PM' : 'AM'; } } this.hours = +value[0] || 0; this.minutes = +value[1] || 0; this.spanHours.innerHTML = this.hours; this.spanMinutes.innerHTML = Timepicker._addLeadingZero(this.minutes); this._updateAmPmView(); } showView(view, delay) { if (view === 'minutes' && $(this.hoursView).css('visibility') === 'visible') { // raiseCallback(this.options.beforeHourSelect); } let isHours = view === 'hours', nextView = isHours ? this.hoursView : this.minutesView, hideView = isHours ? this.minutesView : this.hoursView; this.currentView = view; $(this.spanHours).toggleClass('text-primary', isHours); $(this.spanMinutes).toggleClass('text-primary', !isHours); // Transition view hideView.classList.add('timepicker-dial-out'); $(nextView) .css('visibility', 'visible') .removeClass('timepicker-dial-out'); // Reset clock hand this.resetClock(delay); // After transitions ended clearTimeout(this.toggleViewTimer); this.toggleViewTimer = setTimeout(() => { $(hideView).css('visibility', 'hidden'); }, this.options.duration); } resetClock(delay) { let view = this.currentView, value = this[view], isHours = view === 'hours', unit = Math.PI / (isHours ? 6 : 30), radian = value * unit, radius = isHours && value > 0 && value < 13 ? this.options.innerRadius : this.options.outerRadius, x = Math.sin(radian) * radius, y = -Math.cos(radian) * radius, self = this; if (delay) { $(this.canvas).addClass('timepicker-canvas-out'); setTimeout(() => { $(self.canvas).removeClass('timepicker-canvas-out'); self.setHand(x, y); }, delay); } else { this.setHand(x, y); } } setHand(x, y, roundBy5) { let radian = Math.atan2(x, -y), isHours = this.currentView === 'hours', unit = Math.PI / (isHours || roundBy5 ? 6 : 30), z = Math.sqrt(x * x + y * y), inner = isHours && z < (this.options.outerRadius + this.options.innerRadius) / 2, radius = inner ? this.options.innerRadius : this.options.outerRadius; if (this.options.twelveHour) { radius = this.options.outerRadius; } // Radian should in range [0, 2PI] if (radian < 0) { radian = Math.PI * 2 + radian; } // Get the round value let value = Math.round(radian / unit); // Get the round radian radian = value * unit; // Correct the hours or minutes if (this.options.twelveHour) { if (isHours) { if (value === 0) value = 12; } else { if (roundBy5) value *= 5; if (value === 60) value = 0; } } else { if (isHours) { if (value === 12) { value = 0; } value = inner ? (value === 0 ? 12 : value) : value === 0 ? 0 : value + 12; } else { if (roundBy5) { value *= 5; } if (value === 60) { value = 0; } } } // Once hours or minutes changed, vibrate the device if (this[this.currentView] !== value) { if (this.vibrate && this.options.vibrate) { // Do not vibrate too frequently if (!this.vibrateTimer) { navigator[this.vibrate](10); this.vibrateTimer = setTimeout(() => { this.vibrateTimer = null; }, 100); } } } this[this.currentView] = value; if (isHours) { this['spanHours'].innerHTML = value; } else { this['spanMinutes'].innerHTML = Timepicker._addLeadingZero(value); } // Set clock hand and others' position let cx1 = Math.sin(radian) * (radius - this.options.tickRadius), cy1 = -Math.cos(radian) * (radius - this.options.tickRadius), cx2 = Math.sin(radian) * radius, cy2 = -Math.cos(radian) * radius; this.hand.setAttribute('x2', cx1); this.hand.setAttribute('y2', cy1); this.bg.setAttribute('cx', cx2); this.bg.setAttribute('cy', cy2); } open() { if (this.isOpen) { return; } this.isOpen = true; this._updateTimeFromInput(); this.showView('hours'); this.modal.open(); } close() { if (!this.isOpen) { return; } this.isOpen = false; this.modal.close(); } /** * Finish timepicker selection. */ done(e, clearValue) { // Set input value let last = this.el.value; let value = clearValue ? '' : Timepicker._addLeadingZero(this.hours) + ':' + Timepicker._addLeadingZero(this.minutes); this.time = value; if (!clearValue && this.options.twelveHour) { value = `${value} ${this.amOrPm}`; } this.el.value = value; // Trigger change event if (value !== last) { this.$el.trigger('change'); } this.close(); this.el.focus(); } clear() { this.done(null, true); } } Timepicker._template = [ '' ].join(''); M.Timepicker = Timepicker; if (M.jQueryLoaded) { M.initializeJqueryWrapper(Timepicker, 'timepicker', 'M_Timepicker'); } })(cash);