diff --git a/web_widget_timepicker/README.rst b/web_widget_timepicker/README.rst new file mode 100644 index 00000000..fe0009b3 --- /dev/null +++ b/web_widget_timepicker/README.rst @@ -0,0 +1,72 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + + +=============================== +Timepicker widget in form views +=============================== + +This module provides a timepicker widget for float fields. +It can be used as a replacement for the standard float_time widget in form views. + + +|picker| + + +The widget has the following default timepicker options: + +* the possible selection is based on 15 minute interval (step: 15) +* 24 hour mode in H:i format (timeFormat: 'H:i') +* scroll selection starts at current time (scrollDefault: 'now') + + +|formview| + + +Usage +===== + +In the form view declaration, put widget='timepicker' attribute in the field tag:: + + ... + +
+ ... + + + ... + + + ... + +Additional jquery-timepicker plugin options can be specified by an options attribute:: + + ... + + ... + +See the available options at `jquery-timepicker `_. + +.. |picker| image:: ./images/picker.png +.. |formview| image:: ./images/form_view.png + + +Known issues / Roadmap +====================== + +* No validation on options. + + +Credits +======= + +* The module uses the `jquery-timepicker `_ plugin by Jon Thornton. This software is made available under the open source MIT License. © 2014 Jon Thornton and contributors + +* Odoo Community Association (OCA) + + +Contributors +------------ + +* Michael Fried diff --git a/web_widget_timepicker/__init__.py b/web_widget_timepicker/__init__.py new file mode 100644 index 00000000..2977e4d6 --- /dev/null +++ b/web_widget_timepicker/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). diff --git a/web_widget_timepicker/__openerp__.py b/web_widget_timepicker/__openerp__.py new file mode 100644 index 00000000..634dd98a --- /dev/null +++ b/web_widget_timepicker/__openerp__.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# © 2016 Vividlab () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +{ + "name": "Web Timepicker Widget", + "version": "9.0.1.0.0", + "author": "VividLab, Odoo Community Association (OCA)", + "license": "AGPL-3", + "category": "Web", + "website": "http://www.vividlab.de", + "installable": True, + "depends": [ + "web", + ], + "css": [ + "static/src/lib/jquery.timerpicker/jquery.timepicker.css", + "static/src/css/web_widget_timepicker.css", + ], + "js": [ + "static/src/lib/jquery.timerpicker/jquery.timepicker.js", + "static/src/js/web_widget_timepicker.js", + ], + "data": [ + "views/web_widget_timepicker_assets.xml", + ], + "qweb": [ + "static/src/xml/web_widget_timepicker.xml", + ] +} diff --git a/web_widget_timepicker/images/form_view.png b/web_widget_timepicker/images/form_view.png new file mode 100644 index 00000000..1feae7a1 Binary files /dev/null and b/web_widget_timepicker/images/form_view.png differ diff --git a/web_widget_timepicker/images/picker.png b/web_widget_timepicker/images/picker.png new file mode 100644 index 00000000..1dda86ee Binary files /dev/null and b/web_widget_timepicker/images/picker.png differ diff --git a/web_widget_timepicker/static/description/icon.png b/web_widget_timepicker/static/description/icon.png new file mode 100644 index 00000000..91216607 Binary files /dev/null and b/web_widget_timepicker/static/description/icon.png differ diff --git a/web_widget_timepicker/static/src/css/web_widget_timepicker.css b/web_widget_timepicker/static/src/css/web_widget_timepicker.css new file mode 100644 index 00000000..c66e9cd0 --- /dev/null +++ b/web_widget_timepicker/static/src/css/web_widget_timepicker.css @@ -0,0 +1,3 @@ +.oe_form_editable .oe_form .oe_form_field_time input { + width: 6em; +} diff --git a/web_widget_timepicker/static/src/js/web_widget_timepicker.js b/web_widget_timepicker/static/src/js/web_widget_timepicker.js new file mode 100644 index 00000000..b56a8cf4 --- /dev/null +++ b/web_widget_timepicker/static/src/js/web_widget_timepicker.js @@ -0,0 +1,89 @@ +odoo.define('web_widget_timepicker', function (require) { + "use strict"; + + var core = require('web.core'); + var formats = require('web.formats'); + var common = require('web.form_common'); + + var TimePickerField = common.AbstractField.extend(common.ReinitializeFieldMixin, { + is_field_number: true, + template: "TimePickerField", + internal_format: 'float_time', + widget_class: 'oe_form_field_time', + events: { + 'change input': 'store_dom_value', + }, + init: function (field_manager, node) { + this._super(field_manager, node); + + this.internal_set_value(0); + + this.options = _.defaults( this.options, { + step: 15, + selectOnBlur: true, + timeFormat: 'H:i', + scrollDefault: 'now', + }); + }, + initialize_content: function() { + if(!this.get("effective_readonly")) { + this.$el.find('input').timepicker(this.options); + this.setupFocus(this.$('input')); + } + }, + is_syntax_valid: function() { + if (!this.get("effective_readonly") && this.$("input").size() > 0) { + try { + this.parse_value(this.$('input').val(),''); + return true; + } catch(e) { + return false; + } + } + return true; + }, + is_false: function() { + return this.get('value') === '' || this._super(); + }, + focus: function() { + var input = this.$('input:first')[0]; + return input ? input.focus() : false; + }, + set_dimensions: function (height, width) { + this._super(height, width); + this.$('input').css({ + height: height, + width: width + }); + }, + store_dom_value: function () { + if (!this.get('effective_readonly')) { + this.internal_set_value( + this.parse_value( + this.$('input').val(),'')); + } + }, + parse_value: function(val, def) { + return formats.parse_value(val, {"widget": this.internal_format}, def); + }, + format_value: function(val, def) { + return formats.format_value(val, {"widget": this.internal_format}, def); + }, + render_value: function() { + var show_value = this.format_value(this.get('value'),''); + + if (!this.get("effective_readonly")) { + this.$input = this.$el.find('input'); + this.$input.val(show_value); + } else { + this.$(".oe_form_time_content").text(show_value); + } + }, + }); + + core.form_widget_registry.add('timepicker', TimePickerField); + + return { + TimePickerField: TimePickerField, + }; +}); diff --git a/web_widget_timepicker/static/src/lib/jquery.timepicker/jquery.timepicker.css b/web_widget_timepicker/static/src/lib/jquery.timepicker/jquery.timepicker.css new file mode 100644 index 00000000..cd75f13f --- /dev/null +++ b/web_widget_timepicker/static/src/lib/jquery.timepicker/jquery.timepicker.css @@ -0,0 +1,72 @@ +.ui-timepicker-wrapper { + overflow-y: auto; + height: 150px; + width: 6.5em; + background: #fff; + border: 1px solid #ddd; + -webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2); + -moz-box-shadow:0 5px 10px rgba(0,0,0,0.2); + box-shadow:0 5px 10px rgba(0,0,0,0.2); + outline: none; + z-index: 10001; + margin: 0; +} + +.ui-timepicker-wrapper.ui-timepicker-with-duration { + width: 13em; +} + +.ui-timepicker-wrapper.ui-timepicker-with-duration.ui-timepicker-step-30, +.ui-timepicker-wrapper.ui-timepicker-with-duration.ui-timepicker-step-60 { + width: 11em; +} + +.ui-timepicker-list { + margin: 0; + padding: 0; + list-style: none; +} + +.ui-timepicker-duration { + margin-left: 5px; color: #888; +} + +.ui-timepicker-list:hover .ui-timepicker-duration { + color: #888; +} + +.ui-timepicker-list li { + padding: 3px 0 3px 5px; + cursor: pointer; + white-space: nowrap; + color: #000; + list-style: none; + margin: 0; +} + +.ui-timepicker-list:hover .ui-timepicker-selected { + background: #fff; color: #000; +} + +li.ui-timepicker-selected, +.ui-timepicker-list li:hover, +.ui-timepicker-list .ui-timepicker-selected:hover { + background: #1980EC; color: #fff; +} + +li.ui-timepicker-selected .ui-timepicker-duration, +.ui-timepicker-list li:hover .ui-timepicker-duration { + color: #ccc; +} + +.ui-timepicker-list li.ui-timepicker-disabled, +.ui-timepicker-list li.ui-timepicker-disabled:hover, +.ui-timepicker-list li.ui-timepicker-selected.ui-timepicker-disabled { + color: #888; + cursor: default; +} + +.ui-timepicker-list li.ui-timepicker-disabled:hover, +.ui-timepicker-list li.ui-timepicker-selected.ui-timepicker-disabled { + background: #f2f2f2; +} diff --git a/web_widget_timepicker/static/src/lib/jquery.timepicker/jquery.timepicker.js b/web_widget_timepicker/static/src/lib/jquery.timepicker/jquery.timepicker.js new file mode 100644 index 00000000..d6cb947c --- /dev/null +++ b/web_widget_timepicker/static/src/lib/jquery.timepicker/jquery.timepicker.js @@ -0,0 +1,1225 @@ +/*! + * jquery-timepicker v1.10.1 - A jQuery timepicker plugin inspired by Google Calendar. It supports both mouse and keyboard navigation. + * Copyright (c) 2015 Jon Thornton - http://jonthornton.github.com/jquery-timepicker/ + * License: MIT + */ + + +(function (factory) { + if (typeof exports === "object" && exports && + typeof module === "object" && module && module.exports === exports) { + // Browserify. Attach to jQuery module. + factory(require("jquery")); + } else if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else { + // Browser globals + factory(jQuery); + } +}(function ($) { + var _ONE_DAY = 86400; + var _lang = { + am: 'am', + pm: 'pm', + AM: 'AM', + PM: 'PM', + decimal: '.', + mins: 'mins', + hr: 'hr', + hrs: 'hrs' + }; + + var methods = { + init: function(options) + { + return this.each(function() + { + var self = $(this); + + // pick up settings from data attributes + var attributeOptions = []; + for (var key in $.fn.timepicker.defaults) { + if (self.data(key)) { + attributeOptions[key] = self.data(key); + } + } + + var settings = $.extend({}, $.fn.timepicker.defaults, attributeOptions, options); + + if (settings.lang) { + _lang = $.extend(_lang, settings.lang); + } + + settings = _parseSettings(settings); + self.data('timepicker-settings', settings); + self.addClass('ui-timepicker-input'); + + if (settings.useSelect) { + _render(self); + } else { + self.prop('autocomplete', 'off'); + if (settings.showOn) { + for (var i in settings.showOn) { + self.on(settings.showOn[i]+'.timepicker', methods.show); + } + } + self.on('change.timepicker', _formatValue); + self.on('keydown.timepicker', _keydownhandler); + self.on('keyup.timepicker', _keyuphandler); + if (settings.disableTextInput) { + self.on('keydown.timepicker', _disableTextInputHandler); + } + + _formatValue.call(self.get(0)); + } + }); + }, + + show: function(e) + { + var self = $(this); + var settings = self.data('timepicker-settings'); + + if (e) { + e.preventDefault(); + } + + if (settings.useSelect) { + self.data('timepicker-list').focus(); + return; + } + + if (_hideKeyboard(self)) { + // block the keyboard on mobile devices + self.blur(); + } + + var list = self.data('timepicker-list'); + + // check if input is readonly + if (self.prop('readonly')) { + return; + } + + // check if list needs to be rendered + if (!list || list.length === 0 || typeof settings.durationTime === 'function') { + _render(self); + list = self.data('timepicker-list'); + } + + if (_isVisible(list)) { + return; + } + + self.data('ui-timepicker-value', self.val()); + _setSelected(self, list); + + // make sure other pickers are hidden + methods.hide(); + + // position the dropdown relative to the input + list.show(); + var listOffset = {}; + + if (settings.orientation.match(/r/)) { + // right-align the dropdown + listOffset.left = self.offset().left + self.outerWidth() - list.outerWidth() + parseInt(list.css('marginLeft').replace('px', ''), 10); + } else { + // left-align the dropdown + listOffset.left = self.offset().left + parseInt(list.css('marginLeft').replace('px', ''), 10); + } + + var verticalOrientation; + if (settings.orientation.match(/t/)) { + verticalOrientation = 't'; + } else if (settings.orientation.match(/b/)) { + verticalOrientation = 'b'; + } else if ((self.offset().top + self.outerHeight(true) + list.outerHeight()) > $(window).height() + $(window).scrollTop()) { + verticalOrientation = 't'; + } else { + verticalOrientation = 'b'; + } + + if (verticalOrientation == 't') { + // position the dropdown on top + list.addClass('ui-timepicker-positioned-top'); + listOffset.top = self.offset().top - list.outerHeight() + parseInt(list.css('marginTop').replace('px', ''), 10); + } else { + // put it under the input + list.removeClass('ui-timepicker-positioned-top'); + listOffset.top = self.offset().top + self.outerHeight() + parseInt(list.css('marginTop').replace('px', ''), 10); + } + + list.offset(listOffset); + + // position scrolling + var selected = list.find('.ui-timepicker-selected'); + + if (!selected.length) { + var timeInt = _time2int(_getTimeValue(self)); + if (timeInt !== null) { + selected = _findRow(self, list, timeInt); + } else if (settings.scrollDefault) { + selected = _findRow(self, list, settings.scrollDefault()); + } + } + + if (selected && selected.length) { + var topOffset = list.scrollTop() + selected.position().top - selected.outerHeight(); + list.scrollTop(topOffset); + } else { + list.scrollTop(0); + } + + // prevent scroll propagation + if(settings.stopScrollPropagation) { + $(document).on('wheel.ui-timepicker', '.ui-timepicker-wrapper', function(e){ + e.preventDefault(); + var currentScroll = $(this).scrollTop(); + $(this).scrollTop(currentScroll + e.originalEvent.deltaY); + }); + } + + // attach close handlers + $(document).on('touchstart.ui-timepicker mousedown.ui-timepicker', _closeHandler); + $(window).on('resize.ui-timepicker', _closeHandler); + if (settings.closeOnWindowScroll) { + $(document).on('scroll.ui-timepicker', _closeHandler); + } + + self.trigger('showTimepicker'); + + return this; + }, + + hide: function(e) + { + var self = $(this); + var settings = self.data('timepicker-settings'); + + if (settings && settings.useSelect) { + self.blur(); + } + + $('.ui-timepicker-wrapper').each(function() { + var list = $(this); + if (!_isVisible(list)) { + return; + } + + var self = list.data('timepicker-input'); + var settings = self.data('timepicker-settings'); + + if (settings && settings.selectOnBlur) { + _selectValue(self); + } + + list.hide(); + self.trigger('hideTimepicker'); + }); + + return this; + }, + + option: function(key, value) + { + if (typeof key == 'string' && typeof value == 'undefined') { + return $(this).data('timepicker-settings')[key]; + } + + return this.each(function(){ + var self = $(this); + var settings = self.data('timepicker-settings'); + var list = self.data('timepicker-list'); + + if (typeof key == 'object') { + settings = $.extend(settings, key); + } else if (typeof key == 'string') { + settings[key] = value; + } + + settings = _parseSettings(settings); + + self.data('timepicker-settings', settings); + + if (list) { + list.remove(); + self.data('timepicker-list', false); + } + + if (settings.useSelect) { + _render(self); + } + }); + }, + + getSecondsFromMidnight: function() + { + return _time2int(_getTimeValue(this)); + }, + + getTime: function(relative_date) + { + var self = this; + + var time_string = _getTimeValue(self); + if (!time_string) { + return null; + } + + var offset = _time2int(time_string); + if (offset === null) { + return null; + } + + if (!relative_date) { + relative_date = new Date(); + } + + // construct a Date from relative date, and offset's time + var time = new Date(relative_date); + time.setHours(offset / 3600); + time.setMinutes(offset % 3600 / 60); + time.setSeconds(offset % 60); + time.setMilliseconds(0); + + return time; + }, + + isVisible: function() { + var self = this; + var list = self.data('timepicker-list'); + return !!(list && _isVisible(list)); + }, + + setTime: function(value) + { + var self = this; + var settings = self.data('timepicker-settings'); + + if (settings.forceRoundTime) { + var prettyTime = _roundAndFormatTime(_time2int(value), settings) + } else { + var prettyTime = _int2time(_time2int(value), settings); + } + + if (value && prettyTime === null && settings.noneOption) { + prettyTime = value; + } + + _setTimeValue(self, prettyTime); + if (self.data('timepicker-list')) { + _setSelected(self, self.data('timepicker-list')); + } + + return this; + }, + + remove: function() + { + var self = this; + + // check if this element is a timepicker + if (!self.hasClass('ui-timepicker-input')) { + return; + } + + var settings = self.data('timepicker-settings'); + self.removeAttr('autocomplete', 'off'); + self.removeClass('ui-timepicker-input'); + self.removeData('timepicker-settings'); + self.off('.timepicker'); + + // timepicker-list won't be present unless the user has interacted with this timepicker + if (self.data('timepicker-list')) { + self.data('timepicker-list').remove(); + } + + if (settings.useSelect) { + self.show(); + } + + self.removeData('timepicker-list'); + + return this; + } + }; + + // private methods + + function _isVisible(elem) + { + var el = elem[0]; + return el.offsetWidth > 0 && el.offsetHeight > 0; + } + + function _parseSettings(settings) + { + if (settings.minTime) { + settings.minTime = _time2int(settings.minTime); + } + + if (settings.maxTime) { + settings.maxTime = _time2int(settings.maxTime); + } + + if (settings.durationTime && typeof settings.durationTime !== 'function') { + settings.durationTime = _time2int(settings.durationTime); + } + + if (settings.scrollDefault == 'now') { + settings.scrollDefault = function() { + return settings.roundingFunction(_time2int(new Date()), settings); + } + } else if (settings.scrollDefault && typeof settings.scrollDefault != 'function') { + var val = settings.scrollDefault; + settings.scrollDefault = function() { + return settings.roundingFunction(_time2int(val), settings); + } + } else if (settings.minTime) { + settings.scrollDefault = function() { + return settings.roundingFunction(settings.minTime, settings); + } + } + + if ($.type(settings.timeFormat) === "string" && settings.timeFormat.match(/[gh]/)) { + settings._twelveHourTime = true; + } + + if (settings.showOnFocus === false && settings.showOn.indexOf('focus') != -1) { + settings.showOn.splice(settings.showOn.indexOf('focus'), 1); + } + + if (settings.disableTimeRanges.length > 0) { + // convert string times to integers + for (var i in settings.disableTimeRanges) { + settings.disableTimeRanges[i] = [ + _time2int(settings.disableTimeRanges[i][0]), + _time2int(settings.disableTimeRanges[i][1]) + ]; + } + + // sort by starting time + settings.disableTimeRanges = settings.disableTimeRanges.sort(function(a, b){ + return a[0] - b[0]; + }); + + // merge any overlapping ranges + for (var i = settings.disableTimeRanges.length-1; i > 0; i--) { + if (settings.disableTimeRanges[i][0] <= settings.disableTimeRanges[i-1][1]) { + settings.disableTimeRanges[i-1] = [ + Math.min(settings.disableTimeRanges[i][0], settings.disableTimeRanges[i-1][0]), + Math.max(settings.disableTimeRanges[i][1], settings.disableTimeRanges[i-1][1]) + ]; + settings.disableTimeRanges.splice(i, 1); + } + } + } + + return settings; + } + + function _render(self) + { + var settings = self.data('timepicker-settings'); + var list = self.data('timepicker-list'); + + if (list && list.length) { + list.remove(); + self.data('timepicker-list', false); + } + + if (settings.useSelect) { + list = $(' + + + + + + + + diff --git a/web_widget_timepicker/views/web_widget_timepicker_assets.xml b/web_widget_timepicker/views/web_widget_timepicker_assets.xml new file mode 100644 index 00000000..290bf8b6 --- /dev/null +++ b/web_widget_timepicker/views/web_widget_timepicker_assets.xml @@ -0,0 +1,14 @@ + + + + + + +