diff --git a/src/clockpicker.ts b/src/clockpicker.ts new file mode 100644 index 0000000..f6fe7c6 --- /dev/null +++ b/src/clockpicker.ts @@ -0,0 +1,734 @@ +/*! + * ClockPicker v{package.version} (http://weareoutman.github.io/clockpicker/) + * Copyright 2014 Wang Shenwei. + * Licensed under MIT (https://github.com/weareoutman/clockpicker/blob/gh-pages/LICENSE) + */ + +interface Window { + jQuery: JQueryStatic; +} +interface Navigator { + webkitVibrate (pattern: number | number[]) : boolean; +} +interface Event { + touches; + changedTouches; +} + +interface JQuery { + clockpicker(): JQuery; + clockpicker(methodName: 'show'): any; // show the clockpicker + clockpicker(methodName: 'hide'): any; // hide the clockpicker + clockpicker(methodName: 'remove'): void; // remove the clockpicker (and event listeners) + clockpicker(methodName: 'toggleView'): any; // 'hours' or 'minutes' toggle to hours or minutes view + clockpicker(methodName: string): any; + clockpicker(methodName: string, methodParameter: any): any; + clockpicker(optionLiteral: string, optionName: string): any; + clockpicker(options: ClockPickerOptions): JQuery; +} + + +interface ClockPickerOptions { + default?: string; // default time, 'now' or '13:14' e.g. + fromnow?: number; // set default time to * milliseconds from now (using with default = 'now') + placement?: string; // clock popover placement + align?: string; // popover arrow align + donetext?: string; // done button text + autoclose?: boolean; // auto close when minute is selected + twelvehour?: boolean; // change to 12 hour AM/PM clock from 24 hour + vibrate?: boolean; // vibrate the device when dragging clock hand + beforeShow?: any; // callback function triggered before popup is shown + afterShow?: any; // callback function triggered after popup is shown + beforeDone?: any; // callback function triggered before time is written to input; + afterDone?: any; // callback function triggered after time is written to input; + beforeHide?: any; // callback function triggered before popup is hidden; Note: will be triggered between a beforeDone and afterDone + afterHide?: any; // callback function triggered after popup is hidden. Note: will be triggered between a beforeDone and afterDone + beforeHourSelect?: any; //callback function triggered before user makes an hour selection; + afterHourSelect?: any; // callback function triggered after user makes an hour selection; + init?: any; // callback function triggered after the clockpicker has been initiated +} +interface ClockPickerStyles { + top?: number; + left?: number; +} + +class ClockPicker { + /**Default options*/ + static defaultOptions: ClockPickerOptions = { + default: '', // default time, 'now' or '13:14' e.g. + fromnow: 0, // set default time to * milliseconds from now (using with default = 'now') + placement: 'bottom', // clock popover placement + align: 'left', // popover arrow align + donetext: 'Done', // done button text + autoclose: false, // auto close when minute is selected + twelvehour: false, // change to 12 hour AM/PM clock from 24 hour + vibrate: true // vibrate the device when dragging clock hand + }; + static $ = window.jQuery; + private $win = ClockPicker.$(window); + private $doc = ClockPicker.$(document); + private $body: JQuery; + // Can I use inline svg ? + static svgNS = 'http://www.w3.org/2000/svg'; + static svgSupported = 'SVGAngle' in window && ((): boolean => { + let el: HTMLDivElement = document.createElement('div'); + el.innerHTML = ''; + let supported = (el.firstChild && el.firstChild.namespaceURI) == ClockPicker.svgNS; + el.innerHTML = ''; + return supported; + })(); + + // Can I use transition ? + static transitionSupported = ((): boolean => { + let style = document.createElement('div').style; + return 'transition' in style || + 'WebkitTransition' in style || + 'MozTransition' in style || + 'msTransition' in style || + 'OTransition' in style; + })(); + // Listen touch events in touch screen device, instead of mouse events in desktop. + static touchSupported = 'ontouchstart' in window; + static mousedownEvent = 'mousedown' + (ClockPicker.touchSupported ? ' touchstart' : ''); + static mousemoveEvent = 'mousemove.clockpicker' + (ClockPicker.touchSupported ? ' touchmove.clockpicker' : ''); + static mouseupEvent = 'mouseup.clockpicker' + (ClockPicker.touchSupported ? ' touchend.clockpicker' : ''); + + // Vibrate the device if supported + static vibrate = navigator.vibrate ? 'vibrate' : navigator.webkitVibrate ? 'webkitVibrate' : null; + static createSvgElement(name: string): Element { + return document.createElementNS(ClockPicker.svgNS, name); + } + static leadingZero(num: number): string { + return (num < 10 ? '0' : '') + num; + } + // Get a unique id + static idCounter = 0; + static uniqueId(prefix: string): string { + let id: string = ++ClockPicker.idCounter + ''; + return prefix ? prefix + id : id; + } + + // Clock size + private dialRadius = 100; + private outerRadius = 80; + private innerRadius = 54; + private tickRadius = 13; + private diameter = this.dialRadius * 2; + private duration = ClockPicker.transitionSupported ? 350 : 1; + + // Popover template + static tpl: string = `
+
+
+ : + +
+
+
+
+
+
+
+ +
+
`; + + private id: string; + private element: JQuery; + private options: ClockPickerOptions; + private isAppended = false; + private isShown = false; + private currentView = 'hours'; + private isInput: boolean; + private input: JQuery; + private addon: JQuery; + private popover: JQuery; + private plate: JQuery; + private hoursView: JQuery; + private minutesView: JQuery; + private amPmBlock: JQuery; + private spanHours: JQuery; + private spanMinutes: JQuery; + private spanAmPm: JQuery; + private amOrPm = 'PM'; + private hand: Element; + private bg: Element; + private fg: Element; + private g: Element; + private bearing: Element; + private canvas: JQuery; + private hours; + private minutes; + + constructor(element: JQuery, options: ClockPickerOptions) { + this.popover = $(ClockPicker.tpl); + this.plate = this.popover.find('.clockpicker-plate'); + this.hoursView = this.popover.find('.clockpicker-hours'); + this.minutesView = this.popover.find('.clockpicker-minutes'); + this.amPmBlock = this.popover.find('.clockpicker-am-pm-block'); + this.isInput = element.prop('tagName') === 'INPUT'; + this.input = this.isInput ? element : element.find('input'); + this.addon = element.find('.input-group-addon'); + this.id = ClockPicker.uniqueId('cp'); + this.element = element; + this.options = options; + this.spanHours = this.popover.find('.clockpicker-span-hours'); + this.spanMinutes = this.popover.find('.clockpicker-span-minutes'); + this.spanAmPm = this.popover.find('.clockpicker-span-am-pm'); + // Setup for for 12 hour clock if option is selected + if (options.twelvehour) { + let amPmButtonsTemplate = `
+ + +
`; + let amPmButtons = $(amPmButtonsTemplate); + $('') + .on("click", () => { + this.amOrPm = 'AM'; + $('.clockpicker-span-am-pm').empty().append('AM'); + }).appendTo(this.amPmBlock); + $('') + .on("click", () => { + this.amOrPm = 'PM'; + $('.clockpicker-span-am-pm').empty().append('PM'); + }).appendTo(this.amPmBlock); + } + if (!options.autoclose) { + // If autoclose is not set, append a button + $('') + .click($.proxy(this.done, this)) + .appendTo(this.popover); + } + // Placement and arrow align - make sure they make sense. + if ((options.placement === 'top' || options.placement === 'bottom') && (options.align === 'top' || options.align === 'bottom')) options.align = 'left'; + if ((options.placement === 'left' || options.placement === 'right') && (options.align === 'left' || options.align === 'right')) options.align = 'top'; + this.popover.addClass(options.placement); + this.popover.addClass('clockpicker-align-' + options.align); + this.spanHours.click($.proxy(this.toggleView, this, 'hours')); + this.spanMinutes.click($.proxy(this.toggleView, this, 'minutes')); + // Show or toggle + this.input.on('focus.clockpicker click.clockpicker', $.proxy(this.show, this)); + this.addon.on('click.clockpicker', $.proxy(this.toggle, this)); + // Build ticks + let tickTpl = $('
'); + let tick: JQuery; + let radian: number; + let radius: number; + + // Mousedown or touchstart + let mousedown = (e: JQueryEventObject, space: boolean) => { + let offset = this.plate.offset(); + let isTouch = /^touch/.test(e.type); + let x0 = offset.left + this.dialRadius; + let y0 = offset.top + this.dialRadius; + let dx = (isTouch ? e.originalEvent.touches[0] : e).pageX - x0; + let dy = (isTouch ? e.originalEvent.touches[0] : e).pageY - y0; + let z = Math.sqrt(dx * dx + dy * dy); + let moved = false; + + // When clicking on minutes view space, check the mouse position + if (space && (z < this.outerRadius - this.tickRadius || z > this.outerRadius + this.tickRadius)) { + return; + } + e.preventDefault(); + + // Set cursor style of body after 200ms + let movingTimer = setTimeout(() => { + this.$body.addClass('clockpicker-moving'); + }, 200); + + // Place the canvas to top + if (ClockPicker.svgSupported) { + this.plate.append(this.canvas); + } + + // Clock + this.setHand(dx, dy, !space, true); + + // Mousemove on document + this.$doc.off(ClockPicker.mousemoveEvent).on(ClockPicker.mousemoveEvent, (e) => { + e.preventDefault(); + let isTouch = /^touch/.test(e.type), + x = (isTouch ? e.originalEvent.touches[0] : e).pageX - x0, + y = (isTouch ? e.originalEvent.touches[0] : e).pageY - y0; + if (!moved && x === dx && y === dy) { + // Clicking in chrome on windows will trigger a mousemove event + return; + } + moved = true; + this.setHand(x, y, false, true); + }); + + // Mouseup on document + this.$doc.off(ClockPicker.mouseupEvent).on(ClockPicker.mouseupEvent, (e) => { + this.$doc.off(ClockPicker.mouseupEvent); + e.preventDefault(); + let isTouch = /^touch/.test(e.type), + x = (isTouch ? e.originalEvent.changedTouches[0] : e).pageX - x0, + y = (isTouch ? e.originalEvent.changedTouches[0] : e).pageY - y0; + if ((space || moved) && x === dx && y === dy) { + this.setHand(x, y); + } + if (this.currentView === 'hours') { + this.toggleView('minutes', this.duration / 2); + } else { + if (options.autoclose) { + this.minutesView.addClass('clockpicker-dial-out'); + setTimeout(() => { + this.done(); + }, this.duration / 2); + } + } + this.plate.prepend(this.canvas); + + // Reset cursor style of body + clearTimeout(movingTimer); + this.$body.removeClass('clockpicker-moving'); + + // Unbind mousemove event + this.$doc.off(ClockPicker.mousemoveEvent); + }); + } + + + // Hours view + if (options.twelvehour) { + for (let i = 1; i < 13; i += 1) { + tick = tickTpl.clone(); + radian = i / 6 * Math.PI; + radius = this.outerRadius; + tick.css('font-size', '120%'); + tick.css({ + left: this.dialRadius + Math.sin(radian) * radius - this.tickRadius, + top: this.dialRadius - Math.cos(radian) * radius - this.tickRadius + }); + tick.html(i === 0 ? '00' : i.toString()); + this.hoursView.append(tick); + tick.on(ClockPicker.mousedownEvent, mousedown); + } + } else { + for (let i = 0; i < 24; i += 1) { + tick = tickTpl.clone(); + radian = i / 6 * Math.PI; + let inner = i > 0 && i < 13; + radius = inner ? this.innerRadius : this.outerRadius; + tick.css({ + left: this.dialRadius + Math.sin(radian) * radius - this.tickRadius, + top: this.dialRadius - Math.cos(radian) * radius - this.tickRadius + }); + if (inner) { + tick.css('font-size', '120%'); + } + tick.html(i === 0 ? '00' : i.toString()); + this.hoursView.append(tick); + tick.on(ClockPicker.mousedownEvent, mousedown); + } + } + // Minutes view + for (let i = 0; i < 60; i += 5) { + tick = tickTpl.clone(); + radian = i / 30 * Math.PI; + tick.css({ + left: this.dialRadius + Math.sin(radian) * this.outerRadius - this.tickRadius, + top: this.dialRadius - Math.cos(radian) * this.outerRadius - this.tickRadius + }); + tick.css('font-size', '120%'); + tick.html(ClockPicker.leadingZero(i)); + this.minutesView.append(tick); + tick.on(ClockPicker.mousedownEvent, mousedown); + } + // Clicking on minutes view space + this.plate.on(ClockPicker.mousedownEvent, (e) => { + if ($(e.target).closest('.clockpicker-tick').length === 0) { + mousedown(e, true); + } + }); + + if (ClockPicker.svgSupported) { + // Draw clock hands and others + let canvas = this.popover.find('.clockpicker-canvas'), + svg = ClockPicker.createSvgElement('svg'); + svg.setAttribute('class', 'clockpicker-svg'); + svg.setAttribute('width', this.diameter.toString()); + svg.setAttribute('height', this.diameter.toString()); + let g = ClockPicker.createSvgElement('g'); + g.setAttribute('transform', 'translate(' + this.dialRadius + ',' + this.dialRadius + ')'); + let bearing = ClockPicker.createSvgElement('circle'); + bearing.setAttribute('class', 'clockpicker-canvas-bearing'); + bearing.setAttribute('cx', '0'); + bearing.setAttribute('cy', '0'); + bearing.setAttribute('r', '2'); + let hand = ClockPicker.createSvgElement('line'); + hand.setAttribute('x1', '0'); + hand.setAttribute('y1', '0'); + let bg = ClockPicker.createSvgElement('circle'); + bg.setAttribute('class', 'clockpicker-canvas-bg'); + bg.setAttribute('r', this.tickRadius.toString()); + let fg = ClockPicker.createSvgElement('circle'); + fg.setAttribute('class', 'clockpicker-canvas-fg'); + fg.setAttribute('r', '3.5'); + g.appendChild(hand); + g.appendChild(bg); + g.appendChild(fg); + g.appendChild(bearing); + svg.appendChild(g); + canvas.append(svg); + this.hand = hand; + this.bg = bg; + this.fg = fg; + this.bearing = bearing; + this.g = g; + this.canvas = canvas; + } + this.raiseCallback(this.options.init); + } + raiseCallback(callbackFunction: any ): void { + if (callbackFunction && typeof callbackFunction === "function") { + callbackFunction(); + } + } + // Show or hide popover + private toggle = () => { + this[this.isShown ? 'hide' : 'show'](); + }; + // Set popover position + private locate = () => { + let element = this.element, + popover = this.popover, + offset = element.offset(), + width = element.outerWidth(), + height = element.outerHeight(), + placement = this.options.placement, + align = this.options.align, + styles : ClockPickerStyles = {}, + self = this; + popover.show(); + + // Place the popover + switch (placement) { + case 'bottom': + styles.top = offset.top + height; + break; + case 'right': + styles.left = offset.left + width; + break; + case 'top': + styles.top = offset.top - popover.outerHeight(); + break; + case 'left': + styles.left = offset.left - popover.outerWidth(); + break; + } + + // Align the popover arrow + switch (align) { + case 'left': + styles.left = offset.left; + break; + case 'right': + styles.left = offset.left + width - popover.outerWidth(); + break; + case 'top': + styles.top = offset.top; + break; + case 'bottom': + styles.top = offset.top + height - popover.outerHeight(); + break; + } + + popover.css(styles); + }; + + // Show popover + private show = () => { + // Not show again + if (this.isShown) { + return; + } + + this.raiseCallback(this.options.beforeShow); + + let self = this; + + // Initialize + if (!this.isAppended) { + // Append popover to body + this.$body = $(document.body).append(this.popover); + + // Reset position when resize + this.$win.on('resize.clockpicker' + this.id, () => { + if (this.isShown) { + this.locate(); + } + }); + + this.isAppended = true; + } + + // Get the time + let value = ((this.input.prop('value') || this.options['default'] || '') + '').split(':'); + if (value[0] === 'now') { + let now = new Date(+ new Date() + this.options.fromnow); + value = [ + now.getHours().toString(), + now.getMinutes().toString() + ]; + } + this.hours = + value[0] || 0; + this.minutes = + value[1] || 0; + this.spanHours.html(ClockPicker.leadingZero(this.hours)); + this.spanMinutes.html(ClockPicker.leadingZero(this.minutes)); + + // Toggle to hours view + this.toggleView('hours'); + + // Set position + this.locate(); + + this.isShown = true; + + // Hide when clicking or tabbing on any element except the clock, input and addon + this.$doc.on('click.clockpicker.' + this.id + ' focusin.clockpicker.' + this.id, (e) => { + let target = $(e.target); + if (target.closest(this.popover).length === 0 && + target.closest(this.addon).length === 0 && + target.closest(this.input).length === 0) { + this.hide(); + } + }); + + // Hide when ESC is pressed + this.$doc.on('keyup.clockpicker.' + this.id, (e) => { + if (e.keyCode === 27) { + this.hide(); + } + }); + + this.raiseCallback(this.options.afterShow); + }; + // Hide popover + private hide = () => { + this.raiseCallback(this.options.beforeHide); + + this.isShown = false; + + // Unbinding events on document + this.$doc.off('click.clockpicker.' + this.id + ' focusin.clockpicker.' + this.id); + this.$doc.off('keyup.clockpicker.' + this.id); + + this.popover.hide(); + + this.raiseCallback(this.options.afterHide); + }; + // Toggle to hours or minutes view + private toggleViewTimer: number; + private toggleView = (view: string, delay?: number) => { + let raiseAfterHourSelect = false; + if (view === 'minutes' && $(this.hoursView).css("visibility") === "visible") { + this.raiseCallback(this.options.beforeHourSelect); + raiseAfterHourSelect = true; + } + 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); + + // Let's make transitions + hideView.addClass('clockpicker-dial-out'); + nextView.css('visibility', 'visible').removeClass('clockpicker-dial-out'); + + // Reset clock hand + this.resetClock(delay); + + // After transitions ended + clearTimeout(this.toggleViewTimer); + this.toggleViewTimer = setTimeout(() => { + hideView.css('visibility', 'hidden'); + }, this.duration); + + if (raiseAfterHourSelect) { + this.raiseCallback(this.options.afterHourSelect); + } + }; + // Reset clock hand + private resetClock = (delay?: number) => { + 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.innerRadius : this.outerRadius, + x = Math.sin(radian) * radius, + y = - Math.cos(radian) * radius, + self = this; + if (ClockPicker.svgSupported && delay) { + this.canvas.addClass('clockpicker-canvas-out'); + setTimeout(() => { + this.canvas.removeClass('clockpicker-canvas-out'); + this.setHand(x, y); + }, delay); + } else { + this.setHand(x, y); + } + }; + // Set clock hand to (x, y) + private vibrateTimer: number; + private setHand = (x: number, y: number, roundBy5?: boolean, dragging?: boolean) => { + 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.outerRadius + this.innerRadius) / 2, + radius = inner ? this.innerRadius : this.outerRadius; + + if (this.options.twelvehour) { + radius = this.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 (ClockPicker.vibrate && this.options.vibrate) { + // Do not vibrate too frequently + if (!this.vibrateTimer) { + navigator[ClockPicker.vibrate](10); + this.vibrateTimer = setTimeout($.proxy(() => { + this.vibrateTimer = null; + }, this), 100); + } + } + } + + this[this.currentView] = value; + this[isHours ? 'spanHours' : 'spanMinutes'].html(ClockPicker.leadingZero(value)); + + // If svg is not supported, just add an active class to the tick + if (!ClockPicker.svgSupported) { + this[isHours ? 'hoursView' : 'minutesView'].find('.clockpicker-tick').each(function() { + let $this = $(this); + $this.toggleClass('active', value === + $this.html()); + }); + return; + } + + // Place clock hand at the top when dragging + if (dragging || (!isHours && value % 5)) { + this.g.insertBefore(this.hand, this.bearing); + this.g.insertBefore(this.bg, this.fg); + this.bg.setAttribute('class', 'clockpicker-canvas-bg clockpicker-canvas-bg-trans'); + } else { + // Or place it at the bottom + this.g.insertBefore(this.hand, this.bg); + this.g.insertBefore(this.fg, this.bg); + this.bg.setAttribute('class', 'clockpicker-canvas-bg'); + } + + // Set clock hand and others' position + let cx = Math.sin(radian) * radius, + cy = - Math.cos(radian) * radius; + this.hand.setAttribute('x2', cx.toString()); + this.hand.setAttribute('y2', cy.toString()); + this.bg.setAttribute('cx', cx.toString()); + this.bg.setAttribute('cy', cy.toString()); + this.fg.setAttribute('cx', cx.toString()); + this.fg.setAttribute('cy', cy.toString()); + }; + // Hours and minutes are selected + private done = () => { + this.raiseCallback(this.options.beforeDone); + this.hide(); + let last = this.input.prop('value'), + value = ClockPicker.leadingZero(this.hours) + ':' + ClockPicker.leadingZero(this.minutes); + if (this.options.twelvehour) { + value = value + this.amOrPm; + } + + this.input.prop('value', value); + if (value !== last) { + this.input.triggerHandler('change'); + if (!this.isInput) { + this.element.trigger('change'); + } + } + + if (this.options.autoclose) { + this.input.trigger('blur'); + } + + this.raiseCallback(this.options.afterDone); + }; + // Remove clockpicker from input + private remove = () => { + this.element.removeData('clockpicker'); + this.input.off('focus.clockpicker click.clockpicker'); + this.addon.off('click.clockpicker'); + if (this.isShown) { + this.hide(); + } + if (this.isAppended) { + this.$win.off('resize.clockpicker' + this.id); + this.popover.remove(); + } + }; +} +// Extends $.fn.clockpicker +$.fn.clockpicker = function (option) { + let args = Array.prototype.slice.call(arguments, 1); + return this.each(function () { + let $this = $(this), + data = $this.data('clockpicker'); + if (!data) { + let options = $.extend({}, ClockPicker.defaultOptions, $this.data(), typeof option == 'object' && option); + $this.data('clockpicker', new ClockPicker($this, options)); + } else { + // Manual operations; show, hide, remove, e.g. + if (typeof data[option] === 'function') { + data[option].apply(data, args); + } + } + }); +};