Forráskód Böngészése

Added relative/absolute/since time selector. Moved refresh func out to panel

Rashid Khan 13 éve
szülő
commit
834e63a123

+ 0 - 156
common/css/datepicker.css

@@ -1,156 +0,0 @@
-/* =========================================================
- * bootstrap-datepicker.js
- * original by Stefan Petre
- * tweaked by gus
- * ========================================================= */
-
-.bs-sc-datepicker.dropdown-menu {
-max-width: inherit;
-}
-.bs-sc-datepicker {
-top: 0;
-left: 0;
-padding: 4px;
-margin-top: 1px;
--webkit-border-radius: 4px;
--moz-border-radius: 4px;
-border-radius: 4px;
-z-index: 1051;
-}
-.bs-sc-datepicker:before {
-content: '';
-display: inline-block;
-border-left: 7px solid transparent;
-border-right: 7px solid transparent;
-border-bottom: 7px solid #ccc;
-border-bottom-color: rgba(0, 0, 0, 0.2);
-position: absolute;
-top: -7px;
-left: 6px;
-}
-.bs-sc-datepicker:after {
-content: '';
-display: inline-block;
-border-left: 6px solid transparent;
-border-right: 6px solid transparent;
-border-bottom: 6px solid #ffffff;
-position: absolute;
-top: -6px;
-left: 7px;
-}
-.bs-sc-datepicker table {
-width: 100%;
-margin: 0;
-}
-.bs-sc-datepicker th {
-border-bottom: 1px solid #efefef;
-}
-.bs-sc-datepicker td, .bs-sc-datepicker th {
-text-align: center;
-width: 20px;
-height: 20px;
--webkit-border-radius: 4px;
--moz-border-radius: 4px;
-border-radius: 4px;
-padding: 5px !important;
-}
-.bs-sc-datepicker td.day:hover {
-background: #eeeeee;
-cursor: pointer;
-}
-.bs-sc-datepicker td.old, .bs-sc-datepicker td.new {
-color: #DDDDDD;
-}
-.bs-sc-datepicker td.active, .bs-sc-datepicker td.active:hover {
-background-color: #006dcc;
-background-image: -moz-linear-gradient(top, #0088cc, #0044cc);
-background-image: -ms-linear-gradient(top, #0088cc, #0044cc);
-background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));
-background-image: -webkit-linear-gradient(top, #0088cc, #0044cc);
-background-image: -o-linear-gradient(top, #0088cc, #0044cc);
-background-image: linear-gradient(top, #0088cc, #0044cc);
-background-repeat: repeat-x;
-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0);
-border-color: #0044cc #0044cc #002a80;
-border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
-filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
-color: #fff;
-text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
-}
-.bs-sc-datepicker td.active:hover,
-.bs-sc-datepicker td.active:hover:hover,
-.bs-sc-datepicker td.active:active,
-.bs-sc-datepicker td.active:hover:active,
-.bs-sc-datepicker td.active.active,
-.bs-sc-datepicker td.active:hover.active,
-.bs-sc-datepicker td.active.disabled,
-.bs-sc-datepicker td.active:hover.disabled,
-.bs-sc-datepicker td.active[disabled],
-.bs-sc-datepicker td.active:hover[disabled] {
-background-color: #0044cc;
-}
-.bs-sc-datepicker td.active:active,
-.bs-sc-datepicker td.active:hover:active,
-.bs-sc-datepicker td.active.active,
-.bs-sc-datepicker td.active:hover.active {
-background-color: #003399 \9;
-}
-.bs-sc-datepicker td span {
-display: block;
-width: 47px;
-height: 54px;
-line-height: 54px;
-float: left;
-margin: 2px;
-cursor: pointer;
--webkit-border-radius: 4px;
--moz-border-radius: 4px;
-border-radius: 4px;
-}
-.bs-sc-datepicker td span:hover {
-background: #eeeeee;
-}
-.bs-sc-datepicker td span.active {
-background-color: #006dcc;
-background-image: -moz-linear-gradient(top, #0088cc, #0044cc);
-background-image: -ms-linear-gradient(top, #0088cc, #0044cc);
-background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));
-background-image: -webkit-linear-gradient(top, #0088cc, #0044cc);
-background-image: -o-linear-gradient(top, #0088cc, #0044cc);
-background-image: linear-gradient(top, #0088cc, #0044cc);
-background-repeat: repeat-x;
-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0);
-border-color: #0044cc #0044cc #002a80;
-border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
-filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
-color: #fff;
-text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
-}
-.bs-sc-datepicker td span.active:hover,
-.bs-sc-datepicker td span.active:active,
-.bs-sc-datepicker td span.active.active,
-.bs-sc-datepicker td span.active.disabled,
-.bs-sc-datepicker td span.active[disabled] {
-background-color: #0044cc;
-}
-.bs-sc-datepicker td span.active:active, .bs-sc-datepicker td span.active.active {
-background-color: #003399 \9;
-}
-.bs-sc-datepicker td span.old {
-color: #999999;
-}
-.bs-sc-datepicker th.switch {
-width: 145px;
-}
-.bs-sc-datepicker thead tr:first-child th {
-cursor: pointer;
-}
-.bs-sc-datepicker thead tr:first-child th:hover {
-background: #eeeeee;
-}
-.input-append.date .add-on i, .input-prepend.date .add-on i {
-display: block;
-cursor: pointer;
-width: 16px;
-height: 16px;
-}

+ 29 - 0
common/css/main.css

@@ -22,4 +22,33 @@
 
 .pointer {
   cursor: pointer;
+}
+
+.small {
+  font-size: 85%;
+}
+
+.nomargin {
+  margin: 0px;
+}
+
+.strong {
+  font-weight: bold;
+}
+
+.btn-active {
+  background-color: #E6E6E6;
+  background-image: none;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15) inset, 0 1px 2px rgba(0, 0, 0, 0.05);
+  outline: 0 none
+}
+
+.popover-title { display: none; }
+
+.input-smaller {
+  width: 75px;
+}
+
+.tiny {
+  font-size: 50%;
 }

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 7 - 0
common/css/timepicker.css


+ 688 - 221
common/lib/datepicker.js

@@ -1,188 +1,377 @@
 /* =========================================================
  * bootstrap-datepicker.js
- * original by Stefan Petre
- * tweaked by gus
+ * http://www.eyecon.ro/bootstrap-datepicker
+ * =========================================================
+ * Copyright 2012 Stefan Petre
+ * Improvements by Andrew Rowls
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
  * ========================================================= */
 
 !function( $ ) {
 
+	function UTCDate(){
+		return new Date(Date.UTC.apply(Date, arguments));
+	}
+	function UTCToday(){
+		var today = new Date();
+		return UTCDate(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate());
+	}
+
 	// Picker object
 
-	var Datepicker = function(element, options){
-		this.element = $(element);
+	var Datepicker = function(element, options) {
+		var that = this;
 
-		this.days = options.days||["sun","mon","tue","wed","thu","fri","sat"];
-		this.months = options.months||["january","february","march","april","may","june","july","august","september","october","november","december"];
-		this.format = options.format||$(element).data("datepicker-format")||'mm/dd/yyyy hh:ii:ss';
-		this.noDefault = options.noDefault||$(element).data("datepicker-nodefault")||false;
+		this.element = $(element);
+		this.language = options.language||this.element.data('date-language')||"en";
+		this.language = this.language in dates ? this.language : "en";
+		this.format = DPGlobal.parseFormat(options.format||this.element.data('date-format')||'mm/dd/yyyy');
+		this.picker = $(DPGlobal.template)
+							.appendTo('body')
+							.on({
+								click: $.proxy(this.click, this)
+							});
+		this.isInput = this.element.is('input');
+		this.component = this.element.is('.date') ? this.element.find('.add-on') : false;
+		this.hasInput = this.component && this.element.find('input').length;
+		if(this.component && this.component.length === 0)
+			this.component = false;
+
+		if (this.isInput) {
+			this.element.on({
+				focus: $.proxy(this.show, this),
+				keyup: $.proxy(this.update, this),
+				keydown: $.proxy(this.keydown, this)
+			});
+		} else {
+			if (this.component && this.hasInput){
+				// For components that are not readonly, allow keyboard nav
+				this.element.find('input').on({
+					focus: $.proxy(this.show, this),
+					keyup: $.proxy(this.update, this),
+					keydown: $.proxy(this.keydown, this)
+				});
+
+				this.component.on('click', $.proxy(this.show, this));
+			} else {
+				this.element.on('click', $.proxy(this.show, this));
+			}
+		}
 
-		this.picker = $(DPGlobal.template).appendTo("body").on({
-			mousedown: $.proxy(this.click, this)
+		$(document).on('mousedown', function (e) {
+			// Clicked outside the datepicker, hide it
+			if ($(e.target).closest('.datepicker').length == 0) {
+				that.hide();
+			}
 		});
 
-		this.weekStart = options.weekStart||0;
-		this.weekEnd = this.weekStart == 0 ? 6 : this.weekStart - 1;
-		this.head();
+		this.autoclose = false;
+		if ('autoclose' in options) {
+			this.autoclose = options.autoclose;
+		} else if ('dateAutoclose' in this.element.data()) {
+			this.autoclose = this.element.data('date-autoclose');
+		}
 
-		if (!this.element.prop("value")&&!this.noDefault) {
-			this.element.prop("value",DPGlobal.formatDate(new Date(), this.format));
+		this.keyboardNavigation = true;
+		if ('keyboardNavigation' in options) {
+			this.keyboardNavigation = options.keyboardNavigation;
+		} else if ('dateKeyboardNavigation' in this.element.data()) {
+			this.keyboardNavigation = this.element.data('date-keyboard-navigation');
 		}
 
-		this.update();
+		switch(options.startView || this.element.data('date-start-view')){
+			case 2:
+			case 'decade':
+				this.viewMode = this.startViewMode = 2;
+				break;
+			case 1:
+			case 'year':
+				this.viewMode = this.startViewMode = 1;
+				break;
+			case 0:
+			case 'month':
+			default:
+				this.viewMode = this.startViewMode = 0;
+				break;
+		}
 
-		this.element.on({
-			focus: $.proxy(this.show, this),
-			click: $.proxy(this.show, this),
-			keyup: $.proxy(this.keyup, this)
-		});
+		this.todayBtn = (options.todayBtn||this.element.data('date-today-btn')||false);
+		this.todayHighlight = (options.todayHighlight||this.element.data('date-today-highlight')||false);
+
+		this.weekStart = ((options.weekStart||this.element.data('date-weekstart')||dates[this.language].weekStart||0) % 7);
+		this.weekEnd = ((this.weekStart + 6) % 7);
+		this.startDate = -Infinity;
+		this.endDate = Infinity;
+		this.setStartDate(options.startDate||this.element.data('date-startdate'));
+		this.setEndDate(options.endDate||this.element.data('date-enddate'));
+		this.fillDow();
+		this.fillMonths();
+		this.update();
+		this.showMode();
 	};
 
 	Datepicker.prototype = {
 		constructor: Datepicker,
 
 		show: function(e) {
-			this.update();
 			this.picker.show();
-			this.height = this.element.outerHeight();
+			this.height = this.component ? this.component.outerHeight() : this.element.outerHeight();
+			this.update();
 			this.place();
-			$(window).on("resize", $.proxy(this.place, this));
-			if (e) {
+			$(window).on('resize', $.proxy(this.place, this));
+			if (e ) {
 				e.stopPropagation();
 				e.preventDefault();
 			}
 			this.element.trigger({
-				type: "show",
+				type: 'show',
 				date: this.date
 			});
-			$("body").on("click.bs-sc-datepicker", $.proxy(this.hide, this));
 		},
 
 		hide: function(e){
-			if (e && $(e.target).parents(".bs-sc-datepicker").length) return false;
 			this.picker.hide();
-			$(window).off("resize", this.place);
-			$("body").off("click.bs-sc-datepicker");
+			$(window).off('resize', this.place);
+			this.viewMode = this.startViewMode;
+			this.showMode();
+			if (!this.isInput) {
+				$(document).off('mousedown', this.hide);
+			}
+			if (e && e.currentTarget.value)
+				this.setValue();
+			this.element.trigger({
+				type: 'hide',
+				date: this.date
+			});
+		},
+
+		getDate: function() {
+			var d = this.getUTCDate();
+			return new Date(d.getTime() + (d.getTimezoneOffset()*60000))
+		},
+
+		getUTCDate: function() {
+			return this.date;
 		},
 
-		setValue: function(val) {
-			if (typeof(val)!=='undefined') {
-				this.date = val;
+		setDate: function(d) {
+			this.setUTCDate(new Date(d.getTime() - (d.getTimezoneOffset()*60000)));
+		},
+
+		setUTCDate: function(d) {
+			this.date = d;
+			this.setValue();
+		},
+
+		setValue: function() {
+			var formatted = DPGlobal.formatDate(this.date, this.format, this.language);
+			if (!this.isInput) {
+				if (this.component){
+					this.element.find('input').prop('value', formatted);
+				}
+				this.element.data('date', formatted);
+			} else {
+				this.element.prop('value', formatted);
 			}
-			var formated = DPGlobal.formatDate(this.date, this.format);
-			this.element.prop("value", formated);
+		},
+
+		setStartDate: function(startDate){
+			this.startDate = startDate||-Infinity;
+			if (this.startDate !== -Infinity) {
+				this.startDate = DPGlobal.parseDate(this.startDate, this.format, this.language);
+			}
+			this.update();
+			this.updateNavArrows();
+		},
+
+		setEndDate: function(endDate){
+			this.endDate = endDate||Infinity;
+			if (this.endDate !== Infinity) {
+				this.endDate = DPGlobal.parseDate(this.endDate, this.format, this.language);
+			}
+			this.update();
+			this.updateNavArrows();
 		},
 
 		place: function(){
-			var offset = this.element.offset();
+			var zIndex = parseInt(this.element.parents().filter(function() {
+							return $(this).css('z-index') != 'auto';
+						}).first().css('z-index'))+10;
+			var offset = this.component ? this.component.offset() : this.element.offset();
 			this.picker.css({
 				top: offset.top + this.height,
-				left: offset.left
+				left: offset.left,
+				zIndex: zIndex
 			});
 		},
 
 		update: function(){
-			this.date = DPGlobal.parseDate(this.element.prop("value"), this.format);
-			this.viewDate = new Date(this.date);
+			this.date = DPGlobal.parseDate(
+				this.isInput ? this.element.prop('value') : this.element.data('date') || this.element.find('input').prop('value'),
+				this.format, this.language
+			);
+			if (this.date < this.startDate) {
+				this.viewDate = new Date(this.startDate);
+			} else if (this.date > this.endDate) {
+				this.viewDate = new Date(this.endDate);
+			} else {
+				this.viewDate = new Date(this.date);
+			}
 			this.fill();
 		},
 
-		keyup: function() {
-			this.date = DPGlobal.parseDate(this.element.prop("value"), this.format);
-			this.element.trigger({
-				type: 'changeDate',
-				date: this.date
-			});
-		},
-
-		head: function(){
+		fillDow: function(){
 			var dowCnt = this.weekStart;
 			var html = '<tr>';
 			while (dowCnt < this.weekStart + 7) {
-				html += '<th class="dow">'+this.days[(dowCnt++)%7]+'</th>';
+				html += '<th class="dow">'+dates[this.language].daysMin[(dowCnt++)%7]+'</th>';
 			}
 			html += '</tr>';
-			this.picker.find(".datepicker-days thead").append(html);
+			this.picker.find('.datepicker-days thead').append(html);
 		},
 
-		fill: function() {
-			var d = new Date(this.viewDate),
-				year = d.getFullYear(),
-				month = d.getMonth(),
-				day = d.getDay();
-
-			currentDate = new Date(this.date.getFullYear(), this.date.getMonth(), this.date.getDate(), 0, 0, 0, 0);
-			currentDate = currentDate.valueOf();
-
-			if (month > 0) {
-				var prevMonth = new Date(year, month-1, 1,0,0,0,0);
-				var prevMonthNr = prevMonth.getMonth();
-			} else {
-				var prevMonth = new Date(year-1, 11, 1, 0, 0, 0, 0);
-				var prevMonthNr = prevMonth.getMonth();
+		fillMonths: function(){
+			var html = '';
+			var i = 0
+			while (i < 12) {
+				html += '<span class="month">'+dates[this.language].monthsShort[i++]+'</span>';
 			}
+			this.picker.find('.datepicker-months td').html(html);
+		},
 
-			if (month < 11) {
-				var nextMonthNr = month + 1;
-			} else {
-				var nextMonthNr = 0;
-			}
-
-			var beginMonth = new Date(year, month, 1,0,0,0,0);
-			startAtWeekday = beginMonth.getDay() - this.weekStart;
-			if (startAtWeekday < 0) {
-				prevMonthDays = DPGlobal.getDaysInMonth(prevMonth.getFullYear(), prevMonth.getMonth());
-				startPrevMonthAtDate = prevMonthDays - (6 + startAtWeekday);
-			} else if (startAtWeekday > 0) {
-				prevMonthDays = DPGlobal.getDaysInMonth(prevMonth.getFullYear(), prevMonth.getMonth());
-				startPrevMonthAtDate = prevMonthDays - startAtWeekday + 1;
-			} else {
-				startPrevMonthAtDate = 1;
-				prevMonth.setMonth(month);
-			}
-
-			prevMonth.setDate(startPrevMonthAtDate);
-
-			d = prevMonth;
-
-			html = []; allDone = false; x=0;
-
-			while(!allDone) {
-
-				if (d.getDay() == this.weekStart) {
-
+		fill: function() {
+			var d = new Date(this.viewDate),
+				year = d.getUTCFullYear(),
+				month = d.getUTCMonth(),
+				startYear = this.startDate !== -Infinity ? this.startDate.getUTCFullYear() : -Infinity,
+				startMonth = this.startDate !== -Infinity ? this.startDate.getUTCMonth() : -Infinity,
+				endYear = this.endDate !== Infinity ? this.endDate.getUTCFullYear() : Infinity,
+				endMonth = this.endDate !== Infinity ? this.endDate.getUTCMonth() : Infinity,
+				currentDate = this.date.valueOf(),
+				today = new Date();
+			this.picker.find('.datepicker-days thead th:eq(1)')
+						.text(dates[this.language].months[month]+' '+year);
+			this.picker.find('tfoot th.today')
+						.text(dates[this.language].today)
+						.toggle(this.todayBtn);
+			this.updateNavArrows();
+			this.fillMonths();
+			var prevMonth = UTCDate(year, month-1, 28,0,0,0,0),
+				day = DPGlobal.getDaysInMonth(prevMonth.getUTCFullYear(), prevMonth.getUTCMonth());
+			prevMonth.setUTCDate(day);
+			prevMonth.setUTCDate(day - (prevMonth.getUTCDay() - this.weekStart + 7)%7);
+			var nextMonth = new Date(prevMonth);
+			nextMonth.setUTCDate(nextMonth.getUTCDate() + 42);
+			nextMonth = nextMonth.valueOf();
+			var html = [];
+			var clsName;
+			while(prevMonth.valueOf() < nextMonth) {
+				if (prevMonth.getUTCDay() == this.weekStart) {
 					html.push('<tr>');
 				}
-
 				clsName = '';
-				if (d.getMonth() == prevMonthNr) {
+				if (prevMonth.getUTCFullYear() < year || (prevMonth.getUTCFullYear() == year && prevMonth.getUTCMonth() < month)) {
 					clsName += ' old';
-				} else if (d.getMonth() == nextMonthNr) {
+				} else if (prevMonth.getUTCFullYear() > year || (prevMonth.getUTCFullYear() == year && prevMonth.getUTCMonth() > month)) {
 					clsName += ' new';
 				}
-				if (d.valueOf() == currentDate) {
+				// Compare internal UTC date with local today, not UTC today
+				if (this.todayHighlight &&
+					prevMonth.getUTCFullYear() == today.getFullYear() &&
+					prevMonth.getUTCMonth() == today.getMonth() &&
+					prevMonth.getUTCDate() == today.getDate()) {
+					clsName += ' today';
+				}
+				if (prevMonth.valueOf() == currentDate) {
 					clsName += ' active';
 				}
-				clsName += ' ' + d.valueOf();
-				html.push('<td class="day'+clsName+'">' + d.getDate() + '</td>');
-
-				if (d.getDay() == this.weekEnd) {
-					html.push('</tr>');
+				if (prevMonth.valueOf() < this.startDate || prevMonth.valueOf() > this.endDate) {
+					clsName += ' disabled';
 				}
-
-				d.setDate(d.getDate()+1);
-				allDone = ((d.getDay() == this.weekStart) && (d.getMonth() == nextMonthNr));
-
-				x++;
-				if (x > 99) {
-					console.log("safety");
-					return;
+				html.push('<td class="day'+clsName+'">'+prevMonth.getUTCDate() + '</td>');
+				if (prevMonth.getUTCDay() == this.weekEnd) {
+					html.push('</tr>');
 				}
+				prevMonth.setUTCDate(prevMonth.getUTCDate()+1);
 			}
-
 			this.picker.find('.datepicker-days tbody').empty().append(html.join(''));
+			var currentYear = this.date.getUTCFullYear();
+
+			var months = this.picker.find('.datepicker-months')
+						.find('th:eq(1)')
+							.text(year)
+							.end()
+						.find('span').removeClass('active');
+			if (currentYear == year) {
+				months.eq(this.date.getUTCMonth()).addClass('active');
+			}
+			if (year < startYear || year > endYear) {
+				months.addClass('disabled');
+			}
+			if (year == startYear) {
+				months.slice(0, startMonth).addClass('disabled');
+			}
+			if (year == endYear) {
+				months.slice(endMonth+1).addClass('disabled');
+			}
+
+			html = '';
+			year = parseInt(year/10, 10) * 10;
+			var yearCont = this.picker.find('.datepicker-years')
+								.find('th:eq(1)')
+									.text(year + '-' + (year + 9))
+									.end()
+								.find('td');
+			year -= 1;
+			for (var i = -1; i < 11; i++) {
+				html += '<span class="year'+(i == -1 || i == 10 ? ' old' : '')+(currentYear == year ? ' active' : '')+(year < startYear || year > endYear ? ' disabled' : '')+'">'+year+'</span>';
+				year += 1;
+			}
+			yearCont.html(html);
+		},
 
-			headerStr = this.months[this.viewDate.getMonth()] + ' ' + this.viewDate.getFullYear();
-			this.picker.find('.datepicker-days thead .monthname').html(headerStr);
+		updateNavArrows: function() {
+			var d = new Date(this.viewDate),
+				year = d.getUTCFullYear(),
+				month = d.getUTCMonth();
+			switch (this.viewMode) {
+				case 0:
+					if (this.startDate !== -Infinity && year <= this.startDate.getUTCFullYear() && month <= this.startDate.getUTCMonth()) {
+						this.picker.find('.prev').css({visibility: 'hidden'});
+					} else {
+						this.picker.find('.prev').css({visibility: 'visible'});
+					}
+					if (this.endDate !== Infinity && year >= this.endDate.getUTCFullYear() && month >= this.endDate.getUTCMonth()) {
+						this.picker.find('.next').css({visibility: 'hidden'});
+					} else {
+						this.picker.find('.next').css({visibility: 'visible'});
+					}
+					break;
+				case 1:
+				case 2:
+					if (this.startDate !== -Infinity && year <= this.startDate.getUTCFullYear()) {
+						this.picker.find('.prev').css({visibility: 'hidden'});
+					} else {
+						this.picker.find('.prev').css({visibility: 'visible'});
+					}
+					if (this.endDate !== Infinity && year >= this.endDate.getUTCFullYear()) {
+						this.picker.find('.next').css({visibility: 'hidden'});
+					} else {
+						this.picker.find('.next').css({visibility: 'visible'});
+					}
+					break;
+			}
 		},
 
 		click: function(e) {
@@ -190,62 +379,262 @@
 			e.preventDefault();
 			var target = $(e.target).closest('span, td, th');
 			if (target.length == 1) {
-
 				switch(target[0].nodeName.toLowerCase()) {
-
 					case 'th':
 						switch(target[0].className) {
-							case 'prev':
-								if (this.viewDate.getMonth() > 0) {
-									this.viewDate.setMonth(this.viewDate.getMonth() - 1);
-								} else {
-									this.viewDate.setFullYear(this.viewDate.getFullYear() - 1);
-									this.viewDate.setMonth(11);
-								}
+							case 'switch':
+								this.showMode(1);
 								break;
-
+							case 'prev':
 							case 'next':
-								if (this.viewDate.getMonth() < 11) {
-									this.viewDate.setMonth(this.viewDate.getMonth() + 1);
-								} else {
-									this.viewDate.setFullYear(this.viewDate.getFullYear() + 1);
-									this.viewDate.setMonth(0);
+								var dir = DPGlobal.modes[this.viewMode].navStep * (target[0].className == 'prev' ? -1 : 1);
+								switch(this.viewMode){
+									case 0:
+										this.viewDate = this.moveMonth(this.viewDate, dir);
+										break;
+									case 1:
+									case 2:
+										this.viewDate = this.moveYear(this.viewDate, dir);
+										break;
 								}
-
+								this.fill();
+								break;
+							case 'today':
+								var date = new Date();
+								date.setUTCHours(0);
+								date.setUTCMinutes(0);
+								date.setUTCSeconds(0);
+								date.setUTCMilliseconds(0);
+
+								this.showMode(-2);
+								var which = this.todayBtn == 'linked' ? null : 'view';
+								this._setDate(date, which);
 								break;
 						}
-						this.fill();
+						break;
+					case 'span':
+						if (!target.is('.disabled')) {
+							this.viewDate.setUTCDate(1);
+							if (target.is('.month')) {
+								var month = target.parent().find('span').index(target);
+								this.viewDate.setUTCMonth(month);
+								this.element.trigger({
+									type: 'changeMonth',
+									date: this.viewDate
+								});
+							} else {
+								var year = parseInt(target.text(), 10)||0;
+								this.viewDate.setUTCFullYear(year);
+								this.element.trigger({
+									type: 'changeYear',
+									date: this.viewDate
+								});
+							}
+							this.showMode(-1);
+							this.fill();
+						}
 						break;
 					case 'td':
-						if (target.is('.day')){
+						if (target.is('.day') && !target.is('.disabled')){
+							var day = parseInt(target.text(), 10)||1;
+							var year = this.viewDate.getUTCFullYear(),
+								month = this.viewDate.getUTCMonth();
 							if (target.is('.old')) {
-								return;
+								if (month == 0) {
+									month = 11;
+									year -= 1;
+								} else {
+									month -= 1;
+								}
 							} else if (target.is('.new')) {
-								return;
+								if (month == 11) {
+									month = 0;
+									year += 1;
+								} else {
+									month += 1;
+								}
 							}
-
-							var day = parseInt(target.text(), 10)||1;
-							var month = this.viewDate.getMonth();
-							var year = this.viewDate.getFullYear();
-							this.date = new Date(year, month, day,this.date.getHours(),this.date.getMinutes(),this.date.getSeconds(),0);
-							this.viewDate = new Date(year, month, day,0,0,0,0);
-							this.fill();
-							this.setValue();
-							this.element.trigger({
-								type: 'changeDate',
-								date: this.date
-							});
-							this.hide();
+							this._setDate(UTCDate(year, month, day,0,0,0,0));
 						}
 						break;
 				}
 			}
-			return false;
 		},
 
+		_setDate: function(date, which){
+			if (!which || which == 'date')
+				this.date = date;
+			if (!which || which  == 'view')
+				this.viewDate = date;
+			this.fill();
+			this.setValue();
+			this.element.trigger({
+				type: 'changeDate',
+				date: this.date
+			});
+			var element;
+			if (this.isInput) {
+				element = this.element;
+			} else if (this.component){
+				element = this.element.find('input');
+			}
+			if (element) {
+				element.change();
+				if (this.autoclose) {
+									this.hide();
+				}
+			}
+		},
+
+		moveMonth: function(date, dir){
+			if (!dir) return date;
+			var new_date = new Date(date.valueOf()),
+				day = new_date.getUTCDate(),
+				month = new_date.getUTCMonth(),
+				mag = Math.abs(dir),
+				new_month, test;
+			dir = dir > 0 ? 1 : -1;
+			if (mag == 1){
+				test = dir == -1
+					// If going back one month, make sure month is not current month
+					// (eg, Mar 31 -> Feb 31 == Feb 28, not Mar 02)
+					? function(){ return new_date.getUTCMonth() == month; }
+					// If going forward one month, make sure month is as expected
+					// (eg, Jan 31 -> Feb 31 == Feb 28, not Mar 02)
+					: function(){ return new_date.getUTCMonth() != new_month; };
+				new_month = month + dir;
+				new_date.setUTCMonth(new_month);
+				// Dec -> Jan (12) or Jan -> Dec (-1) -- limit expected date to 0-11
+				if (new_month < 0 || new_month > 11)
+					new_month = (new_month + 12) % 12;
+			} else {
+				// For magnitudes >1, move one month at a time...
+				for (var i=0; i<mag; i++)
+					// ...which might decrease the day (eg, Jan 31 to Feb 28, etc)...
+					new_date = this.moveMonth(new_date, dir);
+				// ...then reset the day, keeping it in the new month
+				new_month = new_date.getUTCMonth();
+				new_date.setUTCDate(day);
+				test = function(){ return new_month != new_date.getUTCMonth(); };
+			}
+			// Common date-resetting loop -- if date is beyond end of month, make it
+			// end of month
+			while (test()){
+				new_date.setUTCDate(--day);
+				new_date.setUTCMonth(new_month);
+			}
+			return new_date;
+		},
+
+		moveYear: function(date, dir){
+			return this.moveMonth(date, dir*12);
+		},
+
+		dateWithinRange: function(date){
+			return date >= this.startDate && date <= this.endDate;
+		},
+
+		keydown: function(e){
+			if (this.picker.is(':not(:visible)')){
+				if (e.keyCode == 27) // allow escape to hide and re-show picker
+					this.show();
+				return;
+			}
+			var dateChanged = false,
+				dir, day, month,
+				newDate, newViewDate;
+			switch(e.keyCode){
+				case 27: // escape
+					this.hide();
+					e.preventDefault();
+					break;
+				case 37: // left
+				case 39: // right
+					if (!this.keyboardNavigation) break;
+					dir = e.keyCode == 37 ? -1 : 1;
+					if (e.ctrlKey){
+						newDate = this.moveYear(this.date, dir);
+						newViewDate = this.moveYear(this.viewDate, dir);
+					} else if (e.shiftKey){
+						newDate = this.moveMonth(this.date, dir);
+						newViewDate = this.moveMonth(this.viewDate, dir);
+					} else {
+						newDate = new Date(this.date);
+						newDate.setUTCDate(this.date.getUTCDate() + dir);
+						newViewDate = new Date(this.viewDate);
+						newViewDate.setUTCDate(this.viewDate.getUTCDate() + dir);
+					}
+					if (this.dateWithinRange(newDate)){
+						this.date = newDate;
+						this.viewDate = newViewDate;
+						this.setValue();
+						this.update();
+						e.preventDefault();
+						dateChanged = true;
+					}
+					break;
+				case 38: // up
+				case 40: // down
+					if (!this.keyboardNavigation) break;
+					dir = e.keyCode == 38 ? -1 : 1;
+					if (e.ctrlKey){
+						newDate = this.moveYear(this.date, dir);
+						newViewDate = this.moveYear(this.viewDate, dir);
+					} else if (e.shiftKey){
+						newDate = this.moveMonth(this.date, dir);
+						newViewDate = this.moveMonth(this.viewDate, dir);
+					} else {
+						newDate = new Date(this.date);
+						newDate.setUTCDate(this.date.getUTCDate() + dir * 7);
+						newViewDate = new Date(this.viewDate);
+						newViewDate.setUTCDate(this.viewDate.getUTCDate() + dir * 7);
+					}
+					if (this.dateWithinRange(newDate)){
+						this.date = newDate;
+						this.viewDate = newViewDate;
+						this.setValue();
+						this.update();
+						e.preventDefault();
+						dateChanged = true;
+					}
+					break;
+				case 13: // enter
+					this.hide();
+					e.preventDefault();
+					break;
+				case 9: // tab
+					this.hide();
+					break;
+			}
+			if (dateChanged){
+				this.element.trigger({
+					type: 'changeDate',
+					date: this.date
+				});
+				var element;
+				if (this.isInput) {
+					element = this.element;
+				} else if (this.component){
+					element = this.element.find('input');
+				}
+				if (element) {
+					element.change();
+				}
+			}
+		},
+
+		showMode: function(dir) {
+			if (dir) {
+				this.viewMode = Math.max(0, Math.min(2, this.viewMode + dir));
+			}
+			this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).show();
+			this.updateNavArrows();
+		}
 	};
 
 	$.fn.datepicker = function ( option ) {
+		var args = Array.apply(null, arguments);
+		args.shift();
 		return this.each(function () {
 			var $this = $(this),
 				data = $this.data('datepicker'),
@@ -253,117 +642,195 @@
 			if (!data) {
 				$this.data('datepicker', (data = new Datepicker(this, $.extend({}, $.fn.datepicker.defaults,options))));
 			}
-			if (typeof option == 'string') data[option]();
+			if (typeof option == 'string' && typeof data[option] == 'function') {
+				data[option].apply(data, args);
+			}
 		});
 	};
 
 	$.fn.datepicker.defaults = {
 	};
 	$.fn.datepicker.Constructor = Datepicker;
+	var dates = $.fn.datepicker.dates = {
+		en: {
+			days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
+			daysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
+			daysMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"],
+			months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
+			monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
+			today: "Today"
+		}
+	}
 
 	var DPGlobal = {
+		modes: [
+			{
+				clsName: 'days',
+				navFnc: 'Month',
+				navStep: 1
+			},
+			{
+				clsName: 'months',
+				navFnc: 'FullYear',
+				navStep: 1
+			},
+			{
+				clsName: 'years',
+				navFnc: 'FullYear',
+				navStep: 10
+		}],
 		isLeapYear: function (year) {
 			return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0))
 		},
 		getDaysInMonth: function (year, month) {
 			return [31, (DPGlobal.isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]
 		},
-		parseDate: function(dateStr, format) { //convert str into date
-			dateStr = dateStr.replace(/:/g, '/');
-			dateStr = dateStr.replace(/ /g, '/');
-			strParts = dateStr.split('/');
-
-			format = format.replace(/:/g, '/');
-			format = format.replace(/ /g, '/');
-			formatParts = format.split('/');
-
-			date = new Date(),
-			date.setHours(0); date.setMinutes(0); date.setSeconds(0); date.setMilliseconds(0);
-
-			for (var key in formatParts) {
-				if (typeof strParts[key] != 'undefined') {
-					val = strParts[key];
-					switch(formatParts[key]) {
-						case 'dd':
+		validParts: /dd?|mm?|MM?|yy(?:yy)?/g,
+		nonpunctuation: /[^ -\/:-@\[-`{-~\t\n\r]+/g,
+		parseFormat: function(format){
+			// IE treats \0 as a string end in inputs (truncating the value),
+			// so it's a bad format delimiter, anyway
+			var separators = format.replace(this.validParts, '\0').split('\0'),
+				parts = format.match(this.validParts);
+			if (!separators || !separators.length || !parts || parts.length == 0){
+				throw new Error("Invalid date format.");
+			}
+			return {separators: separators, parts: parts};
+		},
+		parseDate: function(date, format, language) {
+			if (date instanceof Date) return date;
+			if (/^[-+]\d+[dmwy]([\s,]+[-+]\d+[dmwy])*$/.test(date)) {
+				var part_re = /([-+]\d+)([dmwy])/,
+					parts = date.match(/([-+]\d+)([dmwy])/g),
+					part, dir;
+				date = new Date();
+				for (var i=0; i<parts.length; i++) {
+					part = part_re.exec(parts[i]);
+					dir = parseInt(part[1]);
+					switch(part[2]){
 						case 'd':
-							date.setDate(val);
+							date.setUTCDate(date.getUTCDate() + dir);
 							break;
-						case 'mm':
 						case 'm':
-							date.setMonth(val - 1);
-							break;
-						case 'yy':
-							date.setFullYear(2000 + val);
+							date = Datepicker.prototype.moveMonth.call(Datepicker.prototype, date, dir);
 							break;
-						case 'yyyy':
-							date.setFullYear(val);
+						case 'w':
+							date.setUTCDate(date.getUTCDate() + dir * 7);
 							break;
-						case 'hh':
-						case 'h':
-							date.setHours(val);
-							break;
-						case 'ii':
-						case 'i':
-							date.setMinutes(val);
-							break;
-						case 'ss':
-						case 's':
-							date.setSeconds(val);
+						case 'y':
+							date = Datepicker.prototype.moveYear.call(Datepicker.prototype, date, dir);
 							break;
 					}
 				}
+				return UTCDate(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0);
+			}
+			var parts = date && date.match(this.nonpunctuation) || [],
+				date = new Date(),
+				parsed = {},
+				setters_order = ['yyyy', 'yy', 'M', 'MM', 'm', 'mm', 'd', 'dd'],
+				setters_map = {
+					yyyy: function(d,v){ return d.setUTCFullYear(v); },
+					yy: function(d,v){ return d.setUTCFullYear(2000+v); },
+					m: function(d,v){
+						v -= 1;
+						while (v<0) v += 12;
+						v %= 12;
+						d.setUTCMonth(v);
+						while (d.getUTCMonth() != v)
+							d.setUTCDate(d.getUTCDate()-1);
+						return d;
+					},
+					d: function(d,v){ return d.setUTCDate(v); }
+				},
+				val, filtered, part;
+			setters_map['M'] = setters_map['MM'] = setters_map['mm'] = setters_map['m'];
+			setters_map['dd'] = setters_map['d'];
+			date = UTCDate(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0);
+			if (parts.length == format.parts.length) {
+				for (var i=0, cnt = format.parts.length; i < cnt; i++) {
+					val = parseInt(parts[i], 10);
+					part = format.parts[i];
+					if (isNaN(val)) {
+						switch(part) {
+							case 'MM':
+								filtered = $(dates[language].months).filter(function(){
+									var m = this.slice(0, parts[i].length),
+										p = parts[i].slice(0, m.length);
+									return m == p;
+								});
+								val = $.inArray(filtered[0], dates[language].months) + 1;
+								break;
+							case 'M':
+								filtered = $(dates[language].monthsShort).filter(function(){
+									var m = this.slice(0, parts[i].length),
+										p = parts[i].slice(0, m.length);
+									return m == p;
+								});
+								val = $.inArray(filtered[0], dates[language].monthsShort) + 1;
+								break;
+						}
+					}
+					parsed[part] = val;
+				}
+				for (var i=0, s; i<setters_order.length; i++){
+					s = setters_order[i];
+					if (s in parsed && !isNaN(parsed[s]))
+						setters_map[s](date, parsed[s])
+				}
 			}
 			return date;
 		},
-		formatDate: function(date, format){ // build a formatted string
-			var templateParts = {
-				dd: (date.getDate() < 10 ? '0' : '') + date.getDate(),
-				d: date.getDate(),
-				mm: ((date.getMonth() + 1) < 10 ? '0' : '') + (date.getMonth() + 1),
-				m: date.getMonth() + 1,
-				yyyy: date.getFullYear(),
-				yy: date.getFullYear().toString().substring(2),
-				hh: (date.getHours() < 10 ? '0' : '') + date.getHours(),
-				h: date.getHours(),
-				ii: (date.getMinutes() < 10 ? '0' : '') + date.getMinutes(),
-				i: date.getMinutes(),
-				ss: (date.getSeconds() < 10 ? '0' : '') + date.getSeconds(),
-				s: date.getSeconds()
+		formatDate: function(date, format, language){
+			var val = {
+				d: date.getUTCDate(),
+				m: date.getUTCMonth() + 1,
+				M: dates[language].monthsShort[date.getUTCMonth()],
+				MM: dates[language].months[date.getUTCMonth()],
+				yy: date.getUTCFullYear().toString().substring(2),
+				yyyy: date.getUTCFullYear()
 			};
-
-			var dateStr = format;
-
-			for (var key in templateParts) {
-			    val = templateParts[key];
-			    dateStr = dateStr.replace(key, val);
+			val.dd = (val.d < 10 ? '0' : '') + val.d;
+			val.mm = (val.m < 10 ? '0' : '') + val.m;
+			var date = [],
+				seps = $.extend([], format.separators);
+			for (var i=0, cnt = format.parts.length; i < cnt; i++) {
+				if (seps.length)
+					date.push(seps.shift())
+				date.push(val[format.parts[i]]);
 			}
-
-			return dateStr;
+			return date.join('');
 		},
 		headTemplate: '<thead>'+
 							'<tr>'+
-								'<th class="prev"><i>&larr;</i></th>'+
-								'<th colspan="5" class="monthname"></th>'+
-								'<th class="next"><i>&rarr;</i></th>'+
+								'<th class="prev"><i class="icon-arrow-left"/></th>'+
+								'<th colspan="5" class="switch"></th>'+
+								'<th class="next"><i class="icon-arrow-right"/></th>'+
 							'</tr>'+
 						'</thead>',
-		contTemplate: '<tbody><tr><td colspan="7"></td></tr></tbody>'
+		contTemplate: '<tbody><tr><td colspan="7"></td></tr></tbody>',
+		footTemplate: '<tfoot><tr><th colspan="7" class="today"></th></tr></tfoot>'
 	};
-	DPGlobal.template = '<div class="bs-sc-datepicker dropdown-menu">'+
+	DPGlobal.template = '<div class="datepicker dropdown-menu">'+
 							'<div class="datepicker-days">'+
 								'<table class=" table-condensed">'+
 									DPGlobal.headTemplate+
 									'<tbody></tbody>'+
+									DPGlobal.footTemplate+
+								'</table>'+
+							'</div>'+
+							'<div class="datepicker-months">'+
+								'<table class="table-condensed">'+
+									DPGlobal.headTemplate+
+									DPGlobal.contTemplate+
+									DPGlobal.footTemplate+
+								'</table>'+
+							'</div>'+
+							'<div class="datepicker-years">'+
+								'<table class="table-condensed">'+
+									DPGlobal.headTemplate+
+									DPGlobal.contTemplate+
+									DPGlobal.footTemplate+
 								'</table>'+
 							'</div>'+
 						'</div>';
-
 }( window.jQuery );
-
-$(function() {
-	$("input[data-datepicker-format]").datepicker({
-		weekStart: 1,
-		days: ["zo","ma","di","wo","do","vr","za"],
-		months: ["januari","februari","maart","april","mei","juni","juli","augustus","september","oktober","november","december"]
-	});
-});

+ 806 - 0
common/lib/timepicker.js

@@ -0,0 +1,806 @@
+/* =========================================================
+ * bootstrap-timepicker.js
+ * http://www.github.com/jdewit/bootstrap-timepicker
+ * =========================================================
+ * Copyright 2012
+ *
+ * Created By:
+ * Joris de Wit @joris_dewit
+ *
+ * Contributions By:
+ * Gilbert @mindeavor
+ * Koen Punt info@koenpunt.nl
+ * Nek
+ * Chris Martin
+ * Dominic Barnes contact@dominicbarnes.us
+ * Olivier Louvignes @olouv
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================================================= */
+
+!function($) {
+
+    "use strict"; // jshint ;_;
+
+    var isTouch = 'ontouchstart' in window;
+
+    /* TIMEPICKER PUBLIC CLASS DEFINITION
+     * ================================== */
+    var Timepicker = function(element, options) {
+        this.$element = $(element);
+        this.options = $.extend({}, $.fn.timepicker.defaults, options, this.$element.data());
+        this.minuteStep = this.options.minuteStep || this.minuteStep;
+        this.secondStep = this.options.secondStep || this.secondStep;
+        this.showMeridian = this.options.showMeridian || this.showMeridian;
+        this.showSeconds = this.options.showSeconds || this.showSeconds;
+        this.showInputs = this.options.showInputs || this.showInputs;
+        this.disableFocus = this.options.disableFocus || this.disableFocus;
+        this.template = this.options.template || this.template;
+        this.modalBackdrop = this.options.modalBackdrop || this.modalBackdrop;
+        this.defaultTime = this.options.defaultTime || this.defaultTime;
+        this.open = false;
+        this.init();
+    };
+
+    Timepicker.prototype = {
+
+        constructor: Timepicker
+
+        , init: function () {
+            if (this.$element.parent().hasClass('input-append')) {
+                this.$element.parent('.input-append').find('.add-on').on('click', $.proxy(this.showWidget, this));
+                this.$element.on({
+                    focus: $.proxy(this.highlightUnit, this),
+                    click: $.proxy(this.highlightUnit, this),
+                    keydown: $.proxy(this.elementKeydown, this),
+                    blur: $.proxy(this.blurElement, this)
+                });
+
+            } else {
+                if (this.template) {
+                    this.$element.on({
+                        focus: $.proxy(this.showWidget, this),
+                        click: $.proxy(this.showWidget, this),
+                        blur: $.proxy(this.blurElement, this)
+                    });
+                } else {
+                    this.$element.on({
+                        focus: $.proxy(this.highlightUnit, this),
+                        click: $.proxy(this.highlightUnit, this),
+                        keydown: $.proxy(this.elementKeydown, this),
+                        blur: $.proxy(this.blurElement, this)
+                    });
+                }
+            }
+
+
+            this.$widget = $(this.getTemplate()).appendTo('body');
+
+            this.$widget.on('click', $.proxy(this.widgetClick, this));
+
+            if (this.showInputs) {
+                this.$widget.find('input').on({
+                    click: function() { this.select(); },
+                    keydown: $.proxy(this.widgetKeydown, this),
+                    change: $.proxy(this.updateFromWidgetInputs, this)
+                });
+            }
+
+            this.setDefaultTime(this.defaultTime);
+        }
+
+        , showWidget: function(e) {
+            e.stopPropagation();
+            e.preventDefault();
+
+            if (this.open) {
+                return;
+            }
+
+            this.$element.trigger('show');
+
+            if (isTouch || this.disableFocus) {
+                this.$element.blur();
+            }
+
+            var pos = $.extend({}, this.$element.offset(), {
+                height: this.$element[0].offsetHeight
+            });
+
+            this.updateFromElementVal();
+
+            $('html')
+                .one(isTouch ? 'touchstart.timepicker.data-api' : 'click.timepicker.data-api', $.proxy(this.hideWidget, this))
+                .on(isTouch ? 'touchstart.timepicker.data-api' : 'click.timepicker.data-api', '.bootstrap-timepicker', function (e) { e.stopPropagation() });
+
+            if (this.template === 'modal') {
+                this.$widget.modal('show').on('hidden', $.proxy(this.hideWidget, this));
+            } else {
+                this.$widget.css({
+                    top: pos.top + pos.height
+                    , left: pos.left
+                })
+
+                if (!this.open) {
+                    this.$widget.addClass('open');
+                }
+            }
+
+            this.open = true;
+            this.$element.trigger('shown');
+        }
+
+        , hideWidget: function(){
+            this.$element.trigger('hide');
+
+            if (this.template === 'modal') {
+                this.$widget.modal('hide');
+            } else {
+                this.$widget.removeClass('open');
+            }
+            this.open = false;
+            this.$element.trigger('hidden');
+        }
+
+        , widgetClick: function(e) {
+            e.stopPropagation();
+            e.preventDefault();
+
+            var action = $(e.target).closest('a').data('action');
+            if (action) {
+                this[action]();
+                this.update();
+            }
+        }
+
+        , widgetKeydown: function(e) {
+            var input = $(e.target).closest('input').attr('name');
+
+            switch (e.keyCode) {
+                case 9: //tab
+                    if (this.showMeridian) {
+                        if (input == 'meridian') {
+                            this.hideWidget();
+                        }
+                    } else {
+                        if (this.showSeconds) {
+                            if (input == 'second') {
+                                this.hideWidget();
+                            }
+                        } else {
+                            if (input == 'minute') {
+                                this.hideWidget();
+                            }
+                        }
+                    }
+                break;
+                case 27: // escape
+                    this.hideWidget();
+                break;
+                case 38: // up arrow
+                    switch (input) {
+                        case 'hour':
+                            this.incrementHour();
+                        break;
+                        case 'minute':
+                            this.incrementMinute();
+                        break;
+                        case 'second':
+                            this.incrementSecond();
+                        break;
+                        case 'meridian':
+                            this.toggleMeridian();
+                        break;
+                    }
+                    this.update();
+                break;
+                case 40: // down arrow
+                    switch (input) {
+                        case 'hour':
+                            this.decrementHour();
+                        break;
+                        case 'minute':
+                            this.decrementMinute();
+                        break;
+                        case 'second':
+                            this.decrementSecond();
+                        break;
+                        case 'meridian':
+                            this.toggleMeridian();
+                        break;
+                    }
+                    this.update();
+                break;
+            }
+        }
+
+        , elementKeydown: function(e) {
+            var input = this.$element.get(0);
+            switch (e.keyCode) {
+                case 0: //input
+                break;
+                case 9: //tab
+                    this.updateFromElementVal();
+                    if (this.showMeridian) {
+                        if (this.highlightedUnit != 'meridian') {
+                            e.preventDefault();
+                            this.highlightNextUnit();
+                        }
+                    } else {
+                        if (this.showSeconds) {
+                            if (this.highlightedUnit != 'second') {
+                                e.preventDefault();
+                                this.highlightNextUnit();
+                            }
+                        } else {
+                            if (this.highlightedUnit != 'minute') {
+                                e.preventDefault();
+                                this.highlightNextUnit();
+                            }
+                        }
+                    }
+                break;
+                case 27: // escape
+                    this.updateFromElementVal();
+                break;
+                case 37: // left arrow
+                    this.updateFromElementVal();
+                    this.highlightPrevUnit();
+                break;
+                case 38: // up arrow
+                    switch (this.highlightedUnit) {
+                        case 'hour':
+                            this.incrementHour();
+                        break;
+                        case 'minute':
+                            this.incrementMinute();
+                        break;
+                        case 'second':
+                            this.incrementSecond();
+                        break;
+                        case 'meridian':
+                            this.toggleMeridian();
+                        break;
+                    }
+                    this.updateElement();
+                break;
+                case 39: // right arrow
+                    this.updateFromElementVal();
+                    this.highlightNextUnit();
+                break;
+                case 40: // down arrow
+                    switch (this.highlightedUnit) {
+                        case 'hour':
+                            this.decrementHour();
+                        break;
+                        case 'minute':
+                            this.decrementMinute();
+                        break;
+                        case 'second':
+                            this.decrementSecond();
+                        break;
+                        case 'meridian':
+                            this.toggleMeridian();
+                        break;
+                    }
+                    this.updateElement();
+                break;
+            }
+
+            if (e.keyCode !== 0 && e.keyCode !== 8 && e.keyCode !== 9 && e.keyCode !== 46) {
+                e.preventDefault();
+            }
+        }
+
+        , setValues: function(time) {
+            if (this.showMeridian) {
+                var arr = time.split(' ');
+                var timeArray = arr[0].split(':');
+                this.meridian = arr[1];
+            } else {
+                var timeArray = time.split(':');
+            }
+
+            this.hour = parseInt(timeArray[0], 10);
+            this.minute = parseInt(timeArray[1], 10);
+            this.second = parseInt(timeArray[2], 10);
+
+            if (isNaN(this.hour)) {
+                this.hour = 0;
+            }
+            if (isNaN(this.minute)) {
+                this.minute = 0;
+            }
+
+            if (this.showMeridian) {
+                if (this.hour > 12) {
+                    this.hour = 12;
+                } else if (this.hour < 1) {
+                    this.hour = 1;
+                }
+
+                if (this.meridian == 'am' || this.meridian == 'a') {
+                    this.meridian = 'AM';
+                } else if (this.meridian == 'pm' || this.meridian == 'p') {
+                    this.meridian = 'PM';
+                }
+
+                if (this.meridian != 'AM' && this.meridian != 'PM') {
+                    this.meridian = 'AM';
+                }
+            } else {
+                 if (this.hour >= 24) {
+                    this.hour = 23;
+                } else if (this.hour < 0) {
+                    this.hour = 0;
+                }
+            }
+
+            if (this.minute < 0) {
+                this.minute = 0;
+            } else if (this.minute >= 60) {
+                this.minute = 59;
+            }
+
+            if (this.showSeconds) {
+                if (isNaN(this.second)) {
+                    this.second = 0;
+                } else if (this.second < 0) {
+                    this.second = 0;
+                } else if (this.second >= 60) {
+                    this.second = 59;
+                }
+            }
+
+            if ( this.$element.val() != '' )
+                this.updateElement();
+            this.updateWidget();
+        }
+
+        , setMeridian: function(meridian) {
+            if (meridian == 'a' || meridian == 'am' || meridian == 'AM' ) {
+                this.meridian = 'AM';
+            } else if (meridian == 'p' || meridian == 'pm' || meridian == 'PM' ) {
+                this.meridian = 'PM';
+            } else {
+                this.updateWidget();
+            }
+
+            this.updateElement();
+        }
+
+        , setDefaultTime: function(defaultTime){
+            if (defaultTime) {
+                if (defaultTime === 'current') {
+                    var dTime = new Date();
+                    var hours = dTime.getHours();
+                    var minutes = Math.floor(dTime.getMinutes() / this.minuteStep) * this.minuteStep;
+                    var seconds = Math.floor(dTime.getSeconds() / this.secondStep) * this.secondStep;
+                    var meridian = "AM";
+                    if (this.showMeridian) {
+                        if (hours === 0) {
+                            hours = 12;
+                        } else if (hours >= 12) {
+                            if (hours > 12) {
+                                hours = hours - 12;
+                            }
+                            meridian = "PM";
+                        } else {
+                           meridian = "AM";
+                        }
+                    }
+                    this.hour = hours;
+                    this.minute = minutes;
+                    this.second = seconds;
+                    this.meridian = meridian;
+                } else if (defaultTime === 'value') {
+                    this.setValues(this.$element.val());
+                } else {
+                    this.setValues(defaultTime);
+                }
+                if ( this.$element.val() != '' )
+                    this.updateElement();
+                this.updateWidget();
+            } else {
+                this.hour = 0;
+                this.minute = 0;
+                this.second = 0;
+            }
+        }
+
+        , formatTime: function(hour, minute, second, meridian) {
+            hour = hour < 10 ? '0' + hour : hour;
+            minute = minute < 10 ? '0' + minute : minute;
+            second = second < 10 ? '0' + second : second;
+
+            return hour + ':' + minute + (this.showSeconds ? ':' + second : '') + (this.showMeridian ? ' ' + meridian : '');
+        }
+
+        , getTime: function() {
+            return this.formatTime(this.hour, this.minute, this.second, this.meridian);
+        }
+
+        , setTime: function(time) {
+            this.setValues(time);
+            this.update();
+        }
+
+        , update: function() {
+            this.updateElement();
+            this.updateWidget();
+        }
+
+        , blurElement: function() {
+          this.highlightedUnit = undefined;
+          this.updateFromElementVal();
+        }
+
+        , updateElement: function() {
+            var time = this.getTime();
+
+            this.$element.val(time).change();
+
+            switch (this.highlightedUnit) {
+                case 'hour':
+                    this.highlightHour();
+                break;
+                case 'minute':
+                    this.highlightMinute();
+                break;
+                case 'second':
+                    this.highlightSecond();
+                break;
+                case 'meridian':
+                    this.highlightMeridian();
+                break;
+            }
+        }
+
+        , updateWidget: function() {
+            if (this.showInputs) {
+                this.$widget.find('input.bootstrap-timepicker-hour').val(this.hour < 10 ? '0' + this.hour : this.hour);
+                this.$widget.find('input.bootstrap-timepicker-minute').val(this.minute < 10 ? '0' + this.minute : this.minute);
+                if (this.showSeconds) {
+                    this.$widget.find('input.bootstrap-timepicker-second').val(this.second < 10 ? '0' + this.second : this.second);
+                }
+                if (this.showMeridian) {
+                    this.$widget.find('input.bootstrap-timepicker-meridian').val(this.meridian);
+                }
+            } else {
+                this.$widget.find('span.bootstrap-timepicker-hour').text(this.hour);
+                this.$widget.find('span.bootstrap-timepicker-minute').text(this.minute < 10 ? '0' + this.minute : this.minute);
+                if (this.showSeconds) {
+                    this.$widget.find('span.bootstrap-timepicker-second').text(this.second < 10 ? '0' + this.second : this.second);
+                }
+                if (this.showMeridian) {
+                    this.$widget.find('span.bootstrap-timepicker-meridian').text(this.meridian);
+                }
+            }
+        }
+
+        , updateFromElementVal: function (e) {
+            var time = this.$element.val();
+            if (time) {
+                this.setValues(time);
+                this.updateWidget();
+            }
+        }
+
+        , updateFromWidgetInputs: function () {
+            var time = $('input.bootstrap-timepicker-hour', this.$widget).val() + ':' +
+                       $('input.bootstrap-timepicker-minute', this.$widget).val() +
+                       (this.showSeconds ?
+                           ':' + $('input.bootstrap-timepicker-second', this.$widget).val()
+                        : '') +
+                       (this.showMeridian ?
+                           ' ' + $('input.bootstrap-timepicker-meridian', this.$widget).val()
+                        : '');
+
+            this.setValues(time);
+        }
+
+        , getCursorPosition: function() {
+            var input = this.$element.get(0);
+
+            if ('selectionStart' in input) {
+                // Standard-compliant browsers
+                return input.selectionStart;
+            } else if (document.selection) {
+                // IE fix
+                input.focus();
+                var sel = document.selection.createRange();
+                var selLen = document.selection.createRange().text.length;
+                sel.moveStart('character', - input.value.length);
+
+                return sel.text.length - selLen;
+            }
+        }
+
+        , highlightUnit: function () {
+            var input = this.$element.get(0);
+
+            this.position = this.getCursorPosition();
+            if (this.position >= 0 && this.position <= 2) {
+                this.highlightHour();
+            } else if (this.position >= 3 && this.position <= 5) {
+                this.highlightMinute();
+            } else if (this.position >= 6 && this.position <= 8) {
+                if (this.showSeconds) {
+                    this.highlightSecond();
+                } else {
+                    this.highlightMeridian();
+                }
+            } else if (this.position >= 9 && this.position <= 11) {
+                this.highlightMeridian();
+            }
+        }
+
+        , highlightNextUnit: function() {
+            switch (this.highlightedUnit) {
+                case 'hour':
+                    this.highlightMinute();
+                break;
+                case 'minute':
+                    if (this.showSeconds) {
+                        this.highlightSecond();
+                    } else {
+                        this.highlightMeridian();
+                    }
+                break;
+                case 'second':
+                    this.highlightMeridian();
+                break;
+                case 'meridian':
+                    this.highlightHour();
+                break;
+            }
+        }
+
+        , highlightPrevUnit: function() {
+            switch (this.highlightedUnit) {
+                case 'hour':
+                    this.highlightMeridian();
+                break;
+                case 'minute':
+                    this.highlightHour();
+                break;
+                case 'second':
+                    this.highlightMinute();
+                break;
+                case 'meridian':
+                    if (this.showSeconds) {
+                        this.highlightSecond();
+                    } else {
+                        this.highlightMinute();
+                    }
+                break;
+            }
+        }
+
+        , highlightHour: function() {
+            this.highlightedUnit = 'hour';
+            this.$element.get(0).setSelectionRange(0,2);
+        }
+
+        , highlightMinute: function() {
+            this.highlightedUnit = 'minute';
+            this.$element.get(0).setSelectionRange(3,5);
+        }
+
+        , highlightSecond: function() {
+            this.highlightedUnit = 'second';
+            this.$element.get(0).setSelectionRange(6,8);
+        }
+
+        , highlightMeridian: function() {
+            this.highlightedUnit = 'meridian';
+            if (this.showSeconds) {
+                this.$element.get(0).setSelectionRange(9,11);
+            } else {
+                this.$element.get(0).setSelectionRange(6,8);
+            }
+        }
+
+        , incrementHour: function() {
+            if (this.showMeridian) {
+                if (this.hour === 11) {
+                    this.toggleMeridian();
+                } else if (this.hour === 12) {
+                    return this.hour = 1;
+                }
+            }
+            if (this.hour === 23) {
+                return this.hour = 0;
+            }
+            this.hour = this.hour + 1;
+        }
+
+        , decrementHour: function() {
+            if (this.showMeridian) {
+                if (this.hour === 1) {
+                    return this.hour = 12;
+                }
+                else if (this.hour === 12) {
+                    this.toggleMeridian();
+                }
+            }
+            if (this.hour === 0) {
+                return this.hour = 23;
+            }
+            this.hour = this.hour - 1;
+        }
+
+        , incrementMinute: function() {
+            var newVal = this.minute + this.minuteStep - (this.minute % this.minuteStep);
+            if (newVal > 59) {
+                this.incrementHour();
+                this.minute = newVal - 60;
+            } else {
+                this.minute = newVal;
+            }
+        }
+
+        , decrementMinute: function() {
+            var newVal = this.minute - this.minuteStep;
+            if (newVal < 0) {
+                this.decrementHour();
+                this.minute = newVal + 60;
+            } else {
+                this.minute = newVal;
+            }
+        }
+
+        , incrementSecond: function() {
+            var newVal = this.second + this.secondStep - (this.second % this.secondStep);
+            if (newVal > 59) {
+                this.incrementMinute();
+                this.second = newVal - 60;
+            } else {
+                this.second = newVal;
+            }
+        }
+
+        , decrementSecond: function() {
+            var newVal = this.second - this.secondStep;
+            if (newVal < 0) {
+                this.decrementMinute();
+                this.second = newVal + 60;
+            } else {
+                this.second = newVal;
+            }
+        }
+
+        , toggleMeridian: function() {
+            this.meridian = this.meridian === 'AM' ? 'PM' : 'AM';
+
+            this.update();
+        }
+
+        , getTemplate: function() {
+            if (this.options.templates[this.options.template]) {
+                return this.options.templates[this.options.template];
+            }
+            if (this.showInputs) {
+                var hourTemplate = '<input type="text" name="hour" class="bootstrap-timepicker-hour" maxlength="2"/>';
+                var minuteTemplate = '<input type="text" name="minute" class="bootstrap-timepicker-minute" maxlength="2"/>';
+                var secondTemplate = '<input type="text" name="second" class="bootstrap-timepicker-second" maxlength="2"/>';
+                var meridianTemplate = '<input type="text" name="meridian" class="bootstrap-timepicker-meridian" maxlength="2"/>';
+            } else {
+                var hourTemplate = '<span class="bootstrap-timepicker-hour"></span>';
+                var minuteTemplate = '<span class="bootstrap-timepicker-minute"></span>';
+                var secondTemplate = '<span class="bootstrap-timepicker-second"></span>';
+                var meridianTemplate = '<span class="bootstrap-timepicker-meridian"></span>';
+            }
+            var templateContent = '<table class="'+ (this.showSeconds ? 'show-seconds' : '') +' '+ (this.showMeridian ? 'show-meridian' : '') +'">'+
+                                       '<tr>'+
+                                           '<td><a href="#" data-action="incrementHour"><i class="icon-chevron-up"></i></a></td>'+
+                                           '<td class="separator">&nbsp;</td>'+
+                                           '<td><a href="#" data-action="incrementMinute"><i class="icon-chevron-up"></i></a></td>'+
+                                           (this.showSeconds ?
+                                               '<td class="separator">&nbsp;</td>'+
+                                               '<td><a href="#" data-action="incrementSecond"><i class="icon-chevron-up"></i></a></td>'
+                                           : '') +
+                                           (this.showMeridian ?
+                                               '<td class="separator">&nbsp;</td>'+
+                                               '<td class="meridian-column"><a href="#" data-action="toggleMeridian"><i class="icon-chevron-up"></i></a></td>'
+                                           : '') +
+                                       '</tr>'+
+                                       '<tr>'+
+                                           '<td>'+ hourTemplate +'</td> '+
+                                           '<td class="separator">:</td>'+
+                                           '<td>'+ minuteTemplate +'</td> '+
+                                           (this.showSeconds ?
+                                                '<td class="separator">:</td>'+
+                                                '<td>'+ secondTemplate +'</td>'
+                                           : '') +
+                                           (this.showMeridian ?
+                                                '<td class="separator">&nbsp;</td>'+
+                                                '<td>'+ meridianTemplate +'</td>'
+                                           : '') +
+                                       '</tr>'+
+                                       '<tr>'+
+                                           '<td><a href="#" data-action="decrementHour"><i class="icon-chevron-down"></i></a></td>'+
+                                           '<td class="separator"></td>'+
+                                           '<td><a href="#" data-action="decrementMinute"><i class="icon-chevron-down"></i></a></td>'+
+                                           (this.showSeconds ?
+                                                '<td class="separator">&nbsp;</td>'+
+                                                '<td><a href="#" data-action="decrementSecond"><i class="icon-chevron-down"></i></a></td>'
+                                           : '') +
+                                           (this.showMeridian ?
+                                                '<td class="separator">&nbsp;</td>'+
+                                                '<td><a href="#" data-action="toggleMeridian"><i class="icon-chevron-down"></i></a></td>'
+                                           : '') +
+                                       '</tr>'+
+                                   '</table>';
+
+            var template;
+            switch(this.options.template) {
+                case 'modal':
+                    template = '<div class="bootstrap-timepicker modal hide fade in" style="top: 30%; margin-top: 0; width: 200px; margin-left: -100px;" data-backdrop="'+ (this.modalBackdrop ? 'true' : 'false') +'">'+
+                                   '<div class="modal-header">'+
+                                       '<a href="#" class="close" data-dismiss="modal">×</a>'+
+                                       '<h3>Pick a Time</h3>'+
+                                   '</div>'+
+                                   '<div class="modal-content">'+
+                                        templateContent +
+                                   '</div>'+
+                                   '<div class="modal-footer">'+
+                                       '<a href="#" class="btn btn-primary" data-dismiss="modal">Ok</a>'+
+                                   '</div>'+
+                               '</div>';
+
+                break;
+                case 'dropdown':
+                    template = '<div class="bootstrap-timepicker dropdown-menu">'+
+                                    templateContent +
+                               '</div>';
+                break;
+
+            }
+            return template;
+        }
+    };
+
+
+    /* TIMEPICKER PLUGIN DEFINITION
+     * =========================== */
+
+    $.fn.timepicker = function (option) {
+        return this.each(function () {
+            var $this = $(this)
+            , data = $this.data('timepicker')
+            , options = typeof option == 'object' && option;
+            if (!data) {
+                $this.data('timepicker', (data = new Timepicker(this, options)));
+            }
+            if (typeof option == 'string') {
+                data[option]();
+            }
+        })
+    }
+
+    $.fn.timepicker.defaults = {
+      minuteStep: 15
+    , secondStep: 15
+    , disableFocus: false
+    , defaultTime: 'current'
+    , showSeconds: false
+    , showInputs: true
+    , showMeridian: true
+    , template: 'dropdown'
+    , modalBackdrop: false
+    , templates: {} // set custom templates
+    }
+
+    $.fn.timepicker.Constructor = Timepicker
+}(window.jQuery);

+ 2 - 1
config.js

@@ -30,7 +30,8 @@ var config = new Settings(
     timefield:      '@timestamp', 
     //indexpattern:  '"logstash-"yyyy.mm.dd',
     indexpattern:   '"shakespeare"', 
-    modules:        ['histogram','map','pie','table','stringquery','sort'], 
+    modules:        ['histogram','map','pie','table','stringquery','sort',
+                    'timepicker'], 
 
     defaultfields:  ['line_text'],
     perpage:        50,

+ 25 - 5
dashboards.js

@@ -2,18 +2,38 @@ var dashboards =
 {
   title: "Infinite Monkey Dashboard",
   rows: [
-    {
+      {
       title:  "Query Control",  
       height: "30px",
       panels: [
         {
           type    : "stringquery",
-          span    : 9,
+          span    : 12,
+          group   : "main",
+          query   : "wine"
+        }
+      ]
+    },
+    {
+      title:  "Options",
+      collapse: true,  
+      height: "30px",
+      panels: [
+        {
+          type    : "timepicker",
+          span    : 5,
+          mode    : 'relative',
+          refresh : {
+            enable  : false,
+            interval: 30,
+            min     : 10
+          },
+          timespan: '1h',
           group   : "main"
         },
         {
           type    : "sort",
-          span    : 3,
+          span    : 4,
           group   : "main"
         }
       ]
@@ -51,7 +71,7 @@ var dashboards =
     },
     {
       title:  "Lines of Plays",
-      height: "300px",
+      height: "250px",
       panels: [
         {
           title   : "Plays",
@@ -70,7 +90,7 @@ var dashboards =
           type    : "table",
           span    : 8,
           query   : "*",
-          fields  : ['@timestamp','speaker','text_entry'],
+          fields  : ['@timestamp','play_name','speaker','text_entry'],
           group   : "main"
         }
       ]

+ 5 - 10
index.html

@@ -1,8 +1,8 @@
 <!DOCTYPE html>
-<!--[if lt IE 7]>      <html class="no-js lt-ie9 lt-ie8 lt-ie7 ng-app:kibana" lang="en" id="ng-app"> <![endif]-->
-<!--[if IE 7]>         <html class="no-js lt-ie9 lt-ie8 ng-app:kibana" lang="en" id="ng-app"> <![endif]-->
-<!--[if IE 8]>         <html class="no-js lt-ie9" lang="en" ng-app="kibana"> <![endif]-->
-<!--[if gt IE 8]><!--> <html class="no-js" lang="en" ng-app="kibana"> <!--<![endif]-->
+<!--[if lt IE 7]>      <html class="no-js lt-ie9 lt-ie8 lt-ie7" lang="en" id="ng-app"> <![endif]-->
+<!--[if IE 7]>         <html class="no-js lt-ie9 lt-ie8" lang="en" id="ng-app"> <![endif]-->
+<!--[if IE 8]>         <html class="no-js lt-ie9" lang="en"> <![endif]-->
+<!--[if gt IE 8]><!--> <html class="no-js" lang="en"> <!--<![endif]-->
 <head>
   <meta charset="utf-8">
   <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
@@ -16,7 +16,7 @@
   <link rel="stylesheet" href="/common/css/bootstrap.min.css">
   <link rel="stylesheet" href="/common/css/bootstrap-responsive.min.css">
   <link rel="stylesheet" href="/common/css/elasticjs.css">
-  <link rel="stylesheet" href="/common/css/datepicker.css">
+  <link rel="stylesheet" href="/common/css/timepicker.css">
 
   <!-- project dependency libs -->
   <script src="/common/lib/LAB.min.js"></script>
@@ -34,11 +34,6 @@
         <span class="brand">{{dashboards.title}}</span>
         <span class="brand"><small><small>Kibana Preview</small></small></span>
         <div class='pull-right' style="padding-top: 5px; padding-left: 10px"><input type="file" id="upload" upload /></div>
-        <div  class="btn-group pull-right">
-          <button class="btn" ng-click="pause()"><i ng-class="{'icon-pause': playing,'icon-play': !playing}"></i></button>
-          <button class="btn" ng-repeat='timespan in time_options' ng-click="set_timespan(timespan)">{{timespan}}</button>
-        </div>
-        
       </div>
     </div>
   </div>

+ 10 - 3
js/app.js

@@ -9,6 +9,7 @@ var modules = [
   'kibana.services', 
   'kibana.directives', 
   'elasticjs.service',
+  '$strap.directives',
   'kibana.panels',
   ]
 
@@ -17,7 +18,10 @@ var scripts = []
 var labjs = $LAB
   .script("common/lib/jquery-1.8.0.min.js").wait()
   .script("common/lib/modernizr-2.6.1.min.js")
-  .script("common/lib/underscore.min.js")
+  .script("common/lib/underscore.min.js")  
+  .script("common/lib/bootstrap.min.js")
+  .script('common/lib/datepicker.js')
+  .script('common/lib/timepicker.js')
   .script("common/lib/angular.min.js")
   .script("common/lib/angular-strap.min.js")
   .script("common/lib/elastic.min.js")
@@ -26,12 +30,12 @@ var labjs = $LAB
   .script("common/lib/date.js")
   .script("common/lib/datepicker.js")
   .script("common/lib/shared.js")
+  .script("dashboards.js")
   .script("js/services.js")
   .script("js/controllers.js")
   .script("js/filters.js")
   .script("js/directives.js")
   .script("js/panels.js")
-  .script("dashboards.js");
 
 _.each(config.modules, function(v) {
   labjs = labjs.script('panels/'+v+'/module.js').wait()
@@ -49,5 +53,8 @@ labjs.wait(function(){
           redirectTo: '/dashboard'
         });
     }]);
-
+  angular.element(document).ready(function() {
+    $('body').attr('ng-controller', 'DashCtrl')
+    angular.bootstrap(document, ['kibana']);
+  });
 });

+ 12 - 9
js/controllers.js

@@ -9,17 +9,20 @@ angular.module('kibana.controllers', [])
   $scope.config = config;
   $scope.dashboards = dashboards
   $scope.timespan = config.timespan
-  $scope.from = time_ago($scope.timespan);
-  $scope.to = new Date();
-
-  $scope.time_options = ['5m','15m','1h','6h','12h','24h','2d','7d','30d'];
+  $scope.time = {
+    from : time_ago($scope.timespan),
+    to   : new Date()
+  }
 
+  // I'm leaving in all this refresh stuff until I figure out how index
+  // list caching should work. Maybe it should be handled by each time panel?
+  // That would require dashboard to contain a time panel. Hmm.
   $scope.counter = 0;
   $scope.playing = true;
   $scope.play = function(){
     $scope.counter++;
-    $scope.to = new Date();
-    $scope.from = time_ago($scope.timespan);
+    $scope.time.to = new Date();
+    $scope.time.from = time_ago($scope.timespan);
     $scope.$root.$eval() 
     mytimeout = $timeout($scope.play,config.refresh);
   }
@@ -37,9 +40,9 @@ angular.module('kibana.controllers', [])
 
   // If from/to to change, update index list
   $scope.$watch(function() { 
-    return angular.toJson([$scope.from, $scope.to]) 
+    return angular.toJson([$scope.time.from, $scope.time.to]) 
   }, function(){
-    indices($scope.from,$scope.to).then(function (p) {
+    indices($scope.time.from,$scope.time.to).then(function (p) {
       $scope.index = p.join();
     });
   });
@@ -54,7 +57,7 @@ angular.module('kibana.controllers', [])
 
   $scope.set_timespan = function(timespan) {
     $scope.timespan = timespan;
-    $scope.from = time_ago($scope.timespan);
+    $scope.time.from = time_ago($scope.timespan);
   }
 
   // returns a promise containing an array of all indices matching the index

+ 25 - 20
panels/histogram/module.js

@@ -1,5 +1,5 @@
 angular.module('kibana.histogram', [])
-.controller('histogram', function($scope, $location) {
+.controller('histogram', function($scope, $rootScope) {
 
   // Set and populate defaults
   var _d = {
@@ -14,14 +14,11 @@ angular.module('kibana.histogram', [])
       ? _d[k] : $scope.panel[k];
   });
 
-  if (!(_.isUndefined($scope.panel.group))) {
-    $scope.$on($scope.panel.group+"-query", function(event, query) {
-      $scope.panel.query[0].query = query;
-      $scope.get_data();
-    });
-  }
-
   $scope.get_data = function() {
+    // Make sure we have everything for the request to complete
+    if(_.isUndefined($scope.panel.time))
+      return
+
     var request = $scope.ejs.Request().indices($scope.index);
     
     // Build the question part of the query
@@ -30,8 +27,8 @@ angular.module('kibana.histogram', [])
       queries.push($scope.ejs.FilteredQuery(
         ejs.QueryStringQuery(v.query || '*'),
         ejs.RangeFilter(config.timefield)
-          .from($scope.from)
-          .to($scope.to)
+          .from($scope.panel.time.from)
+          .to($scope.panel.time.to)
           .cache(false))
       )
     });
@@ -53,15 +50,14 @@ angular.module('kibana.histogram', [])
     results.then(function(results) {
       $scope.hits = results.hits.total;
       // Null values at each end of the time range make sure we see entire range
-
       $scope.data = [];
       _.each(results.facets, function(v, k) {
         var series = {};
-        var data = [[$scope.from.getTime(), null]];
+        var data = [[$scope.panel.time.from.getTime(), null]];
         _.each(v.entries, function(v, k) {
           data.push([v['time'],v['count']])
         });
-        data.push([$scope.to.getTime(), null])
+        data.push([$scope.panel.time.to.getTime(), null])
         series.data = {
           label: $scope.panel.query[k].label, 
           data: data, 
@@ -73,13 +69,22 @@ angular.module('kibana.histogram', [])
     });
   }
 
-  $scope.$watch(function() { 
-    return angular.toJson([$scope.from, $scope.to]) 
-  }, function(){
-    $scope.panel.interval = secondsToHms(
-      calculate_interval($scope.from,$scope.to,50,0)/1000),
-    $scope.get_data();
-  });
+  if (!(_.isUndefined($scope.panel.group))) {
+    $scope.$on($scope.panel.group+"-query", function(event, query) {
+      $scope.panel.query[0].query = query;
+      $scope.get_data();
+    });
+    $scope.$on($scope.panel.group+"-time", function(event, time) {
+      $scope.panel.time = time;
+      $scope.panel.interval = secondsToHms(
+        calculate_interval(time.from,time.to,50,0)/1000),
+      $scope.get_data();
+    });
+  }
+
+  // Now that we're all setup, request the time from our group
+  $rootScope.$broadcast($scope.panel.group+"-get_time")
+
 
 })
 .directive('histogram', function() {

+ 20 - 16
panels/map/module.js

@@ -1,5 +1,5 @@
 angular.module('kibana.map', [])
-.controller('map', function($scope, $location) {
+.controller('map', function($scope, $rootScope) {
 
   // Set and populate defaults
   var _d = {
@@ -14,15 +14,11 @@ angular.module('kibana.map', [])
       ? _d[k] : $scope.panel[k];
   });
 
-
-  if (!(_.isUndefined($scope.panel.group))) {
-    $scope.$on($scope.panel.group+"-query", function(event, query) {
-      $scope.panel.query = query;
-      $scope.get_data();
-    });
-  }
-
   $scope.get_data = function() {
+    // Make sure we have everything for the request to complete
+    if(_.isUndefined($scope.panel.time))
+      return
+
     var request = $scope.ejs.Request().indices($scope.index);
 
     // Then the insert into facet and make the request
@@ -35,8 +31,8 @@ angular.module('kibana.map', [])
           ejs.FilteredQuery(
             ejs.QueryStringQuery($scope.panel.query || '*'),
             ejs.RangeFilter(config.timefield)
-              .from($scope.from)
-              .to($scope.to)
+              .from($scope.panel.time.from)
+              .to($scope.panel.time.to)
               .cache(false)
             )))).size(0)
       .doSearch();
@@ -51,11 +47,19 @@ angular.module('kibana.map', [])
     });
   }
 
-  $scope.$watch(function() { 
-    return angular.toJson([$scope.from, $scope.to]) 
-  }, function(){
-    $scope.get_data();
-  });
+  if (!(_.isUndefined($scope.panel.group))) {
+    $scope.$on($scope.panel.group+"-query", function(event, query) {
+      $scope.panel.query = query;
+      $scope.get_data();
+    });
+    $scope.$on($scope.panel.group+"-time", function(event, time) {
+      $scope.panel.time = time;
+      $scope.get_data();
+    });
+  }
+
+  // Now that we're all setup, request the time from our group
+  $rootScope.$broadcast($scope.panel.group+"-get_time")
 
 })
 .directive('map', function() {

+ 21 - 10
panels/pie/module.js

@@ -2,7 +2,7 @@ labjs = labjs.script("common/lib/panels/jquery.flot.js")
   .script("common/lib/panels/jquery.flot.pie.js")
 
 angular.module('kibana.pie', [])
-.controller('pie', function($scope, $location) {
+.controller('pie', function($scope, $rootScope) {
 
   // Set and populate defaults
   var _d = {
@@ -26,6 +26,9 @@ angular.module('kibana.pie', [])
   }
 
   $scope.get_data = function() {
+    if(_.isUndefined($scope.panel.time))
+      return
+    
     var request = $scope.ejs.Request().indices($scope.index);
 
     // If we have an array, use query facet
@@ -36,8 +39,8 @@ angular.module('kibana.pie', [])
         queries.push(ejs.FilteredQuery(
           ejs.QueryStringQuery(v.query || '*'),
           ejs.RangeFilter(config.timefield)
-            .from($scope.from)
-            .to($scope.to)
+            .from($scope.panel.time.from)
+            .to($scope.panel.time.to)
             .cache(false))
         )
       });
@@ -74,8 +77,8 @@ angular.module('kibana.pie', [])
             ejs.FilteredQuery(
               ejs.QueryStringQuery($scope.panel.query.query || '*'),
               ejs.RangeFilter(config.timefield)
-                .from($scope.from)
-                .to($scope.to)
+                .from($scope.panel.time.from)
+                .to($scope.panel.time.to)
                 .cache(false)
               )))).size(0)
         .doSearch();
@@ -100,11 +103,19 @@ angular.module('kibana.pie', [])
     }
   }
 
-  $scope.$watch(function() { 
-    return angular.toJson([$scope.from, $scope.to]) 
-  }, function(){
-    $scope.get_data();
-  });
+  if (!(_.isUndefined($scope.panel.group))) {
+    $scope.$on($scope.panel.group+"-query", function(event, query) {
+      $scope.panel.query.query = query;
+      $scope.get_data();
+    });
+    $scope.$on($scope.panel.group+"-time", function(event, time) {
+      $scope.panel.time = time;
+      $scope.get_data();
+    });
+  }
+
+  // Now that we're all setup, request the time from our group
+  $rootScope.$broadcast($scope.panel.group+"-get_time")
 
 })
 .directive('pie', function() {

+ 2 - 1
panels/sort/module.html

@@ -1,5 +1,6 @@
 <div ng-controller='sort' style="white-space: nowrap;">
   <h4 ng-class="{'ng-cloak': !panel.title}">{{panel.title}}</h4>
+  <label><small>{{panel.label}}</small></label>
+  <select style="width:85%" ng-model="panel.sort[0]" ng-options="f for f in fields"></select>
   <i ng-click="toggle_sort()" ng-class="{'icon-chevron-up': panel.sort[1] == 'asc','icon-chevron-down': panel.sort[1] == 'desc'}"></i>
-  <select ng-model="panel.sort[0]" ng-options="f for f in fields"></select>
 </div>

+ 1 - 0
panels/sort/module.js

@@ -3,6 +3,7 @@ angular.module('kibana.sort', [])
 
   // Set and populate defaults
   var _d = {
+    label   : "Sort",
     query   : "*",
     size    : 100,
     sort    : [config.timefield,'desc'],

+ 4 - 3
panels/stringquery/module.html

@@ -1,7 +1,8 @@
 <div ng-controller='stringquery'>
   <h4 ng-class="{'ng-cloak': !panel.title}">{{panel.title}}</h4>
-  <form class="form-search" style="margin-bottom:0px">
-    <input type="text" class="input-medium search-query" ng-model="query" style="width:85%">
-    <button type="submit" class="btn" ng-click="send_query(query)">Search</button>
+  <form class="input-append" style="margin-bottom:0px; white-space:nowrap;">
+    <label><small>{{panel.label}}</small></label>
+    <input type="text" ng-model="panel.query" style="width:90%">
+    <button type="submit" class="btn" ng-click="send_query(panel.query)"><i class="icon-search"></i></button>
   </form>
 </div>

+ 1 - 0
panels/stringquery/module.js

@@ -3,6 +3,7 @@ angular.module('kibana.stringquery', [])
 
   // Set and populate defaults
   var _d = {
+    label   : "Search",
     query   : "*",
     size    : 100,
     sort    : [config.timefield,'desc'],

+ 23 - 16
panels/table/module.js

@@ -12,28 +12,23 @@ angular.module('kibana.table', [])
       ? _d[k] : $scope.panel[k];
   });
 
-  // Events which this panel receives and sends
-  if (!(_.isUndefined($scope.panel.group))) {
-    // Receives these events
-    $scope.$on($scope.panel.group+"-query", function(event, query) {
-      $scope.panel.query = query;
-      $scope.get_data();
-    });
-  }
-
   $scope.toggle_sort = function() {
     $scope.panel.sort[1] = $scope.panel.sort[1] == 'asc' ? 'desc' : 'asc';
   }
 
   $scope.get_data = function() {
+    // Make sure we have everything for the request to complete
+    if(_.isUndefined($scope.panel.time))
+      return
+    
     var request = $scope.ejs.Request().indices($scope.index);
 
     var results = request
       .query(ejs.FilteredQuery(
         ejs.QueryStringQuery($scope.panel.query || '*'),
         ejs.RangeFilter(config.timefield)
-          .from($scope.from)
-          .to($scope.to)
+          .from($scope.panel.time.from)
+          .to($scope.panel.time.to)
           .cache(false)
         )
       )
@@ -63,10 +58,22 @@ angular.module('kibana.table', [])
     });
   }
 
-  $scope.$watch(function() { 
-    return angular.toJson([$scope.from, $scope.to, $scope.panel.sort]) 
-  }, function(){
-    $scope.get_data();
-  });
+  $scope.$watch(function() {
+    return angular.toJson($scope.panel.sort)
+  }, function(){$scope.get_data()});
+
+  if (!(_.isUndefined($scope.panel.group))) {
+    $scope.$on($scope.panel.group+"-query", function(event, query) {
+      $scope.panel.query = query;
+      $scope.get_data();
+    });
+    $scope.$on($scope.panel.group+"-time", function(event, time) {
+      $scope.panel.time = time;
+      $scope.get_data();
+    });
+  }
+
+  // Now that we're all setup, request the time from our group
+  $rootScope.$broadcast($scope.panel.group+"-get_time")
 
 })

+ 64 - 0
panels/timepicker/module.html

@@ -0,0 +1,64 @@
+<div ng-controller='timepicker' style="white-space: nowrap;">
+  <h4 ng-class="{'ng-cloak': !panel.title}">{{panel.title}}</h4>
+    <div class="row-fluid" ng-switch="panel.mode">
+      <div ng-switch-when="absolute">
+        <div class="span5">
+          <form class="nomargin">
+            <label><small>From</small></label>
+            <input type="text" class="input-smaller" ng-change="time_check()" ng-model="timepicker.from.date" data-date-format="mm/dd/yyyy" bs-datepicker>  
+            <input type="text" class="input-mini" ng-change="time_check()" data-show-meridian="false" data-show-seconds="true" ng-model="timepicker.from.time" bs-timepicker>
+          </form>
+        </div>
+        <div class="span5" style="margin-left:10px">
+          <form class="nomargin">
+            <label style="margin-left:5px"><small>To (<a ng-click="to_now()">now</a>)</small></label>
+            <input type="text" class="input-smaller" ng-change="time_check()" ng-model="timepicker.to.date" data-date-format="mm/dd/yyyy" bs-datepicker>  
+            <input type="text" class="input-mini" ng-change="time_check()" data-show-meridian="false" data-show-seconds="true" ng-model="timepicker.to.time" bs-timepicker>
+        </form>
+        </div>
+        <div class="span1">
+          <form class="nomargin">
+            <label><small><br></small></label>
+            <button class="btn" ng-click="time_apply()" ><i class="icon-check"></i></button>
+          </form>
+        </div>
+      </div>
+      <div ng-switch-when="since">
+        <div class="span5">
+          <form class="nomargin">
+            <label><small>Since</small></label>
+            <input type="text" class="input-smaller" ng-change="time_check()" ng-model="timepicker.from.date" data-date-format="mm/dd/yyyy" bs-datepicker>  
+            <input type="text" class="input-mini" ng-change="time_check()" data-show-meridian="false" data-show-seconds="true" ng-model="timepicker.from.time" bs-timepicker>
+          </form>
+        </div>
+        <div class="span1" style="margin-left:10px">
+          <form class="nomargin">
+            <label><small><br></small></label>
+            <button class="btn" ng-click="time_apply()" ><i class="icon-check"></i></button>
+          </form>
+        </div>
+      </div>
+      <div ng-switch-when="relative">
+        <div class="span11">
+          <form class="nomargin input-append">
+            <label><small>The last</small></label>
+            <button class="btn btn" ng-repeat='timespan in panel.time_options' ng-class="{'btn-success': (panel.timespan == timespan)}" ng-click="set_timespan(timespan)">{{timespan}}</button>
+            <!--<select ng-model="panel.sort[0]" ng-options="f for f in fields"></select>-->
+          </form>
+        </div>
+      </div>
+    </div>
+    <div class="row-fluid nomargin">
+      <div class="span12 small">
+        <a ng-click="set_mode('relative')" ng-class="{'strong': (panel.mode == 'relative')}">Relative</a> | 
+        <a ng-click="set_mode('absolute')" ng-class="{'strong': (panel.mode == 'absolute')}">Absolute</a> | 
+        <a ng-click="set_mode('since')"    ng-class="{'strong': (panel.mode == 'since')}">Since</a>
+        <span ng-hide="panel.mode == 'absolute'"> | 
+          <input type="checkbox" ng-model="panel.refresh.enable"> Auto-refresh 
+          <span ng-class="{'ng-cloak': !panel.refresh.enable}">
+            every <a data-title="<small>Auto-refresh Settings</small>" data-placement="bottom" bs-popover="'panels/timepicker/refreshctrl.html'">{{panel.refresh.interval}}s</a>.
+          </span>
+        </span>
+      </div>
+    </div>
+</div>

+ 168 - 0
panels/timepicker/module.js

@@ -0,0 +1,168 @@
+angular.module('kibana.timepicker', [])
+.controller('timepicker', function($scope, $rootScope, $timeout) {
+
+  // Set and populate defaults
+  var _d = {
+    mode    : "relative",
+    time_options : ['5m','15m','1h','6h','12h','24h','2d','7d','30d'],
+    timespan : '15m',
+    refresh : {
+      enable: false, 
+      interval: 3,
+      min: 3
+    },
+    time    : {
+      from  : $scope.time.from,
+      to    : $scope.time.to
+    }
+  }
+  _.each(_d, function(v, k) {
+    $scope.panel[k] = _.isUndefined($scope.panel[k]) 
+      ? _d[k] : $scope.panel[k];
+  });
+
+  // Private refresh interval that we can use for view display without causing
+  // unnecessary refreshes during changes
+  $scope.refresh_interval = $scope.panel.refresh.interval
+
+  // Init a private time object with Date() objects depending on mode
+  switch($scope.panel.mode) {
+    case 'absolute':
+      $scope.time = {
+        from : Date.parse($scope.panel.time.from),
+        to   : Date.parse($scope.panel.time.to)
+      }
+      break;
+    case 'since':
+      $scope.time = {
+        from : Date.parse($scope.panel.time.from),
+        to   : new Date()
+      }
+      break;
+    case 'relative':
+      $scope.time = {
+        from : time_ago($scope.panel.timespan),
+        to   : new Date()
+      }
+      break;
+  }
+
+  // Init the values for the time/date pickers
+  $scope.timepicker = {
+    from : {
+      time : $scope.time.from.format("HH:MM:ss"),
+      date : $scope.time.from.format("mm/dd/yyyy")
+    },
+    to : {
+      time : $scope.time.to.format("HH:MM:ss"),
+      date : $scope.time.to.format("mm/dd/yyyy")
+    } 
+  } 
+
+  // In the case that a panel is not ready to receive a time event, it may
+  // request one be sent by broadcasting a 'get_time' even to its group
+  if (!(_.isUndefined($scope.panel.group))) {
+    // Broadcast time when initializing
+    $rootScope.$broadcast($scope.panel.group+"-time", $scope.time)
+
+    // And whenever it is requested
+    $scope.$on($scope.panel.group+"-get_time", function(event) {
+      $rootScope.$broadcast($scope.panel.group+"-time", $scope.time)
+    });
+  }
+  
+  $scope.$watch('panel.refresh.enable', function() {$scope.refresh()});
+  $scope.$watch('panel.refresh.interval', function() {
+    $timeout(function(){
+      if(_.isNumber($scope.panel.refresh.interval)) {
+        if($scope.panel.refresh.interval < $scope.panel.refresh.min) {
+          $scope.panel.refresh.interval = $scope.panel.refresh.min        
+          $timeout.cancel($scope.panel.refresh.timer)
+          return;
+        }
+        $timeout.cancel($scope.panel.refresh.timer)
+        $scope.refresh()
+      } else {
+        $timeout.cancel($scope.panel.refresh.timer)
+      }
+    });
+  });
+
+
+  $scope.refresh = function() {
+    if ($scope.panel.refresh.enable) {
+      $scope.time_apply();
+      $scope.panel.refresh.timer = $timeout(
+        $scope.refresh,
+        $scope.panel.refresh.interval*1000
+      );
+    } else {
+      $timeout.cancel($scope.panel.refresh.timer)
+    }
+  }
+
+  $scope.set_mode = function(mode) {
+    $scope.panel.mode = mode;
+    $scope.panel.refresh.enable = mode === 'absolute' ? 
+      false : $scope.panel.refresh.enable
+  }
+
+  $scope.to_now = function() {
+    $scope.timepicker.to = {
+      time : new Date().format("HH:MM:ss"),
+      date : new Date().format("mm/dd/yyyy")
+    }
+  }
+
+  $scope.set_timespan = function(timespan) {
+    $scope.panel.timespan = timespan;
+    $scope.timepicker.from = {
+      time : time_ago(timespan).format("HH:MM:ss"),
+      date : time_ago(timespan).format("mm/dd/yyyy")
+    }
+    $scope.time_apply();
+  }
+
+  $scope.time_check = function(){
+    var from = $scope.panel.mode === 'relative' ? time_ago($scope.panel.timespan) :
+      Date.parse($scope.timepicker.from.date + " " + $scope.timepicker.from.time)
+    var to = $scope.panel.mode !== 'absolute' ? new Date() :
+      Date.parse($scope.timepicker.to.date + " " + $scope.timepicker.to.time)
+
+    if (from.getTime() >= to.getTime())
+      from = new Date(to.getTime() - 1000)
+
+    // Janky 0s timeout to get around $scope queue processing view issue
+    $timeout(function(){
+      $scope.timepicker = {
+        from : {
+          time : from.format("HH:MM:ss"),
+          date : from.format("mm/dd/yyyy")
+        },
+        to : {
+          time : to.format("HH:MM:ss"),
+          date : to.format("mm/dd/yyyy")
+        } 
+      }
+    });
+  }
+
+  $scope.time_apply = function() {  
+    $scope.time_check();
+    // Update internal time object
+    $scope.time = { 
+      from : Date.parse($scope.timepicker.from.date + " " + $scope.timepicker.from.time), 
+      to : Date.parse($scope.timepicker.to.date + " " + $scope.timepicker.to.time)
+    };
+
+    // Broadcast time
+    $rootScope.$broadcast($scope.panel.group+"-time", $scope.time)
+
+    // Update panel's string representation of the time object
+    $scope.panel.time = { 
+      from : $scope.time.from.format("mm/dd/yyyy HH:MM:ss"),
+      to : $scope.time.to.format("mm/dd/yyyy HH:MM:ss") 
+    };
+  };
+
+})

+ 5 - 0
panels/timepicker/refreshctrl.html

@@ -0,0 +1,5 @@
+<form name="refreshPopover" class='form-inline' style="margin:0px">
+    <label><small>Interval (seconds)</small></label>
+    <input type="number" class="input-mini" ng-model="refresh_interval"> 
+    <button type="button" class="btn" ng-click="panel.refresh.interval=refresh_interval;dismiss()"><i class="icon-check"></i></button>
+</form>

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott