From 0f3f39d5c60792b0a684e6c735ec0a70fa925f47 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 24 Mar 2016 00:06:04 -0500 Subject: [PATCH] Add new TimeFieldRenderer, make it default for Time fields Uses a jQuery UI widget similar to datepicker: https://fgelinas.com/code/timepicker/ --- tailbone/app.py | 1 + tailbone/forms/renderers/__init__.py | 2 +- tailbone/forms/renderers/common.py | 52 + tailbone/static/css/jquery.ui.timepicker.css | 57 + .../static/js/lib/jquery.ui.timepicker.js | 1496 +++++++++++++++++ tailbone/static/js/tailbone.js | 7 + tailbone/templates/base.mako | 2 + 7 files changed, 1616 insertions(+), 1 deletion(-) create mode 100644 tailbone/static/css/jquery.ui.timepicker.css create mode 100644 tailbone/static/js/lib/jquery.ui.timepicker.js diff --git a/tailbone/app.py b/tailbone/app.py index a0273acd..7ac1520b 100644 --- a/tailbone/app.py +++ b/tailbone/app.py @@ -137,6 +137,7 @@ def make_pyramid_config(settings): formalchemy.FieldSet.default_renderers[sa.Boolean] = renderers.YesNoFieldRenderer formalchemy.FieldSet.default_renderers[sa.Date] = renderers.DateFieldRenderer formalchemy.FieldSet.default_renderers[sa.DateTime] = renderers.DateTimeFieldRenderer + formalchemy.FieldSet.default_renderers[sa.Time] = renderers.TimeFieldRenderer formalchemy.FieldSet.default_renderers[GPCType] = renderers.GPCFieldRenderer return config diff --git a/tailbone/forms/renderers/__init__.py b/tailbone/forms/renderers/__init__.py index 34605661..9607e5c4 100644 --- a/tailbone/forms/renderers/__init__.py +++ b/tailbone/forms/renderers/__init__.py @@ -30,7 +30,7 @@ from .core import CustomFieldRenderer, DateFieldRenderer from .common import (AutocompleteFieldRenderer, DecimalFieldRenderer, CurrencyFieldRenderer, - DateTimeFieldRenderer, DateTimePrettyFieldRenderer, + DateTimeFieldRenderer, DateTimePrettyFieldRenderer, TimeFieldRenderer, EnumFieldRenderer, YesNoFieldRenderer) from .people import (PersonFieldRenderer, PersonFieldLinkRenderer, diff --git a/tailbone/forms/renderers/common.py b/tailbone/forms/renderers/common.py index c639cf21..43158ce3 100644 --- a/tailbone/forms/renderers/common.py +++ b/tailbone/forms/renderers/common.py @@ -26,7 +26,14 @@ Common Field Renderers from __future__ import unicode_literals, absolute_import +import datetime + +import pytz + +from rattail.time import localtime + import formalchemy +from formalchemy import helpers from formalchemy.fields import FieldRenderer, SelectFieldRenderer, CheckBoxFieldRenderer from pyramid.renderers import render @@ -102,6 +109,51 @@ class DateTimePrettyFieldRenderer(formalchemy.DateTimeFieldRenderer): return pretty_datetime(self.request.rattail_config, value) +class TimeFieldRenderer(formalchemy.TimeFieldRenderer): + """ + Custom renderer for time fields. In edit mode, renders a simple text + input, which is expected to become a 'timepicker' widget in the UI. + However the particular magic required for that lives in 'tailbone.js'. + """ + format = '%I:%M %p' + + def render(self, **kwargs): + kwargs.setdefault('class_', 'timepicker') + return helpers.text_field(self.name, value=self.value, **kwargs) + + def render_readonly(self, **kwargs): + return self.render_value(self.raw_value) + + def render_value(self, value): + value = self.convert_value(value) + if isinstance(value, datetime.time): + return value.strftime(self.format) + return '' + + def convert_value(self, value): + if isinstance(value, datetime.datetime): + if not value.tzinfo: + value = pytz.utc.localize(value) + return localtime(self.request.rattail_config, value).time() + return value + + def stringify_value(self, value, as_html=False): + if not as_html: + return self.render_value(value) + return super(TimeFieldRenderer, self).stringify_value(value, as_html=as_html) + + def _serialized_value(self): + return self.params.getone(self.name) + + def deserialize(self): + value = self._serialized_value() + if value: + try: + return datetime.datetime.strptime(value, self.format).time() + except ValueError: + pass + + class EnumFieldRenderer(SelectFieldRenderer): """ Renderer for simple enumeration fields. diff --git a/tailbone/static/css/jquery.ui.timepicker.css b/tailbone/static/css/jquery.ui.timepicker.css new file mode 100644 index 00000000..b5930fb7 --- /dev/null +++ b/tailbone/static/css/jquery.ui.timepicker.css @@ -0,0 +1,57 @@ +/* + * Timepicker stylesheet + * Highly inspired from datepicker + * FG - Nov 2010 - Web3R + * + * version 0.0.3 : Fixed some settings, more dynamic + * version 0.0.4 : Removed width:100% on tables + * version 0.1.1 : set width 0 on tables to fix an ie6 bug + */ + +.ui-timepicker-inline { display: inline; } + +#ui-timepicker-div { padding: 0.2em; } +.ui-timepicker-table { display: inline-table; width: 0; } +.ui-timepicker-table table { margin:0.15em 0 0 0; border-collapse: collapse; } + +.ui-timepicker-hours, .ui-timepicker-minutes { padding: 0.2em; } + +.ui-timepicker-table .ui-timepicker-title { line-height: 1.8em; text-align: center; } +.ui-timepicker-table td { padding: 0.1em; width: 2.2em; } +.ui-timepicker-table th.periods { padding: 0.1em; width: 2.2em; } + +/* span for disabled cells */ +.ui-timepicker-table td span { + display:block; + padding:0.2em 0.3em 0.2em 0.5em; + width: 1.2em; + + text-align:right; + text-decoration:none; +} +/* anchors for clickable cells */ +.ui-timepicker-table td a { + display:block; + padding:0.2em 0.3em 0.2em 0.5em; + width: 1.2em; + cursor: pointer; + text-align:right; + text-decoration:none; +} + + +/* buttons and button pane styling */ +.ui-timepicker .ui-timepicker-buttonpane { + background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0; +} +.ui-timepicker .ui-timepicker-buttonpane button { margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; } +/* The close button */ +.ui-timepicker .ui-timepicker-close { float: right } + +/* the now button */ +.ui-timepicker .ui-timepicker-now { float: left; } + +/* the deselect button */ +.ui-timepicker .ui-timepicker-deselect { float: left; } + + diff --git a/tailbone/static/js/lib/jquery.ui.timepicker.js b/tailbone/static/js/lib/jquery.ui.timepicker.js new file mode 100644 index 00000000..d8a0cfb7 --- /dev/null +++ b/tailbone/static/js/lib/jquery.ui.timepicker.js @@ -0,0 +1,1496 @@ +/* + * jQuery UI Timepicker + * + * Copyright 2010-2013, Francois Gelinas + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://fgelinas.com/code/timepicker + * + * Depends: + * jquery.ui.core.js + * jquery.ui.position.js (only if position settings are used) + * + * Change version 0.1.0 - moved the t-rex up here + * + ____ + ___ .-~. /_"-._ + `-._~-. / /_ "~o\ :Y + \ \ / : \~x. ` ') + ] Y / | Y< ~-.__j + / ! _.--~T : l l< /.-~ + / / ____.--~ . ` l /~\ \<|Y + / / .-~~" /| . ',-~\ \L| + / / / .^ \ Y~Y \.^>/l_ "--' + / Y .-"( . l__ j_j l_/ /~_.-~ . + Y l / \ ) ~~~." / `/"~ / \.__/l_ + | \ _.-" ~-{__ l : l._Z~-.___.--~ + | ~---~ / ~~"---\_ ' __[> + l . _.^ ___ _>-y~ + \ \ . .-~ .-~ ~>--" / + \ ~---" / ./ _.-' + "-.,_____.,_ _.--~\ _.-~ + ~~ ( _} -Row + `. ~( + ) \ + /,`--'~\--'~\ + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ->T-Rex<- +*/ + +(function ($) { + + $.extend($.ui, { timepicker: { version: "0.3.3"} }); + + var PROP_NAME = 'timepicker', + tpuuid = new Date().getTime(); + + /* Time picker manager. + Use the singleton instance of this class, $.timepicker, to interact with the time picker. + Settings for (groups of) time pickers are maintained in an instance object, + allowing multiple different settings on the same page. */ + + function Timepicker() { + this.debug = true; // Change this to true to start debugging + this._curInst = null; // The current instance in use + this._disabledInputs = []; // List of time picker inputs that have been disabled + this._timepickerShowing = false; // True if the popup picker is showing , false if not + this._inDialog = false; // True if showing within a "dialog", false if not + this._dialogClass = 'ui-timepicker-dialog'; // The name of the dialog marker class + this._mainDivId = 'ui-timepicker-div'; // The ID of the main timepicker division + this._inlineClass = 'ui-timepicker-inline'; // The name of the inline marker class + this._currentClass = 'ui-timepicker-current'; // The name of the current hour / minutes marker class + this._dayOverClass = 'ui-timepicker-days-cell-over'; // The name of the day hover marker class + + this.regional = []; // Available regional settings, indexed by language code + this.regional[''] = { // Default regional settings + hourText: 'Hour', // Display text for hours section + minuteText: 'Minute', // Display text for minutes link + amPmText: ['AM', 'PM'], // Display text for AM PM + closeButtonText: 'Done', // Text for the confirmation button (ok button) + nowButtonText: 'Now', // Text for the now button + deselectButtonText: 'Deselect' // Text for the deselect button + }; + this._defaults = { // Global defaults for all the time picker instances + showOn: 'focus', // 'focus' for popup on focus, + // 'button' for trigger button, or 'both' for either (not yet implemented) + button: null, // 'button' element that will trigger the timepicker + showAnim: 'fadeIn', // Name of jQuery animation for popup + showOptions: {}, // Options for enhanced animations + appendText: '', // Display text following the input box, e.g. showing the format + + beforeShow: null, // Define a callback function executed before the timepicker is shown + onSelect: null, // Define a callback function when a hour / minutes is selected + onClose: null, // Define a callback function when the timepicker is closed + + timeSeparator: ':', // The character to use to separate hours and minutes. + periodSeparator: ' ', // The character to use to separate the time from the time period. + showPeriod: false, // Define whether or not to show AM/PM with selected time + showPeriodLabels: true, // Show the AM/PM labels on the left of the time picker + showLeadingZero: true, // Define whether or not to show a leading zero for hours < 10. [true/false] + showMinutesLeadingZero: true, // Define whether or not to show a leading zero for minutes < 10. + altField: '', // Selector for an alternate field to store selected time into + defaultTime: 'now', // Used as default time when input field is empty or for inline timePicker + // (set to 'now' for the current time, '' for no highlighted time) + myPosition: 'left top', // Position of the dialog relative to the input. + // see the position utility for more info : http://jqueryui.com/demos/position/ + atPosition: 'left bottom', // Position of the input element to match + // Note : if the position utility is not loaded, the timepicker will attach left top to left bottom + //NEW: 2011-02-03 + onHourShow: null, // callback for enabling / disabling on selectable hours ex : function(hour) { return true; } + onMinuteShow: null, // callback for enabling / disabling on time selection ex : function(hour,minute) { return true; } + + hours: { + starts: 0, // first displayed hour + ends: 23 // last displayed hour + }, + minutes: { + starts: 0, // first displayed minute + ends: 55, // last displayed minute + interval: 5, // interval of displayed minutes + manual: [] // optional extra manual entries for minutes + }, + rows: 4, // number of rows for the input tables, minimum 2, makes more sense if you use multiple of 2 + // 2011-08-05 0.2.4 + showHours: true, // display the hours section of the dialog + showMinutes: true, // display the minute section of the dialog + optionalMinutes: false, // optionally parse inputs of whole hours with minutes omitted + + // buttons + showCloseButton: false, // shows an OK button to confirm the edit + showNowButton: false, // Shows the 'now' button + showDeselectButton: false, // Shows the deselect time button + + maxTime: { + hour: null, + minute: null + }, + minTime: { + hour: null, + minute: null + } + + }; + $.extend(this._defaults, this.regional['']); + + this.tpDiv = $(''); + } + + $.extend(Timepicker.prototype, { + /* Class name added to elements to indicate already configured with a time picker. */ + markerClassName: 'hasTimepicker', + + /* Debug logging (if enabled). */ + log: function () { + if (this.debug) + console.log.apply('', arguments); + }, + + _widgetTimepicker: function () { + return this.tpDiv; + }, + + /* Override the default settings for all instances of the time picker. + @param settings object - the new settings to use as defaults (anonymous object) + @return the manager object */ + setDefaults: function (settings) { + extendRemove(this._defaults, settings || {}); + return this; + }, + + /* Attach the time picker to a jQuery selection. + @param target element - the target input field or division or span + @param settings object - the new settings to use for this time picker instance (anonymous) */ + _attachTimepicker: function (target, settings) { + // check for settings on the control itself - in namespace 'time:' + var inlineSettings = null; + for (var attrName in this._defaults) { + var attrValue = target.getAttribute('time:' + attrName); + if (attrValue) { + inlineSettings = inlineSettings || {}; + try { + inlineSettings[attrName] = eval(attrValue); + } catch (err) { + inlineSettings[attrName] = attrValue; + } + } + } + var nodeName = target.nodeName.toLowerCase(); + var inline = (nodeName == 'div' || nodeName == 'span'); + + if (!target.id) { + this.uuid += 1; + target.id = 'tp' + this.uuid; + } + var inst = this._newInst($(target), inline); + inst.settings = $.extend({}, settings || {}, inlineSettings || {}); + if (nodeName == 'input') { + this._connectTimepicker(target, inst); + // init inst.hours and inst.minutes from the input value + this._setTimeFromField(inst); + } else if (inline) { + this._inlineTimepicker(target, inst); + } + + + }, + + /* Create a new instance object. */ + _newInst: function (target, inline) { + var id = target[0].id.replace(/([^A-Za-z0-9_-])/g, '\\\\$1'); // escape jQuery meta chars + return { + id: id, input: target, // associated target + inline: inline, // is timepicker inline or not : + tpDiv: (!inline ? this.tpDiv : // presentation div + $('
')) + }; + }, + + /* Attach the time picker to an input field. */ + _connectTimepicker: function (target, inst) { + var input = $(target); + inst.append = $([]); + inst.trigger = $([]); + if (input.hasClass(this.markerClassName)) { return; } + this._attachments(input, inst); + input.addClass(this.markerClassName). + keydown(this._doKeyDown). + keyup(this._doKeyUp). + bind("setData.timepicker", function (event, key, value) { + inst.settings[key] = value; + }). + bind("getData.timepicker", function (event, key) { + return this._get(inst, key); + }); + $.data(target, PROP_NAME, inst); + }, + + /* Handle keystrokes. */ + _doKeyDown: function (event) { + var inst = $.timepicker._getInst(event.target); + var handled = true; + inst._keyEvent = true; + if ($.timepicker._timepickerShowing) { + switch (event.keyCode) { + case 9: $.timepicker._hideTimepicker(); + handled = false; + break; // hide on tab out + case 13: + $.timepicker._updateSelectedValue(inst); + $.timepicker._hideTimepicker(); + + return false; // don't submit the form + break; // select the value on enter + case 27: $.timepicker._hideTimepicker(); + break; // hide on escape + default: handled = false; + } + } + else if (event.keyCode == 36 && event.ctrlKey) { // display the time picker on ctrl+home + $.timepicker._showTimepicker(this); + } + else { + handled = false; + } + if (handled) { + event.preventDefault(); + event.stopPropagation(); + } + }, + + /* Update selected time on keyUp */ + /* Added verion 0.0.5 */ + _doKeyUp: function (event) { + var inst = $.timepicker._getInst(event.target); + $.timepicker._setTimeFromField(inst); + $.timepicker._updateTimepicker(inst); + }, + + /* Make attachments based on settings. */ + _attachments: function (input, inst) { + var appendText = this._get(inst, 'appendText'); + var isRTL = this._get(inst, 'isRTL'); + if (inst.append) { inst.append.remove(); } + if (appendText) { + inst.append = $('' + appendText + ''); + input[isRTL ? 'before' : 'after'](inst.append); + } + input.unbind('focus.timepicker', this._showTimepicker); + input.unbind('click.timepicker', this._adjustZIndex); + + if (inst.trigger) { inst.trigger.remove(); } + + var showOn = this._get(inst, 'showOn'); + if (showOn == 'focus' || showOn == 'both') { // pop-up time picker when in the marked field + input.bind("focus.timepicker", this._showTimepicker); + input.bind("click.timepicker", this._adjustZIndex); + } + if (showOn == 'button' || showOn == 'both') { // pop-up time picker when 'button' element is clicked + var button = this._get(inst, 'button'); + + // Add button if button element is not set + if(button == null) { + button = $(''); + input.after(button); + } + + $(button).bind("click.timepicker", function () { + if ($.timepicker._timepickerShowing && $.timepicker._lastInput == input[0]) { + $.timepicker._hideTimepicker(); + } else if (!inst.input.is(':disabled')) { + $.timepicker._showTimepicker(input[0]); + } + return false; + }); + + } + }, + + + /* Attach an inline time picker to a div. */ + _inlineTimepicker: function(target, inst) { + var divSpan = $(target); + if (divSpan.hasClass(this.markerClassName)) + return; + divSpan.addClass(this.markerClassName).append(inst.tpDiv). + bind("setData.timepicker", function(event, key, value){ + inst.settings[key] = value; + }).bind("getData.timepicker", function(event, key){ + return this._get(inst, key); + }); + $.data(target, PROP_NAME, inst); + + this._setTimeFromField(inst); + this._updateTimepicker(inst); + inst.tpDiv.show(); + }, + + _adjustZIndex: function(input) { + input = input.target || input; + var inst = $.timepicker._getInst(input); + inst.tpDiv.css('zIndex', $.timepicker._getZIndex(input) +1); + }, + + /* Pop-up the time picker for a given input field. + @param input element - the input field attached to the time picker or + event - if triggered by focus */ + _showTimepicker: function (input) { + input = input.target || input; + if (input.nodeName.toLowerCase() != 'input') { input = $('input', input.parentNode)[0]; } // find from button/image trigger + + if ($.timepicker._isDisabledTimepicker(input) || $.timepicker._lastInput == input) { return; } // already here + + // fix v 0.0.8 - close current timepicker before showing another one + $.timepicker._hideTimepicker(); + + var inst = $.timepicker._getInst(input); + if ($.timepicker._curInst && $.timepicker._curInst != inst) { + $.timepicker._curInst.tpDiv.stop(true, true); + } + var beforeShow = $.timepicker._get(inst, 'beforeShow'); + extendRemove(inst.settings, (beforeShow ? beforeShow.apply(input, [input, inst]) : {})); + inst.lastVal = null; + $.timepicker._lastInput = input; + + $.timepicker._setTimeFromField(inst); + + // calculate default position + if ($.timepicker._inDialog) { input.value = ''; } // hide cursor + if (!$.timepicker._pos) { // position below input + $.timepicker._pos = $.timepicker._findPos(input); + $.timepicker._pos[1] += input.offsetHeight; // add the height + } + var isFixed = false; + $(input).parents().each(function () { + isFixed |= $(this).css('position') == 'fixed'; + return !isFixed; + }); + + var offset = { left: $.timepicker._pos[0], top: $.timepicker._pos[1] }; + + $.timepicker._pos = null; + // determine sizing offscreen + inst.tpDiv.css({ position: 'absolute', display: 'block', top: '-1000px' }); + $.timepicker._updateTimepicker(inst); + + + // position with the ui position utility, if loaded + if ( ( ! inst.inline ) && ( typeof $.ui.position == 'object' ) ) { + inst.tpDiv.position({ + of: inst.input, + my: $.timepicker._get( inst, 'myPosition' ), + at: $.timepicker._get( inst, 'atPosition' ), + // offset: $( "#offset" ).val(), + // using: using, + collision: 'flip' + }); + var offset = inst.tpDiv.offset(); + $.timepicker._pos = [offset.top, offset.left]; + } + + + // reset clicked state + inst._hoursClicked = false; + inst._minutesClicked = false; + + // fix width for dynamic number of time pickers + // and adjust position before showing + offset = $.timepicker._checkOffset(inst, offset, isFixed); + inst.tpDiv.css({ position: ($.timepicker._inDialog && $.blockUI ? + 'static' : (isFixed ? 'fixed' : 'absolute')), display: 'none', + left: offset.left + 'px', top: offset.top + 'px' + }); + if ( ! inst.inline ) { + var showAnim = $.timepicker._get(inst, 'showAnim'); + var duration = $.timepicker._get(inst, 'duration'); + + var postProcess = function () { + $.timepicker._timepickerShowing = true; + var borders = $.timepicker._getBorders(inst.tpDiv); + inst.tpDiv.find('iframe.ui-timepicker-cover'). // IE6- only + css({ left: -borders[0], top: -borders[1], + width: inst.tpDiv.outerWidth(), height: inst.tpDiv.outerHeight() + }); + }; + + // Fixed the zIndex problem for real (I hope) - FG - v 0.2.9 + $.timepicker._adjustZIndex(input); + //inst.tpDiv.css('zIndex', $.timepicker._getZIndex(input) +1); + + if ($.effects && $.effects[showAnim]) { + inst.tpDiv.show(showAnim, $.timepicker._get(inst, 'showOptions'), duration, postProcess); + } + else { + inst.tpDiv.show((showAnim ? duration : null), postProcess); + } + if (!showAnim || !duration) { postProcess(); } + if (inst.input.is(':visible') && !inst.input.is(':disabled')) { inst.input.focus(); } + $.timepicker._curInst = inst; + } + }, + + // This is an enhanced copy of the zIndex function of UI core 1.8.?? For backward compatibility. + // Enhancement returns maximum zindex value discovered while traversing parent elements, + // rather than the first zindex value found. Ensures the timepicker popup will be in front, + // even in funky scenarios like non-jq dialog containers with large fixed zindex values and + // nested zindex-influenced elements of their own. + _getZIndex: function (target) { + var elem = $(target); + var maxValue = 0; + var position, value; + while (elem.length && elem[0] !== document) { + position = elem.css("position"); + if (position === "absolute" || position === "relative" || position === "fixed") { + value = parseInt(elem.css("zIndex"), 10); + if (!isNaN(value) && value !== 0) { + if (value > maxValue) { maxValue = value; } + } + } + elem = elem.parent(); + } + + return maxValue; + }, + + /* Refresh the time picker + @param target element - The target input field or inline container element. */ + _refreshTimepicker: function(target) { + var inst = this._getInst(target); + if (inst) { + this._updateTimepicker(inst); + } + }, + + + /* Generate the time picker content. */ + _updateTimepicker: function (inst) { + inst.tpDiv.empty().append(this._generateHTML(inst)); + this._rebindDialogEvents(inst); + + }, + + _rebindDialogEvents: function (inst) { + var borders = $.timepicker._getBorders(inst.tpDiv), + self = this; + inst.tpDiv + .find('iframe.ui-timepicker-cover') // IE6- only + .css({ left: -borders[0], top: -borders[1], + width: inst.tpDiv.outerWidth(), height: inst.tpDiv.outerHeight() + }) + .end() + // after the picker html is appended bind the click & double click events (faster in IE this way + // then letting the browser interpret the inline events) + // the binding for the minute cells also exists in _updateMinuteDisplay + .find('.ui-timepicker-minute-cell') + .unbind() + .bind("click", { fromDoubleClick:false }, $.proxy($.timepicker.selectMinutes, this)) + .bind("dblclick", { fromDoubleClick:true }, $.proxy($.timepicker.selectMinutes, this)) + .end() + .find('.ui-timepicker-hour-cell') + .unbind() + .bind("click", { fromDoubleClick:false }, $.proxy($.timepicker.selectHours, this)) + .bind("dblclick", { fromDoubleClick:true }, $.proxy($.timepicker.selectHours, this)) + .end() + .find('.ui-timepicker td a') + .unbind() + .bind('mouseout', function () { + $(this).removeClass('ui-state-hover'); + if (this.className.indexOf('ui-timepicker-prev') != -1) $(this).removeClass('ui-timepicker-prev-hover'); + if (this.className.indexOf('ui-timepicker-next') != -1) $(this).removeClass('ui-timepicker-next-hover'); + }) + .bind('mouseover', function () { + if ( ! self._isDisabledTimepicker(inst.inline ? inst.tpDiv.parent()[0] : inst.input[0])) { + $(this).parents('.ui-timepicker-calendar').find('a').removeClass('ui-state-hover'); + $(this).addClass('ui-state-hover'); + if (this.className.indexOf('ui-timepicker-prev') != -1) $(this).addClass('ui-timepicker-prev-hover'); + if (this.className.indexOf('ui-timepicker-next') != -1) $(this).addClass('ui-timepicker-next-hover'); + } + }) + .end() + .find('.' + this._dayOverClass + ' a') + .trigger('mouseover') + .end() + .find('.ui-timepicker-now').bind("click", function(e) { + $.timepicker.selectNow(e); + }).end() + .find('.ui-timepicker-deselect').bind("click",function(e) { + $.timepicker.deselectTime(e); + }).end() + .find('.ui-timepicker-close').bind("click",function(e) { + $.timepicker._hideTimepicker(); + }).end(); + }, + + /* Generate the HTML for the current state of the time picker. */ + _generateHTML: function (inst) { + + var h, m, row, col, html, hoursHtml, minutesHtml = '', + showPeriod = (this._get(inst, 'showPeriod') == true), + showPeriodLabels = (this._get(inst, 'showPeriodLabels') == true), + showLeadingZero = (this._get(inst, 'showLeadingZero') == true), + showHours = (this._get(inst, 'showHours') == true), + showMinutes = (this._get(inst, 'showMinutes') == true), + amPmText = this._get(inst, 'amPmText'), + rows = this._get(inst, 'rows'), + amRows = 0, + pmRows = 0, + amItems = 0, + pmItems = 0, + amFirstRow = 0, + pmFirstRow = 0, + hours = Array(), + hours_options = this._get(inst, 'hours'), + hoursPerRow = null, + hourCounter = 0, + hourLabel = this._get(inst, 'hourText'), + showCloseButton = this._get(inst, 'showCloseButton'), + closeButtonText = this._get(inst, 'closeButtonText'), + showNowButton = this._get(inst, 'showNowButton'), + nowButtonText = this._get(inst, 'nowButtonText'), + showDeselectButton = this._get(inst, 'showDeselectButton'), + deselectButtonText = this._get(inst, 'deselectButtonText'), + showButtonPanel = showCloseButton || showNowButton || showDeselectButton; + + + + // prepare all hours and minutes, makes it easier to distribute by rows + for (h = hours_options.starts; h <= hours_options.ends; h++) { + hours.push (h); + } + hoursPerRow = Math.ceil(hours.length / rows); // always round up + + if (showPeriodLabels) { + for (hourCounter = 0; hourCounter < hours.length; hourCounter++) { + if (hours[hourCounter] < 12) { + amItems++; + } + else { + pmItems++; + } + } + hourCounter = 0; + + amRows = Math.floor(amItems / hours.length * rows); + pmRows = Math.floor(pmItems / hours.length * rows); + + // assign the extra row to the period that is more densely populated + if (rows != amRows + pmRows) { + // Make sure: AM Has Items and either PM Does Not, AM has no rows yet, or AM is more dense + if (amItems && (!pmItems || !amRows || (pmRows && amItems / amRows >= pmItems / pmRows))) { + amRows++; + } else { + pmRows++; + } + } + amFirstRow = Math.min(amRows, 1); + pmFirstRow = amRows + 1; + + if (amRows == 0) { + hoursPerRow = Math.ceil(pmItems / pmRows); + } else if (pmRows == 0) { + hoursPerRow = Math.ceil(amItems / amRows); + } else { + hoursPerRow = Math.ceil(Math.max(amItems / amRows, pmItems / pmRows)); + } + } + + + html = ''; + + if (showHours) { + + html += ''; // Close the Hour td + } + + if (showMinutes) { + html += ''; + } + + html += ''; + + + if (showButtonPanel) { + var buttonPanel = ''; + } + html += '
' + + '
' + + hourLabel + + '
' + + ''; + + for (row = 1; row <= rows; row++) { + html += ''; + // AM + if (row == amFirstRow && showPeriodLabels) { + html += ''; + } + // PM + if (row == pmFirstRow && showPeriodLabels) { + html += ''; + } + for (col = 1; col <= hoursPerRow; col++) { + if (showPeriodLabels && row < pmFirstRow && hours[hourCounter] >= 12) { + html += this._generateHTMLHourCell(inst, undefined, showPeriod, showLeadingZero); + } else { + html += this._generateHTMLHourCell(inst, hours[hourCounter], showPeriod, showLeadingZero); + hourCounter++; + } + } + html += ''; + } + html += '
' + amPmText[0] + '' + amPmText[1] + '
' + // Close the hours cells table + '
'; + html += this._generateHTMLMinutes(inst); + html += '
'; + if (showNowButton) { + buttonPanel += ''; + } + if (showDeselectButton) { + buttonPanel += ''; + } + if (showCloseButton) { + buttonPanel += ''; + } + + html += buttonPanel + '
'; + + return html; + }, + + /* Special function that update the minutes selection in currently visible timepicker + * called on hour selection when onMinuteShow is defined */ + _updateMinuteDisplay: function (inst) { + var newHtml = this._generateHTMLMinutes(inst); + inst.tpDiv.find('td.ui-timepicker-minutes').html(newHtml); + this._rebindDialogEvents(inst); + // after the picker html is appended bind the click & double click events (faster in IE this way + // then letting the browser interpret the inline events) + // yes I know, duplicate code, sorry +/* .find('.ui-timepicker-minute-cell') + .bind("click", { fromDoubleClick:false }, $.proxy($.timepicker.selectMinutes, this)) + .bind("dblclick", { fromDoubleClick:true }, $.proxy($.timepicker.selectMinutes, this)); +*/ + + }, + + /* + * Generate the minutes table + * This is separated from the _generateHTML function because is can be called separately (when hours changes) + */ + _generateHTMLMinutes: function (inst) { + + var m, row, html = '', + rows = this._get(inst, 'rows'), + minutes = Array(), + minutes_options = this._get(inst, 'minutes'), + minutesPerRow = null, + minuteCounter = 0, + showMinutesLeadingZero = (this._get(inst, 'showMinutesLeadingZero') == true), + onMinuteShow = this._get(inst, 'onMinuteShow'), + minuteLabel = this._get(inst, 'minuteText'); + + if ( ! minutes_options.starts) { + minutes_options.starts = 0; + } + if ( ! minutes_options.ends) { + minutes_options.ends = 59; + } + if ( ! minutes_options.manual) { + minutes_options.manual = []; + } + for (m = minutes_options.starts; m <= minutes_options.ends; m += minutes_options.interval) { + minutes.push(m); + } + for (i = 0; i < minutes_options.manual.length;i++) { + var currMin = minutes_options.manual[i]; + + // Validate & filter duplicates of manual minute input + if (typeof currMin != 'number' || currMin < 0 || currMin > 59 || $.inArray(currMin, minutes) >= 0) { + continue; + } + minutes.push(currMin); + } + + // Sort to get correct order after adding manual minutes + // Use compare function to sort by number, instead of string (default) + minutes.sort(function(a, b) { + return a-b; + }); + + minutesPerRow = Math.round(minutes.length / rows + 0.49); // always round up + + /* + * The minutes table + */ + // if currently selected minute is not enabled, we have a problem and need to select a new minute. + if (onMinuteShow && + (onMinuteShow.apply((inst.input ? inst.input[0] : null), [inst.hours , inst.minutes]) == false) ) { + // loop minutes and select first available + for (minuteCounter = 0; minuteCounter < minutes.length; minuteCounter += 1) { + m = minutes[minuteCounter]; + if (onMinuteShow.apply((inst.input ? inst.input[0] : null), [inst.hours, m])) { + inst.minutes = m; + break; + } + } + } + + + + html += '
' + + minuteLabel + + '
' + + ''; + + minuteCounter = 0; + for (row = 1; row <= rows; row++) { + html += ''; + while (minuteCounter < row * minutesPerRow) { + var m = minutes[minuteCounter]; + var displayText = ''; + if (m !== undefined ) { + displayText = (m < 10) && showMinutesLeadingZero ? "0" + m.toString() : m.toString(); + } + html += this._generateHTMLMinuteCell(inst, m, displayText); + minuteCounter++; + } + html += ''; + } + + html += '
'; + + return html; + }, + + /* Generate the content of a "Hour" cell */ + _generateHTMLHourCell: function (inst, hour, showPeriod, showLeadingZero) { + + var displayHour = hour; + if ((hour > 12) && showPeriod) { + displayHour = hour - 12; + } + if ((displayHour == 0) && showPeriod) { + displayHour = 12; + } + if ((displayHour < 10) && showLeadingZero) { + displayHour = '0' + displayHour; + } + + var html = ""; + var enabled = true; + var onHourShow = this._get(inst, 'onHourShow'); //custom callback + var maxTime = this._get(inst, 'maxTime'); + var minTime = this._get(inst, 'minTime'); + + if (hour == undefined) { + html = ' '; + return html; + } + + if (onHourShow) { + enabled = onHourShow.apply((inst.input ? inst.input[0] : null), [hour]); + } + + if (enabled) { + if ( !isNaN(parseInt(maxTime.hour)) && hour > maxTime.hour ) enabled = false; + if ( !isNaN(parseInt(minTime.hour)) && hour < minTime.hour ) enabled = false; + } + + if (enabled) { + html = '' + + '' + + displayHour.toString() + + ''; + } + else { + html = + '' + + '' + + displayHour.toString() + + '' + + ''; + } + return html; + }, + + /* Generate the content of a "Hour" cell */ + _generateHTMLMinuteCell: function (inst, minute, displayText) { + var html = ""; + var enabled = true; + var hour = inst.hours; + var onMinuteShow = this._get(inst, 'onMinuteShow'); //custom callback + var maxTime = this._get(inst, 'maxTime'); + var minTime = this._get(inst, 'minTime'); + + if (onMinuteShow) { + //NEW: 2011-02-03 we should give the hour as a parameter as well! + enabled = onMinuteShow.apply((inst.input ? inst.input[0] : null), [inst.hours,minute]); //trigger callback + } + + if (minute == undefined) { + html = ' '; + return html; + } + + if (enabled && hour !== null) { + if ( !isNaN(parseInt(maxTime.hour)) && !isNaN(parseInt(maxTime.minute)) && hour >= maxTime.hour && minute > maxTime.minute ) enabled = false; + if ( !isNaN(parseInt(minTime.hour)) && !isNaN(parseInt(minTime.minute)) && hour <= minTime.hour && minute < minTime.minute ) enabled = false; + } + + if (enabled) { + html = '' + + '' + + displayText + + ''; + } + else { + + html = '' + + '' + + displayText + + '' + + ''; + } + return html; + }, + + + /* Detach a timepicker from its control. + @param target element - the target input field or division or span */ + _destroyTimepicker: function(target) { + var $target = $(target); + var inst = $.data(target, PROP_NAME); + if (!$target.hasClass(this.markerClassName)) { + return; + } + var nodeName = target.nodeName.toLowerCase(); + $.removeData(target, PROP_NAME); + if (nodeName == 'input') { + inst.append.remove(); + inst.trigger.remove(); + $target.removeClass(this.markerClassName) + .unbind('focus.timepicker', this._showTimepicker) + .unbind('click.timepicker', this._adjustZIndex); + } else if (nodeName == 'div' || nodeName == 'span') + $target.removeClass(this.markerClassName).empty(); + }, + + /* Enable the date picker to a jQuery selection. + @param target element - the target input field or division or span */ + _enableTimepicker: function(target) { + var $target = $(target), + target_id = $target.attr('id'), + inst = $.data(target, PROP_NAME); + + if (!$target.hasClass(this.markerClassName)) { + return; + } + var nodeName = target.nodeName.toLowerCase(); + if (nodeName == 'input') { + target.disabled = false; + var button = this._get(inst, 'button'); + $(button).removeClass('ui-state-disabled').disabled = false; + inst.trigger.filter('button'). + each(function() { this.disabled = false; }).end(); + } + else if (nodeName == 'div' || nodeName == 'span') { + var inline = $target.children('.' + this._inlineClass); + inline.children().removeClass('ui-state-disabled'); + inline.find('button').each( + function() { this.disabled = false } + ) + } + this._disabledInputs = $.map(this._disabledInputs, + function(value) { return (value == target_id ? null : value); }); // delete entry + }, + + /* Disable the time picker to a jQuery selection. + @param target element - the target input field or division or span */ + _disableTimepicker: function(target) { + var $target = $(target); + var inst = $.data(target, PROP_NAME); + if (!$target.hasClass(this.markerClassName)) { + return; + } + var nodeName = target.nodeName.toLowerCase(); + if (nodeName == 'input') { + var button = this._get(inst, 'button'); + + $(button).addClass('ui-state-disabled').disabled = true; + target.disabled = true; + + inst.trigger.filter('button'). + each(function() { this.disabled = true; }).end(); + + } + else if (nodeName == 'div' || nodeName == 'span') { + var inline = $target.children('.' + this._inlineClass); + inline.children().addClass('ui-state-disabled'); + inline.find('button').each( + function() { this.disabled = true } + ) + + } + this._disabledInputs = $.map(this._disabledInputs, + function(value) { return (value == target ? null : value); }); // delete entry + this._disabledInputs[this._disabledInputs.length] = $target.attr('id'); + }, + + /* Is the first field in a jQuery collection disabled as a timepicker? + @param target_id element - the target input field or division or span + @return boolean - true if disabled, false if enabled */ + _isDisabledTimepicker: function (target_id) { + if ( ! target_id) { return false; } + for (var i = 0; i < this._disabledInputs.length; i++) { + if (this._disabledInputs[i] == target_id) { return true; } + } + return false; + }, + + /* Check positioning to remain on screen. */ + _checkOffset: function (inst, offset, isFixed) { + var tpWidth = inst.tpDiv.outerWidth(); + var tpHeight = inst.tpDiv.outerHeight(); + var inputWidth = inst.input ? inst.input.outerWidth() : 0; + var inputHeight = inst.input ? inst.input.outerHeight() : 0; + var viewWidth = document.documentElement.clientWidth + $(document).scrollLeft(); + var viewHeight = document.documentElement.clientHeight + $(document).scrollTop(); + + offset.left -= (this._get(inst, 'isRTL') ? (tpWidth - inputWidth) : 0); + offset.left -= (isFixed && offset.left == inst.input.offset().left) ? $(document).scrollLeft() : 0; + offset.top -= (isFixed && offset.top == (inst.input.offset().top + inputHeight)) ? $(document).scrollTop() : 0; + + // now check if timepicker is showing outside window viewport - move to a better place if so. + offset.left -= Math.min(offset.left, (offset.left + tpWidth > viewWidth && viewWidth > tpWidth) ? + Math.abs(offset.left + tpWidth - viewWidth) : 0); + offset.top -= Math.min(offset.top, (offset.top + tpHeight > viewHeight && viewHeight > tpHeight) ? + Math.abs(tpHeight + inputHeight) : 0); + + return offset; + }, + + /* Find an object's position on the screen. */ + _findPos: function (obj) { + var inst = this._getInst(obj); + var isRTL = this._get(inst, 'isRTL'); + while (obj && (obj.type == 'hidden' || obj.nodeType != 1)) { + obj = obj[isRTL ? 'previousSibling' : 'nextSibling']; + } + var position = $(obj).offset(); + return [position.left, position.top]; + }, + + /* Retrieve the size of left and top borders for an element. + @param elem (jQuery object) the element of interest + @return (number[2]) the left and top borders */ + _getBorders: function (elem) { + var convert = function (value) { + return { thin: 1, medium: 2, thick: 3}[value] || value; + }; + return [parseFloat(convert(elem.css('border-left-width'))), + parseFloat(convert(elem.css('border-top-width')))]; + }, + + + /* Close time picker if clicked elsewhere. */ + _checkExternalClick: function (event) { + if (!$.timepicker._curInst) { return; } + var $target = $(event.target); + if ($target[0].id != $.timepicker._mainDivId && + $target.parents('#' + $.timepicker._mainDivId).length == 0 && + !$target.hasClass($.timepicker.markerClassName) && + !$target.hasClass($.timepicker._triggerClass) && + $.timepicker._timepickerShowing && !($.timepicker._inDialog && $.blockUI)) + $.timepicker._hideTimepicker(); + }, + + /* Hide the time picker from view. + @param input element - the input field attached to the time picker */ + _hideTimepicker: function (input) { + var inst = this._curInst; + if (!inst || (input && inst != $.data(input, PROP_NAME))) { return; } + if (this._timepickerShowing) { + var showAnim = this._get(inst, 'showAnim'); + var duration = this._get(inst, 'duration'); + var postProcess = function () { + $.timepicker._tidyDialog(inst); + this._curInst = null; + }; + if ($.effects && $.effects[showAnim]) { + inst.tpDiv.hide(showAnim, $.timepicker._get(inst, 'showOptions'), duration, postProcess); + } + else { + inst.tpDiv[(showAnim == 'slideDown' ? 'slideUp' : + (showAnim == 'fadeIn' ? 'fadeOut' : 'hide'))]((showAnim ? duration : null), postProcess); + } + if (!showAnim) { postProcess(); } + + this._timepickerShowing = false; + + this._lastInput = null; + if (this._inDialog) { + this._dialogInput.css({ position: 'absolute', left: '0', top: '-100px' }); + if ($.blockUI) { + $.unblockUI(); + $('body').append(this.tpDiv); + } + } + this._inDialog = false; + + var onClose = this._get(inst, 'onClose'); + if (onClose) { + onClose.apply( + (inst.input ? inst.input[0] : null), + [(inst.input ? inst.input.val() : ''), inst]); // trigger custom callback + } + + } + }, + + + + /* Tidy up after a dialog display. */ + _tidyDialog: function (inst) { + inst.tpDiv.removeClass(this._dialogClass).unbind('.ui-timepicker'); + }, + + /* Retrieve the instance data for the target control. + @param target element - the target input field or division or span + @return object - the associated instance data + @throws error if a jQuery problem getting data */ + _getInst: function (target) { + try { + return $.data(target, PROP_NAME); + } + catch (err) { + throw 'Missing instance data for this timepicker'; + } + }, + + /* Get a setting value, defaulting if necessary. */ + _get: function (inst, name) { + return inst.settings[name] !== undefined ? + inst.settings[name] : this._defaults[name]; + }, + + /* Parse existing time and initialise time picker. */ + _setTimeFromField: function (inst) { + if (inst.input.val() == inst.lastVal) { return; } + var defaultTime = this._get(inst, 'defaultTime'); + + var timeToParse = defaultTime == 'now' ? this._getCurrentTimeRounded(inst) : defaultTime; + if ((inst.inline == false) && (inst.input.val() != '')) { timeToParse = inst.input.val() } + + if (timeToParse instanceof Date) { + inst.hours = timeToParse.getHours(); + inst.minutes = timeToParse.getMinutes(); + } else { + var timeVal = inst.lastVal = timeToParse; + if (timeToParse == '') { + inst.hours = -1; + inst.minutes = -1; + } else { + var time = this.parseTime(inst, timeVal); + inst.hours = time.hours; + inst.minutes = time.minutes; + } + } + + + $.timepicker._updateTimepicker(inst); + }, + + /* Update or retrieve the settings for an existing time picker. + @param target element - the target input field or division or span + @param name object - the new settings to update or + string - the name of the setting to change or retrieve, + when retrieving also 'all' for all instance settings or + 'defaults' for all global defaults + @param value any - the new value for the setting + (omit if above is an object or to retrieve a value) */ + _optionTimepicker: function(target, name, value) { + var inst = this._getInst(target); + if (arguments.length == 2 && typeof name == 'string') { + return (name == 'defaults' ? $.extend({}, $.timepicker._defaults) : + (inst ? (name == 'all' ? $.extend({}, inst.settings) : + this._get(inst, name)) : null)); + } + var settings = name || {}; + if (typeof name == 'string') { + settings = {}; + settings[name] = value; + } + if (inst) { + extendRemove(inst.settings, settings); + if (this._curInst == inst) { + this._hideTimepicker(); + this._updateTimepicker(inst); + } + if (inst.inline) { + this._updateTimepicker(inst); + } + } + }, + + + /* Set the time for a jQuery selection. + @param target element - the target input field or division or span + @param time String - the new time */ + _setTimeTimepicker: function(target, time) { + var inst = this._getInst(target); + if (inst) { + this._setTime(inst, time); + this._updateTimepicker(inst); + this._updateAlternate(inst, time); + } + }, + + /* Set the time directly. */ + _setTime: function(inst, time, noChange) { + var origHours = inst.hours; + var origMinutes = inst.minutes; + if (time instanceof Date) { + inst.hours = time.getHours(); + inst.minutes = time.getMinutes(); + } else { + var time = this.parseTime(inst, time); + inst.hours = time.hours; + inst.minutes = time.minutes; + } + + if ((origHours != inst.hours || origMinutes != inst.minutes) && !noChange) { + inst.input.trigger('change'); + } + this._updateTimepicker(inst); + this._updateSelectedValue(inst); + }, + + /* Return the current time, ready to be parsed, rounded to the closest minute by interval */ + _getCurrentTimeRounded: function (inst) { + var currentTime = new Date(), + currentMinutes = currentTime.getMinutes(), + minutes_options = this._get(inst, 'minutes'), + // round to closest interval + adjustedMinutes = Math.round(currentMinutes / minutes_options.interval) * minutes_options.interval; + currentTime.setMinutes(adjustedMinutes); + return currentTime; + }, + + /* + * Parse a time string into hours and minutes + */ + parseTime: function (inst, timeVal) { + var retVal = new Object(); + retVal.hours = -1; + retVal.minutes = -1; + + if(!timeVal) + return ''; + + var timeSeparator = this._get(inst, 'timeSeparator'), + amPmText = this._get(inst, 'amPmText'), + showHours = this._get(inst, 'showHours'), + showMinutes = this._get(inst, 'showMinutes'), + optionalMinutes = this._get(inst, 'optionalMinutes'), + showPeriod = (this._get(inst, 'showPeriod') == true), + p = timeVal.indexOf(timeSeparator); + + // check if time separator found + if (p != -1) { + retVal.hours = parseInt(timeVal.substr(0, p), 10); + retVal.minutes = parseInt(timeVal.substr(p + 1), 10); + } + // check for hours only + else if ( (showHours) && ( !showMinutes || optionalMinutes ) ) { + retVal.hours = parseInt(timeVal, 10); + } + // check for minutes only + else if ( ( ! showHours) && (showMinutes) ) { + retVal.minutes = parseInt(timeVal, 10); + } + + if (showHours) { + var timeValUpper = timeVal.toUpperCase(); + if ((retVal.hours < 12) && (showPeriod) && (timeValUpper.indexOf(amPmText[1].toUpperCase()) != -1)) { + retVal.hours += 12; + } + // fix for 12 AM + if ((retVal.hours == 12) && (showPeriod) && (timeValUpper.indexOf(amPmText[0].toUpperCase()) != -1)) { + retVal.hours = 0; + } + } + + return retVal; + }, + + selectNow: function(event) { + var id = $(event.target).attr("data-timepicker-instance-id"), + $target = $(id), + inst = this._getInst($target[0]); + //if (!inst || (input && inst != $.data(input, PROP_NAME))) { return; } + var currentTime = new Date(); + inst.hours = currentTime.getHours(); + inst.minutes = currentTime.getMinutes(); + this._updateSelectedValue(inst); + this._updateTimepicker(inst); + this._hideTimepicker(); + }, + + deselectTime: function(event) { + var id = $(event.target).attr("data-timepicker-instance-id"), + $target = $(id), + inst = this._getInst($target[0]); + inst.hours = -1; + inst.minutes = -1; + this._updateSelectedValue(inst); + this._hideTimepicker(); + }, + + + selectHours: function (event) { + var $td = $(event.currentTarget), + id = $td.attr("data-timepicker-instance-id"), + newHours = parseInt($td.attr("data-hour")), + fromDoubleClick = event.data.fromDoubleClick, + $target = $(id), + inst = this._getInst($target[0]), + showMinutes = (this._get(inst, 'showMinutes') == true); + + // don't select if disabled + if ( $.timepicker._isDisabledTimepicker($target.attr('id')) ) { return false } + + $td.parents('.ui-timepicker-hours:first').find('a').removeClass('ui-state-active'); + $td.children('a').addClass('ui-state-active'); + inst.hours = newHours; + + // added for onMinuteShow callback + var onMinuteShow = this._get(inst, 'onMinuteShow'), + maxTime = this._get(inst, 'maxTime'), + minTime = this._get(inst, 'minTime'); + if (onMinuteShow || maxTime.minute || minTime.minute) { + // this will trigger a callback on selected hour to make sure selected minute is allowed. + this._updateMinuteDisplay(inst); + } + + this._updateSelectedValue(inst); + + inst._hoursClicked = true; + if ((inst._minutesClicked) || (fromDoubleClick) || (showMinutes == false)) { + $.timepicker._hideTimepicker(); + } + // return false because if used inline, prevent the url to change to a hashtag + return false; + }, + + selectMinutes: function (event) { + var $td = $(event.currentTarget), + id = $td.attr("data-timepicker-instance-id"), + newMinutes = parseInt($td.attr("data-minute")), + fromDoubleClick = event.data.fromDoubleClick, + $target = $(id), + inst = this._getInst($target[0]), + showHours = (this._get(inst, 'showHours') == true); + + // don't select if disabled + if ( $.timepicker._isDisabledTimepicker($target.attr('id')) ) { return false } + + $td.parents('.ui-timepicker-minutes:first').find('a').removeClass('ui-state-active'); + $td.children('a').addClass('ui-state-active'); + + inst.minutes = newMinutes; + this._updateSelectedValue(inst); + + inst._minutesClicked = true; + if ((inst._hoursClicked) || (fromDoubleClick) || (showHours == false)) { + $.timepicker._hideTimepicker(); + // return false because if used inline, prevent the url to change to a hashtag + return false; + } + + // return false because if used inline, prevent the url to change to a hashtag + return false; + }, + + _updateSelectedValue: function (inst) { + var newTime = this._getParsedTime(inst); + if (inst.input) { + inst.input.val(newTime); + inst.input.trigger('change'); + } + var onSelect = this._get(inst, 'onSelect'); + if (onSelect) { onSelect.apply((inst.input ? inst.input[0] : null), [newTime, inst]); } // trigger custom callback + this._updateAlternate(inst, newTime); + return newTime; + }, + + /* this function process selected time and return it parsed according to instance options */ + _getParsedTime: function(inst) { + + if (inst.hours == -1 && inst.minutes == -1) { + return ''; + } + + // default to 0 AM if hours is not valid + if ((inst.hours < inst.hours.starts) || (inst.hours > inst.hours.ends )) { inst.hours = 0; } + // default to 0 minutes if minute is not valid + if ((inst.minutes < inst.minutes.starts) || (inst.minutes > inst.minutes.ends)) { inst.minutes = 0; } + + var period = "", + showPeriod = (this._get(inst, 'showPeriod') == true), + showLeadingZero = (this._get(inst, 'showLeadingZero') == true), + showHours = (this._get(inst, 'showHours') == true), + showMinutes = (this._get(inst, 'showMinutes') == true), + optionalMinutes = (this._get(inst, 'optionalMinutes') == true), + amPmText = this._get(inst, 'amPmText'), + selectedHours = inst.hours ? inst.hours : 0, + selectedMinutes = inst.minutes ? inst.minutes : 0, + displayHours = selectedHours ? selectedHours : 0, + parsedTime = ''; + + // fix some display problem when hours or minutes are not selected yet + if (displayHours == -1) { displayHours = 0 } + if (selectedMinutes == -1) { selectedMinutes = 0 } + + if (showPeriod) { + if (inst.hours == 0) { + displayHours = 12; + } + if (inst.hours < 12) { + period = amPmText[0]; + } + else { + period = amPmText[1]; + if (displayHours > 12) { + displayHours -= 12; + } + } + } + + var h = displayHours.toString(); + if (showLeadingZero && (displayHours < 10)) { h = '0' + h; } + + var m = selectedMinutes.toString(); + if (selectedMinutes < 10) { m = '0' + m; } + + if (showHours) { + parsedTime += h; + } + if (showHours && showMinutes && (!optionalMinutes || m != 0)) { + parsedTime += this._get(inst, 'timeSeparator'); + } + if (showMinutes && (!optionalMinutes || m != 0)) { + parsedTime += m; + } + if (showHours) { + if (period.length > 0) { parsedTime += this._get(inst, 'periodSeparator') + period; } + } + + return parsedTime; + }, + + /* Update any alternate field to synchronise with the main field. */ + _updateAlternate: function(inst, newTime) { + var altField = this._get(inst, 'altField'); + if (altField) { // update alternate field too + $(altField).each(function(i,e) { + $(e).val(newTime); + }); + } + }, + + _getTimeAsDateTimepicker: function(input) { + var inst = this._getInst(input); + if (inst.hours == -1 && inst.minutes == -1) { + return ''; + } + + // default to 0 AM if hours is not valid + if ((inst.hours < inst.hours.starts) || (inst.hours > inst.hours.ends )) { inst.hours = 0; } + // default to 0 minutes if minute is not valid + if ((inst.minutes < inst.minutes.starts) || (inst.minutes > inst.minutes.ends)) { inst.minutes = 0; } + + return new Date(0, 0, 0, inst.hours, inst.minutes, 0); + }, + /* This might look unused but it's called by the $.fn.timepicker function with param getTime */ + /* added v 0.2.3 - gitHub issue #5 - Thanks edanuff */ + _getTimeTimepicker : function(input) { + var inst = this._getInst(input); + return this._getParsedTime(inst); + }, + _getHourTimepicker: function(input) { + var inst = this._getInst(input); + if ( inst == undefined) { return -1; } + return inst.hours; + }, + _getMinuteTimepicker: function(input) { + var inst= this._getInst(input); + if ( inst == undefined) { return -1; } + return inst.minutes; + } + + }); + + + + /* Invoke the timepicker functionality. + @param options string - a command, optionally followed by additional parameters or + Object - settings for attaching new timepicker functionality + @return jQuery object */ + $.fn.timepicker = function (options) { + /* Initialise the time picker. */ + if (!$.timepicker.initialized) { + $(document).mousedown($.timepicker._checkExternalClick); + $.timepicker.initialized = true; + } + + /* Append timepicker main container to body if not exist. */ + if ($("#"+$.timepicker._mainDivId).length === 0) { + $('body').append($.timepicker.tpDiv); + } + + var otherArgs = Array.prototype.slice.call(arguments, 1); + if (typeof options == 'string' && (options == 'getTime' || options == 'getTimeAsDate' || options == 'getHour' || options == 'getMinute' )) + return $.timepicker['_' + options + 'Timepicker']. + apply($.timepicker, [this[0]].concat(otherArgs)); + if (options == 'option' && arguments.length == 2 && typeof arguments[1] == 'string') + return $.timepicker['_' + options + 'Timepicker']. + apply($.timepicker, [this[0]].concat(otherArgs)); + return this.each(function () { + typeof options == 'string' ? + $.timepicker['_' + options + 'Timepicker']. + apply($.timepicker, [this].concat(otherArgs)) : + $.timepicker._attachTimepicker(this, options); + }); + }; + + /* jQuery extend now ignores nulls! */ + function extendRemove(target, props) { + $.extend(target, props); + for (var name in props) + if (props[name] == null || props[name] == undefined) + target[name] = props[name]; + return target; + }; + + $.timepicker = new Timepicker(); // singleton instance + $.timepicker.initialized = false; + $.timepicker.uuid = new Date().getTime(); + $.timepicker.version = "0.3.3"; + + // Workaround for #4055 + // Add another global to avoid noConflict issues with inline event handlers + window['TP_jQuery_' + tpuuid] = $; + +})(jQuery); diff --git a/tailbone/static/js/tailbone.js b/tailbone/static/js/tailbone.js index fb84dd4b..6613c172 100644 --- a/tailbone/static/js/tailbone.js +++ b/tailbone/static/js/tailbone.js @@ -112,6 +112,13 @@ $(function() { $('input[type=submit]').button(); $('input[type=reset]').button(); + /* + * Apply timepicker behavior to text inputs which are marked for it. + */ + $('input[type=text].timepicker').timepicker({ + showPeriod: true + }); + /* * When filter labels are clicked, (un)check the associated checkbox. */ diff --git a/tailbone/templates/base.mako b/tailbone/templates/base.mako index f7a9ae47..3dcd5621 100644 --- a/tailbone/templates/base.mako +++ b/tailbone/templates/base.mako @@ -131,6 +131,7 @@ ${h.javascript_link('https://code.jquery.com/ui/1.11.4/jquery-ui.min.js')} ${h.javascript_link(request.static_url('tailbone:static/js/lib/jquery.ui.menubar.js'))} ${h.javascript_link(request.static_url('tailbone:static/js/lib/jquery.loadmask.min.js'))} + ${h.javascript_link(request.static_url('tailbone:static/js/lib/jquery.ui.timepicker.js'))} ${h.javascript_link(request.static_url('tailbone:static/js/tailbone.js'))} @@ -139,6 +140,7 @@ ${self.jquery_theme()} ${h.stylesheet_link(request.static_url('tailbone:static/css/jquery.ui.menubar.css'))} ${h.stylesheet_link(request.static_url('tailbone:static/css/jquery.loadmask.css'))} + ${h.stylesheet_link(request.static_url('tailbone:static/css/jquery.ui.timepicker.css'))} ${h.stylesheet_link(request.static_url('tailbone:static/css/jquery.ui.tailbone.css'))} ${h.stylesheet_link(request.static_url('tailbone:static/css/base.css'))} ${h.stylesheet_link(request.static_url('tailbone:static/css/layout.css'))}