diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/DocumentDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/DocumentDao.java index 34f9155e..4f4c4684 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/DocumentDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/DocumentDao.java @@ -41,7 +41,6 @@ public class DocumentDao { // Create the document EntityManager em = ThreadLocalContext.get().getEntityManager(); - document.setCreateDate(new Date()); em.persist(document); return document.getId(); @@ -121,11 +120,18 @@ public class DocumentDao { criteriaList.add("d.DOC_IDUSER_C = :userId"); parameterMap.put("userId", criteria.getUserId()); } - if (criteria.getSearch() != null) { criteriaList.add("(d.DOC_TITLE_C LIKE :search OR d.DOC_DESCRIPTION_C LIKE :search)"); parameterMap.put("search", "%" + criteria.getSearch() + "%"); } + if (criteria.getCreateDateMin() != null) { + criteriaList.add("d.DOC_CREATEDATE_D >= :createDateMin"); + parameterMap.put("createDateMin", criteria.getCreateDateMin()); + } + if (criteria.getCreateDateMax() != null) { + criteriaList.add("d.DOC_CREATEDATE_D <= :createDateMax"); + parameterMap.put("createDateMax", criteria.getCreateDateMax()); + } criteriaList.add("d.DOC_DELETEDATE_D is null"); diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/criteria/DocumentCriteria.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/criteria/DocumentCriteria.java index 195b8809..70074657 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/criteria/DocumentCriteria.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/criteria/DocumentCriteria.java @@ -1,5 +1,7 @@ package com.sismics.docs.core.dao.jpa.criteria; +import java.util.Date; + /** * Document criteria. @@ -17,6 +19,16 @@ public class DocumentCriteria { */ private String search; + /** + * Minimum creation date. + */ + private Date createDateMin; + + /** + * Maximum creation date. + */ + private Date createDateMax; + /** * Getter of userId. * @@ -52,4 +64,40 @@ public class DocumentCriteria { public void setSearch(String search) { this.search = search; } + + /** + * Getter of createDateMin. + * + * @return the createDateMin + */ + public Date getCreateDateMin() { + return createDateMin; + } + + /** + * Setter of createDateMin. + * + * @param createDateMin createDateMin + */ + public void setCreateDateMin(Date createDateMin) { + this.createDateMin = createDateMin; + } + + /** + * Getter of createDateMax. + * + * @return the createDateMax + */ + public Date getCreateDateMax() { + return createDateMax; + } + + /** + * Setter of createDateMax. + * + * @param createDateMax createDateMax + */ + public void setCreateDateMax(Date createDateMax) { + this.createDateMax = createDateMax; + } } diff --git a/docs-parent/TODO b/docs-parent/TODO index fefb14e4..761c77da 100644 --- a/docs-parent/TODO +++ b/docs-parent/TODO @@ -1,5 +1,4 @@ -- Client/server side edition of created date - Client/server side search on tags -- Client/server side search on creation date +- Client side search on creation date - Client/server side edition of existing tag names - Server side reordering files \ No newline at end of file diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java index 29de6fd4..0e670784 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java @@ -2,6 +2,7 @@ package com.sismics.docs.rest.resource; import java.text.MessageFormat; import java.util.ArrayList; +import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -105,11 +106,17 @@ public class DocumentResource extends BaseResource { @QueryParam("offset") Integer offset, @QueryParam("sort_column") Integer sortColumn, @QueryParam("asc") Boolean asc, - @QueryParam("search") String search) throws JSONException { + @QueryParam("search") String search, + @QueryParam("create_date_min") String createDateMinStr, + @QueryParam("create_date_max") String createDateMaxStr) throws JSONException { if (!authenticate()) { throw new ForbiddenClientException(); } + // Validate input data + Date createDateMin = ValidationUtil.validateDate(createDateMinStr, "create_date_min", true); + Date createDateMax = ValidationUtil.validateDate(createDateMaxStr, "create_date_max", true); + JSONObject response = new JSONObject(); List documents = new ArrayList(); @@ -118,6 +125,8 @@ public class DocumentResource extends BaseResource { SortCriteria sortCriteria = new SortCriteria(sortColumn, asc); DocumentCriteria documentCriteria = new DocumentCriteria(); documentCriteria.setUserId(principal.getId()); + documentCriteria.setCreateDateMin(createDateMin); + documentCriteria.setCreateDateMax(createDateMax); if (!Strings.isNullOrEmpty(search)) { documentCriteria.setSearch(search); } @@ -150,7 +159,8 @@ public class DocumentResource extends BaseResource { public Response add( @FormParam("title") String title, @FormParam("description") String description, - @FormParam("tags[]") List tagList) throws JSONException { + @FormParam("tags[]") List tagList, + @FormParam("create_date") String createDateStr) throws JSONException { if (!authenticate()) { throw new ForbiddenClientException(); } @@ -158,6 +168,7 @@ public class DocumentResource extends BaseResource { // Validate input data title = ValidationUtil.validateLength(title, "title", 1, 100, false); description = ValidationUtil.validateLength(description, "description", 0, 4000, true); + Date createDate = ValidationUtil.validateDate(createDateStr, "create_date", true); // Create the document DocumentDao documentDao = new DocumentDao(); @@ -165,6 +176,11 @@ public class DocumentResource extends BaseResource { document.setUserId(principal.getId()); document.setTitle(title); document.setDescription(description); + if (createDate == null) { + document.setCreateDate(new Date()); + } else { + document.setCreateDate(createDate); + } String documentId = documentDao.create(document); // Update tags @@ -190,7 +206,8 @@ public class DocumentResource extends BaseResource { @PathParam("id") String id, @FormParam("title") String title, @FormParam("description") String description, - @FormParam("tags[]") List tagList) throws JSONException { + @FormParam("tags[]") List tagList, + @FormParam("create_date") String createDateStr) throws JSONException { if (!authenticate()) { throw new ForbiddenClientException(); } @@ -198,6 +215,7 @@ public class DocumentResource extends BaseResource { // Validate input data title = ValidationUtil.validateLength(title, "title", 1, 100, false); description = ValidationUtil.validateLength(description, "description", 0, 4000, true); + Date createDate = ValidationUtil.validateDate(createDateStr, "create_date", true); // Get the document DocumentDao documentDao = new DocumentDao(); @@ -215,6 +233,9 @@ public class DocumentResource extends BaseResource { if (description != null) { document.setDescription(description); } + if (createDate != null) { + document.setCreateDate(createDate); + } // Update tags updateTagList(id, tagList); diff --git a/docs-web/src/main/webapp/js/controller/DocumentEdit.js b/docs-web/src/main/webapp/js/controller/DocumentEdit.js index 79a324fe..51af20d1 100644 --- a/docs-web/src/main/webapp/js/controller/DocumentEdit.js +++ b/docs-web/src/main/webapp/js/controller/DocumentEdit.js @@ -26,6 +26,9 @@ App.controller('DocumentEdit', function($scope, $q, $http, $state, $stateParams, $scope.edit = function() { var promise = null; var document = angular.copy($scope.document); + if (document.create_date instanceof Date) { + document.create_date = document.create_date.getTime(); + } // Extract ids from tags document.tags = _.pluck(document.tags, 'id'); diff --git a/docs-web/src/main/webapp/js/controller/DocumentView.js b/docs-web/src/main/webapp/js/controller/DocumentView.js index 0b0b8c76..02d3c56b 100644 --- a/docs-web/src/main/webapp/js/controller/DocumentView.js +++ b/docs-web/src/main/webapp/js/controller/DocumentView.js @@ -3,7 +3,7 @@ /** * Document view controller. */ -App.controller('DocumentView', function($rootScope, $scope, $state, $stateParams, $dialog, Restangular) { +App.controller('DocumentView', function($scope, $state, $stateParams, $dialog, Restangular) { // Load data from server $scope.document = Restangular.one('document', $stateParams.id).get(); @@ -25,7 +25,7 @@ App.controller('DocumentView', function($rootScope, $scope, $state, $stateParams */ $scope.loadFiles = function() { Restangular.one('file').getList('list', { id: $stateParams.id }).then(function(data) { - $rootScope.files = data.files; + $scope.files = data.files; }); }; $scope.loadFiles(); diff --git a/docs-web/src/main/webapp/js/controller/FileView.js b/docs-web/src/main/webapp/js/controller/FileView.js index e61e72fc..a90fb009 100644 --- a/docs-web/src/main/webapp/js/controller/FileView.js +++ b/docs-web/src/main/webapp/js/controller/FileView.js @@ -7,23 +7,28 @@ App.controller('FileView', function($dialog, $state, $stateParams) { var dialog = $dialog.dialog({ keyboard: true, templateUrl: 'partial/file.view.html', - controller: function($rootScope, $scope, $state, $stateParams) { + controller: function($scope, $state, $stateParams, Restangular) { $scope.id = $stateParams.fileId; - // Search current file - _.each($rootScope.files, function(value, key, list) { - if (value.id == $scope.id) { - $scope.file = value; - } + // Load files + Restangular.one('file').getList('list', { id: $stateParams.id }).then(function(data) { + $scope.files = data.files; + + // Search current file + _.each($scope.files, function(value, key, list) { + if (value.id == $scope.id) { + $scope.file = value; + } + }); }); /** * Navigate to the next file. */ $scope.nextFile = function() { - _.each($rootScope.files, function(value, key, list) { + _.each($scope.files, function(value, key, list) { if (value.id == $scope.id) { - var next = $rootScope.files[key + 1]; + var next = $scope.files[key + 1]; if (next) { dialog.close({}); $state.transitionTo('document.view.file', { id: $stateParams.id, fileId: next.id }); @@ -36,9 +41,9 @@ App.controller('FileView', function($dialog, $state, $stateParams) { * Navigate to the previous file. */ $scope.previousFile = function() { - _.each($rootScope.files, function(value, key, list) { + _.each($scope.files, function(value, key, list) { if (value.id == $scope.id) { - var previous = $rootScope.files[key - 1]; + var previous = $scope.files[key - 1]; if (previous) { dialog.close({}); $state.transitionTo('document.view.file', { id: $stateParams.id, fileId: previous.id }); diff --git a/docs-web/src/main/webapp/js/controller/Login.js b/docs-web/src/main/webapp/js/controller/Login.js index 072cab42..f159d211 100644 --- a/docs-web/src/main/webapp/js/controller/Login.js +++ b/docs-web/src/main/webapp/js/controller/Login.js @@ -3,9 +3,10 @@ /** * Login controller. */ -App.controller('Login', function($scope, $state, $dialog, User) { +App.controller('Login', function($scope, $rootScope, $state, $dialog, User) { $scope.login = function() { User.login($scope.user).then(function() { + $rootScope.userInfo = User.userInfo(true); $state.transitionTo('document.default'); }, function() { var title = 'Login failed'; diff --git a/docs-web/src/main/webapp/js/controller/Navigation.js b/docs-web/src/main/webapp/js/controller/Navigation.js index 3a7a6a64..b213b8d3 100644 --- a/docs-web/src/main/webapp/js/controller/Navigation.js +++ b/docs-web/src/main/webapp/js/controller/Navigation.js @@ -3,6 +3,6 @@ /** * Navigation controller. */ -App.controller('Navigation', function($scope, User) { - $scope.userInfo = User.userInfo(); +App.controller('Navigation', function($scope, $rootScope, User) { + $rootScope.userInfo = User.userInfo(); }); \ No newline at end of file diff --git a/docs-web/src/main/webapp/js/service/Tag.js b/docs-web/src/main/webapp/js/service/Tag.js index 567fe9d9..31dc780a 100644 --- a/docs-web/src/main/webapp/js/service/Tag.js +++ b/docs-web/src/main/webapp/js/service/Tag.js @@ -12,17 +12,7 @@ App.factory('Tag', function(Restangular) { * @param force If true, force reloading data */ tags: function(force) { - if (tags == null || force) { - tags = Restangular.one('tag/list').get(); - } - return tags; - }, - - /** - * Login an user. - */ - login: function(user) { - return Restangular.one('user').post('login', user); + return Restangular.one('tag/list').get(); } } }); \ No newline at end of file diff --git a/docs-web/src/main/webapp/lib/angular.ui-bootstrap.js b/docs-web/src/main/webapp/lib/angular.ui-bootstrap.js index 000e2309..93dbd667 100644 --- a/docs-web/src/main/webapp/lib/angular.ui-bootstrap.js +++ b/docs-web/src/main/webapp/lib/angular.ui-bootstrap.js @@ -1,5 +1,5 @@ -angular.module("ui.bootstrap", ["ui.bootstrap.tpls", "ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.datepicker","ui.bootstrap.dialog","ui.bootstrap.dropdownToggle","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.position","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]); -angular.module("ui.bootstrap.tpls", ["template/accordion/accordion-group.html","template/accordion/accordion.html","template/alert/alert.html","template/carousel/carousel.html","template/carousel/slide.html","template/datepicker/datepicker.html","template/dialog/message.html","template/pagination/pager.html","template/pagination/pagination.html","template/tooltip/tooltip-html-unsafe-popup.html","template/tooltip/tooltip-popup.html","template/popover/popover.html","template/progressbar/bar.html","template/progressbar/progress.html","template/rating/rating.html","template/tabs/tab.html","template/tabs/tabset.html","template/timepicker/timepicker.html","template/typeahead/typeahead.html"]); +angular.module("ui.bootstrap", ["ui.bootstrap.tpls", "ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dialog","ui.bootstrap.dropdownToggle","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]); +angular.module("ui.bootstrap.tpls", ["template/accordion/accordion-group.html","template/accordion/accordion.html","template/alert/alert.html","template/carousel/carousel.html","template/carousel/slide.html","template/datepicker/datepicker.html","template/datepicker/popup.html","template/dialog/message.html","template/pagination/pager.html","template/pagination/pagination.html","template/tooltip/tooltip-html-unsafe-popup.html","template/tooltip/tooltip-popup.html","template/popover/popover.html","template/progressbar/bar.html","template/progressbar/progress.html","template/rating/rating.html","template/tabs/tab.html","template/tabs/tabset.html","template/timepicker/timepicker.html","template/typeahead/typeahead-match.html","template/typeahead/typeahead-popup.html"]); angular.module('ui.bootstrap.transition', []) /** @@ -733,7 +733,101 @@ function CarouselDemoCtrl($scope) { }; }]); -angular.module('ui.bootstrap.datepicker', []) +angular.module('ui.bootstrap.position', []) + +/** + * A set of utility methods that can be use to retrieve position of DOM elements. + * It is meant to be used where we need to absolute-position DOM elements in + * relation to other, existing elements (this is the case for tooltips, popovers, + * typeahead suggestions etc.). + */ + .factory('$position', ['$document', '$window', function ($document, $window) { + + var mouseX, mouseY; + + $document.bind('mousemove', function mouseMoved(event) { + mouseX = event.pageX; + mouseY = event.pageY; + }); + + function getStyle(el, cssprop) { + if (el.currentStyle) { //IE + return el.currentStyle[cssprop]; + } else if ($window.getComputedStyle) { + return $window.getComputedStyle(el)[cssprop]; + } + // finally try and get inline style + return el.style[cssprop]; + } + + /** + * Checks if a given element is statically positioned + * @param element - raw DOM element + */ + function isStaticPositioned(element) { + return (getStyle(element, "position") || 'static' ) === 'static'; + } + + /** + * returns the closest, non-statically positioned parentOffset of a given element + * @param element + */ + var parentOffsetEl = function (element) { + var docDomEl = $document[0]; + var offsetParent = element.offsetParent || docDomEl; + while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) { + offsetParent = offsetParent.offsetParent; + } + return offsetParent || docDomEl; + }; + + return { + /** + * Provides read-only equivalent of jQuery's position function: + * http://api.jquery.com/position/ + */ + position: function (element) { + var elBCR = this.offset(element); + var offsetParentBCR = { top: 0, left: 0 }; + var offsetParentEl = parentOffsetEl(element[0]); + if (offsetParentEl != $document[0]) { + offsetParentBCR = this.offset(angular.element(offsetParentEl)); + offsetParentBCR.top += offsetParentEl.clientTop; + offsetParentBCR.left += offsetParentEl.clientLeft; + } + + return { + width: element.prop('offsetWidth'), + height: element.prop('offsetHeight'), + top: elBCR.top - offsetParentBCR.top, + left: elBCR.left - offsetParentBCR.left + }; + }, + + /** + * Provides read-only equivalent of jQuery's offset function: + * http://api.jquery.com/offset/ + */ + offset: function (element) { + var boundingClientRect = element[0].getBoundingClientRect(); + return { + width: element.prop('offsetWidth'), + height: element.prop('offsetHeight'), + top: boundingClientRect.top + ($window.pageYOffset || $document[0].body.scrollTop), + left: boundingClientRect.left + ($window.pageXOffset || $document[0].body.scrollLeft) + }; + }, + + /** + * Provides the coordinates of the mouse + */ + mouse: function () { + return {x: mouseX, y: mouseY}; + } + }; + }]); + +angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.position']) .constant('datepickerConfig', { dayFormat: 'dd', @@ -744,31 +838,139 @@ angular.module('ui.bootstrap.datepicker', []) monthTitleFormat: 'yyyy', showWeeks: true, startingDay: 0, - yearRange: 20 + yearRange: 20, + minDate: null, + maxDate: null }) +.controller('DatepickerController', ['$scope', '$attrs', 'dateFilter', 'datepickerConfig', function($scope, $attrs, dateFilter, dtConfig) { + var format = { + day: getValue($attrs.dayFormat, dtConfig.dayFormat), + month: getValue($attrs.monthFormat, dtConfig.monthFormat), + year: getValue($attrs.yearFormat, dtConfig.yearFormat), + dayHeader: getValue($attrs.dayHeaderFormat, dtConfig.dayHeaderFormat), + dayTitle: getValue($attrs.dayTitleFormat, dtConfig.dayTitleFormat), + monthTitle: getValue($attrs.monthTitleFormat, dtConfig.monthTitleFormat) + }, + startingDay = getValue($attrs.startingDay, dtConfig.startingDay), + yearRange = getValue($attrs.yearRange, dtConfig.yearRange); + + this.minDate = dtConfig.minDate ? new Date(dtConfig.minDate) : null; + this.maxDate = dtConfig.maxDate ? new Date(dtConfig.maxDate) : null; + + function getValue(value, defaultValue) { + return angular.isDefined(value) ? $scope.$parent.$eval(value) : defaultValue; + } + + function getDaysInMonth( year, month ) { + return new Date(year, month, 0).getDate(); + } + + function getDates(startDate, n) { + var dates = new Array(n); + var current = startDate, i = 0; + while (i < n) { + dates[i++] = new Date(current); + current.setDate( current.getDate() + 1 ); + } + return dates; + } + + function makeDate(date, format, isSelected, isSecondary) { + return { date: date, label: dateFilter(date, format), selected: !!isSelected, secondary: !!isSecondary }; + } + + this.modes = [ + { + name: 'day', + getVisibleDates: function(date, selected) { + var year = date.getFullYear(), month = date.getMonth(), firstDayOfMonth = new Date(year, month, 1); + var difference = startingDay - firstDayOfMonth.getDay(), + numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference, + firstDate = new Date(firstDayOfMonth), numDates = 0; + + if ( numDisplayedFromPreviousMonth > 0 ) { + firstDate.setDate( - numDisplayedFromPreviousMonth + 1 ); + numDates += numDisplayedFromPreviousMonth; // Previous + } + numDates += getDaysInMonth(year, month + 1); // Current + numDates += (7 - numDates % 7) % 7; // Next + + var days = getDates(firstDate, numDates), labels = new Array(7); + for (var i = 0; i < numDates; i ++) { + var dt = new Date(days[i]); + days[i] = makeDate(dt, format.day, (selected && selected.getDate() === dt.getDate() && selected.getMonth() === dt.getMonth() && selected.getFullYear() === dt.getFullYear()), dt.getMonth() !== month); + } + for (var j = 0; j < 7; j++) { + labels[j] = dateFilter(days[j].date, format.dayHeader); + } + return { objects: days, title: dateFilter(date, format.dayTitle), labels: labels }; + }, + compare: function(date1, date2) { + return (new Date( date1.getFullYear(), date1.getMonth(), date1.getDate() ) - new Date( date2.getFullYear(), date2.getMonth(), date2.getDate() ) ); + }, + split: 7, + step: { months: 1 } + }, + { + name: 'month', + getVisibleDates: function(date, selected) { + var months = new Array(12), year = date.getFullYear(); + for ( var i = 0; i < 12; i++ ) { + var dt = new Date(year, i, 1); + months[i] = makeDate(dt, format.month, (selected && selected.getMonth() === i && selected.getFullYear() === year)); + } + return { objects: months, title: dateFilter(date, format.monthTitle) }; + }, + compare: function(date1, date2) { + return new Date( date1.getFullYear(), date1.getMonth() ) - new Date( date2.getFullYear(), date2.getMonth() ); + }, + split: 3, + step: { years: 1 } + }, + { + name: 'year', + getVisibleDates: function(date, selected) { + var years = new Array(yearRange), year = date.getFullYear(), startYear = parseInt((year - 1) / yearRange, 10) * yearRange + 1; + for ( var i = 0; i < yearRange; i++ ) { + var dt = new Date(startYear + i, 0, 1); + years[i] = makeDate(dt, format.year, (selected && selected.getFullYear() === dt.getFullYear())); + } + return { objects: years, title: [years[0].label, years[yearRange - 1].label].join(' - ') }; + }, + compare: function(date1, date2) { + return date1.getFullYear() - date2.getFullYear(); + }, + split: 5, + step: { years: yearRange } + } + ]; + + this.isDisabled = function(date, mode) { + var currentMode = this.modes[mode || 0]; + return ((this.minDate && currentMode.compare(date, this.minDate) < 0) || (this.maxDate && currentMode.compare(date, this.maxDate) > 0) || ($scope.dateDisabled && $scope.dateDisabled({date: date, mode: currentMode.name}))); + }; +}]) + .directive( 'datepicker', ['dateFilter', '$parse', 'datepickerConfig', function (dateFilter, $parse, datepickerConfig) { return { restrict: 'EA', replace: true, + templateUrl: 'template/datepicker/datepicker.html', scope: { - model: '=ngModel', dateDisabled: '&' }, - templateUrl: 'template/datepicker/datepicker.html', - link: function(scope, element, attrs) { - scope.mode = 'day'; // Initial mode + require: ['datepicker', '?^ngModel'], + controller: 'DatepickerController', + link: function(scope, element, attrs, ctrls) { + var datepickerCtrl = ctrls[0], ngModel = ctrls[1]; + + if (!ngModel) { + return; // do nothing if no ng-model + } // Configuration parameters - var selected = new Date(), showWeeks, minDate, maxDate, format = {}; - format.day = angular.isDefined(attrs.dayFormat) ? scope.$eval(attrs.dayFormat) : datepickerConfig.dayFormat; - format.month = angular.isDefined(attrs.monthFormat) ? scope.$eval(attrs.monthFormat) : datepickerConfig.monthFormat; - format.year = angular.isDefined(attrs.yearFormat) ? scope.$eval(attrs.yearFormat) : datepickerConfig.yearFormat; - format.dayHeader = angular.isDefined(attrs.dayHeaderFormat) ? scope.$eval(attrs.dayHeaderFormat) : datepickerConfig.dayHeaderFormat; - format.dayTitle = angular.isDefined(attrs.dayTitleFormat) ? scope.$eval(attrs.dayTitleFormat) : datepickerConfig.dayTitleFormat; - format.monthTitle = angular.isDefined(attrs.monthTitleFormat) ? scope.$eval(attrs.monthTitleFormat) : datepickerConfig.monthTitleFormat; - var startingDay = angular.isDefined(attrs.startingDay) ? scope.$eval(attrs.startingDay) : datepickerConfig.startingDay; - var yearRange = angular.isDefined(attrs.yearRange) ? scope.$eval(attrs.yearRange) : datepickerConfig.yearRange; + var mode = 0, selected = new Date(), showWeeks = datepickerConfig.showWeeks; if (attrs.showWeeks) { scope.$parent.$watch($parse(attrs.showWeeks), function(value) { @@ -776,174 +978,282 @@ angular.module('ui.bootstrap.datepicker', []) updateShowWeekNumbers(); }); } else { - showWeeks = datepickerConfig.showWeeks; updateShowWeekNumbers(); } if (attrs.min) { scope.$parent.$watch($parse(attrs.min), function(value) { - minDate = new Date(value); + datepickerCtrl.minDate = value ? new Date(value) : null; refill(); }); } if (attrs.max) { scope.$parent.$watch($parse(attrs.max), function(value) { - maxDate = new Date(value); + datepickerCtrl.maxDate = value ? new Date(value) : null; refill(); }); } - function updateCalendar (rows, labels, title) { - scope.rows = rows; - scope.labels = labels; - scope.title = title; - } - - // Define whether the week number are visible function updateShowWeekNumbers() { - scope.showWeekNumbers = ( scope.mode === 'day' && showWeeks ); - } - - function compare( date1, date2 ) { - if ( scope.mode === 'year') { - return date2.getFullYear() - date1.getFullYear(); - } else if ( scope.mode === 'month' ) { - return new Date( date2.getFullYear(), date2.getMonth() ) - new Date( date1.getFullYear(), date1.getMonth() ); - } else if ( scope.mode === 'day' ) { - return (new Date( date2.getFullYear(), date2.getMonth(), date2.getDate() ) - new Date( date1.getFullYear(), date1.getMonth(), date1.getDate() ) ); - } - } - - function isDisabled(date) { - return ((minDate && compare(date, minDate) > 0) || (maxDate && compare(date, maxDate) < 0) || (scope.dateDisabled && scope.dateDisabled({ date: date, mode: scope.mode }))); + scope.showWeekNumbers = mode === 0 && showWeeks; } // Split array into smaller arrays - var split = function(a, size) { + function split(arr, size) { var arrays = []; - while (a.length > 0) { - arrays.push(a.splice(0, size)); + while (arr.length > 0) { + arrays.push(arr.splice(0, size)); } return arrays; - }; - var getDaysInMonth = function( year, month ) { - return new Date(year, month + 1, 0).getDate(); - }; + } - var fill = { - day: function() { - var days = [], labels = [], lastDate = null; + function refill( updateSelected ) { + var date = null, valid = true; - function addDays( dt, n, isCurrentMonth ) { - for (var i =0; i < n; i ++) { - days.push( {date: new Date(dt), isCurrent: isCurrentMonth, isSelected: isSelected(dt), label: dateFilter(dt, format.day), disabled: isDisabled(dt) } ); - dt.setDate( dt.getDate() + 1 ); - } - lastDate = dt; - } + if ( ngModel.$modelValue ) { + date = new Date( ngModel.$modelValue ); - var d = new Date(selected); - d.setDate(1); - - var difference = startingDay - d.getDay(); - var numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference; - - if ( numDisplayedFromPreviousMonth > 0 ) { - d.setDate( - numDisplayedFromPreviousMonth + 1 ); - addDays(d, numDisplayedFromPreviousMonth, false); - } - addDays(lastDate || d, getDaysInMonth(selected.getFullYear(), selected.getMonth()), true); - addDays(lastDate, (7 - days.length % 7) % 7, false); - - // Day labels - for (i = 0; i < 7; i++) { - labels.push( dateFilter(days[i].date, format.dayHeader) ); - } - updateCalendar( split( days, 7 ), labels, dateFilter(selected, format.dayTitle) ); - }, - month: function() { - var months = [], i = 0, year = selected.getFullYear(); - while ( i < 12 ) { - var dt = new Date(year, i++, 1); - months.push( {date: dt, isCurrent: true, isSelected: isSelected(dt), label: dateFilter(dt, format.month), disabled: isDisabled(dt)} ); - } - updateCalendar( split( months, 3 ), [], dateFilter(selected, format.monthTitle) ); - }, - year: function() { - var years = [], year = parseInt((selected.getFullYear() - 1) / yearRange, 10) * yearRange + 1; - for ( var i = 0; i < yearRange; i++ ) { - var dt = new Date(year + i, 0, 1); - years.push( {date: dt, isCurrent: true, isSelected: isSelected(dt), label: dateFilter(dt, format.year), disabled: isDisabled(dt)} ); - } - var title = years[0].label + ' - ' + years[years.length - 1].label; - updateCalendar( split( years, 5 ), [], title ); - } - }; - var refill = function() { - fill[scope.mode](); - }; - var isSelected = function( dt ) { - if ( scope.model && scope.model.getFullYear() === dt.getFullYear() ) { - if ( scope.mode === 'year' ) { - return true; - } - if ( scope.model.getMonth() === dt.getMonth() ) { - return ( scope.mode === 'month' || (scope.mode === 'day' && scope.model.getDate() === dt.getDate()) ); + if ( isNaN(date) ) { + valid = false; + console.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); + } else if ( updateSelected ) { + selected = date; } } - return false; - }; + ngModel.$setValidity('date', valid); - scope.$watch('model', function ( dt, olddt ) { - if ( angular.isDate(dt) ) { - selected = angular.copy(dt); - } + var currentMode = datepickerCtrl.modes[mode], data = currentMode.getVisibleDates(selected, date); + angular.forEach(data.objects, function(obj) { + obj.disabled = datepickerCtrl.isDisabled(obj.date, mode); + }); - if ( ! angular.equals(dt, olddt) ) { - refill(); - } - }); - scope.$watch('mode', function() { + ngModel.$setValidity('date-disabled', (!date || !datepickerCtrl.isDisabled(date))); + + scope.rows = split(data.objects, currentMode.split); + scope.labels = data.labels || []; + scope.title = data.title; + } + + function setMode(value) { + mode = value; updateShowWeekNumbers(); refill(); - }); + } - scope.select = function( dt ) { - selected = new Date(dt); + ngModel.$render = function() { + refill( true ); + }; - if ( scope.mode === 'year' ) { - scope.mode = 'month'; - selected.setFullYear( dt.getFullYear() ); - } else if ( scope.mode === 'month' ) { - scope.mode = 'day'; - selected.setMonth( dt.getMonth() ); - } else if ( scope.mode === 'day' ) { - scope.model = new Date(selected); + scope.select = function( date ) { + if ( mode === 0 ) { + var dt = new Date( ngModel.$modelValue ); + dt.setFullYear( date.getFullYear(), date.getMonth(), date.getDate() ); + ngModel.$setViewValue( dt ); + refill( true ); + } else { + selected = date; + setMode( mode - 1 ); } }; - scope.move = function(step) { - if (scope.mode === 'day') { - selected.setMonth( selected.getMonth() + step ); - } else if (scope.mode === 'month') { - selected.setFullYear( selected.getFullYear() + step ); - } else if (scope.mode === 'year') { - selected.setFullYear( selected.getFullYear() + step * yearRange ); - } + scope.move = function(direction) { + var step = datepickerCtrl.modes[mode].step; + selected.setMonth( selected.getMonth() + direction * (step.months || 0) ); + selected.setFullYear( selected.getFullYear() + direction * (step.years || 0) ); refill(); }; scope.toggleMode = function() { - scope.mode = ( scope.mode === 'day' ) ? 'month' : ( scope.mode === 'month' ) ? 'year' : 'day'; + setMode( (mode + 1) % datepickerCtrl.modes.length ); }; scope.getWeekNumber = function(row) { - if ( scope.mode !== 'day' || ! scope.showWeekNumbers || row.length !== 7 ) { - return; + return ( mode === 0 && scope.showWeekNumbers && row.length === 7 ) ? getISO8601WeekNumber(row[0].date) : null; + }; + + function getISO8601WeekNumber(date) { + var checkDate = new Date(date); + checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday + var time = checkDate.getTime(); + checkDate.setMonth(0); // Compare with Jan 1 + checkDate.setDate(1); + return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; + } + } + }; +}]) + +.constant('datepickerPopupConfig', { + dateFormat: 'yyyy-MM-dd', + closeOnDateSelection: true +}) + +.directive('datepickerPopup', ['$compile', '$parse', '$document', '$position', 'dateFilter', 'datepickerPopupConfig', +function ($compile, $parse, $document, $position, dateFilter, datepickerPopupConfig) { + return { + restrict: 'EA', + require: 'ngModel', + link: function(originalScope, element, attrs, ngModel) { + + var closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection; + var dateFormat = attrs.datepickerPopup || datepickerPopupConfig.dateFormat; + + // create a child scope for the datepicker directive so we are not polluting original scope + var scope = originalScope.$new(); + originalScope.$on('$destroy', function() { + scope.$destroy(); + }); + + function formatDate(value) { + return (value) ? dateFilter(value, dateFormat) : null; + } + ngModel.$formatters.push(formatDate); + + // TODO: reverse from dateFilter string to Date object + function parseDate(value) { + if ( value ) { + var date = new Date(value); + if (!isNaN(date)) { + return date; + } + } + return value; + } + ngModel.$parsers.push(parseDate); + + var getIsOpen, setIsOpen; + if ( attrs.open ) { + getIsOpen = $parse(attrs.open); + setIsOpen = getIsOpen.assign; + + originalScope.$watch(getIsOpen, function updateOpen(value) { + scope.isOpen = !! value; + }); + } + scope.isOpen = getIsOpen ? getIsOpen(originalScope) : false; // Initial state + + function setOpen( value ) { + if (setIsOpen) { + setIsOpen(originalScope, !!value); + } else { + scope.isOpen = !!value; + } + } + + var documentClickBind = function(event) { + if (scope.isOpen && event.target !== element[0]) { + scope.$apply(function() { + setOpen(false); + }); + } + }; + + var elementFocusBind = function() { + scope.$apply(function() { + setOpen( true ); + }); + }; + + // popup element used to display calendar + var popupEl = angular.element(''); + popupEl.attr({ + 'ng-model': 'date', + 'ng-change': 'dateSelection()' + }); + var datepickerEl = popupEl.find('datepicker'); + if (attrs.datepickerOptions) { + datepickerEl.attr(angular.extend({}, originalScope.$eval(attrs.datepickerOptions))); + } + + var $setModelValue = $parse(attrs.ngModel).assign; + + // Inner change + scope.dateSelection = function() { + $setModelValue(originalScope, scope.date); + if (closeOnDateSelection) { + setOpen( false ); + } + }; + + // Outter change + scope.$watch(function() { + return ngModel.$modelValue; + }, function(value) { + if (angular.isString(value)) { + var date = parseDate(value); + + if (value && !date) { + $setModelValue(originalScope, null); + throw new Error(value + ' cannot be parsed to a date object.'); + } else { + value = date; + } + } + scope.date = value; + updatePosition(); + }); + + function addWatchableAttribute(attribute, scopeProperty, datepickerAttribute) { + if (attribute) { + originalScope.$watch($parse(attribute), function(value){ + scope[scopeProperty] = value; + }); + datepickerEl.attr(datepickerAttribute || scopeProperty, scopeProperty); + } + } + addWatchableAttribute(attrs.min, 'min'); + addWatchableAttribute(attrs.max, 'max'); + if (attrs.showWeeks) { + addWatchableAttribute(attrs.showWeeks, 'showWeeks', 'show-weeks'); + } else { + scope.showWeeks = true; + datepickerEl.attr('show-weeks', 'showWeeks'); + } + if (attrs.dateDisabled) { + datepickerEl.attr('date-disabled', attrs.dateDisabled); + } + + function updatePosition() { + scope.position = $position.position(element); + scope.position.top = scope.position.top + element.prop('offsetHeight'); + } + + scope.$watch('isOpen', function(value) { + if (value) { + updatePosition(); + $document.bind('click', documentClickBind); + element.unbind('focus', elementFocusBind); + element.focus(); + } else { + $document.unbind('click', documentClickBind); + element.bind('focus', elementFocusBind); } - var index = ( startingDay > 4 ) ? 11 - startingDay : 4 - startingDay; // Thursday - var d = new Date( row[ index ].date ); - d.setHours(0, 0, 0); - return Math.ceil((((d - new Date(d.getFullYear(), 0, 1)) / 86400000) + 1) / 7); // 86400000 = 1000*60*60*24; + if ( setIsOpen ) { + setIsOpen(originalScope, value); + } + }); + + scope.today = function() { + $setModelValue(originalScope, new Date()); }; + scope.clear = function() { + $setModelValue(originalScope, null); + }; + + element.after($compile(popupEl)(scope)); + } + }; +}]) + +.directive('datepickerPopupWrap', [function() { + return { + restrict:'E', + replace: true, + transclude: true, + templateUrl: 'template/datepicker/popup.html', + link:function (scope, element, attrs) { + element.bind('click', function(event) { + event.preventDefault(); + event.stopPropagation(); + }); } }; }]); @@ -1523,100 +1833,6 @@ angular.module('ui.bootstrap.pagination', []) }; }]); -angular.module('ui.bootstrap.position', []) - -/** - * A set of utility methods that can be use to retrieve position of DOM elements. - * It is meant to be used where we need to absolute-position DOM elements in - * relation to other, existing elements (this is the case for tooltips, popovers, - * typeahead suggestions etc.). - */ - .factory('$position', ['$document', '$window', function ($document, $window) { - - var mouseX, mouseY; - - $document.bind('mousemove', function mouseMoved(event) { - mouseX = event.pageX; - mouseY = event.pageY; - }); - - function getStyle(el, cssprop) { - if (el.currentStyle) { //IE - return el.currentStyle[cssprop]; - } else if ($window.getComputedStyle) { - return $window.getComputedStyle(el)[cssprop]; - } - // finally try and get inline style - return el.style[cssprop]; - } - - /** - * Checks if a given element is statically positioned - * @param element - raw DOM element - */ - function isStaticPositioned(element) { - return (getStyle(element, "position") || 'static' ) === 'static'; - } - - /** - * returns the closest, non-statically positioned parentOffset of a given element - * @param element - */ - var parentOffsetEl = function (element) { - var docDomEl = $document[0]; - var offsetParent = element.offsetParent || docDomEl; - while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) { - offsetParent = offsetParent.offsetParent; - } - return offsetParent || docDomEl; - }; - - return { - /** - * Provides read-only equivalent of jQuery's position function: - * http://api.jquery.com/position/ - */ - position: function (element) { - var elBCR = this.offset(element); - var offsetParentBCR = { top: 0, left: 0 }; - var offsetParentEl = parentOffsetEl(element[0]); - if (offsetParentEl != $document[0]) { - offsetParentBCR = this.offset(angular.element(offsetParentEl)); - offsetParentBCR.top += offsetParentEl.clientTop; - offsetParentBCR.left += offsetParentEl.clientLeft; - } - - return { - width: element.prop('offsetWidth'), - height: element.prop('offsetHeight'), - top: elBCR.top - offsetParentBCR.top, - left: elBCR.left - offsetParentBCR.left - }; - }, - - /** - * Provides read-only equivalent of jQuery's offset function: - * http://api.jquery.com/offset/ - */ - offset: function (element) { - var boundingClientRect = element[0].getBoundingClientRect(); - return { - width: element.prop('offsetWidth'), - height: element.prop('offsetHeight'), - top: boundingClientRect.top + ($window.pageYOffset || $document[0].body.scrollTop), - left: boundingClientRect.left + ($window.pageXOffset || $document[0].body.scrollLeft) - }; - }, - - /** - * Provides the coordinates of the mouse - */ - mouse: function () { - return {x: mouseX, y: mouseY}; - } - }; - }]); - /** * The following features are still outstanding: animation as a * function, placement as a function, inside, support for more triggers than @@ -2098,13 +2314,15 @@ angular.module('ui.bootstrap.rating', []) return { restrict: 'EA', scope: { - value: '=' + value: '=', + onHover: '&', + onLeave: '&' }, templateUrl: 'template/rating/rating.html', replace: true, link: function(scope, element, attrs) { - var maxRange = angular.isDefined(attrs.max) ? scope.$eval(attrs.max) : ratingConfig.max; + var maxRange = angular.isDefined(attrs.max) ? scope.$parent.$eval(attrs.max) : ratingConfig.max; scope.range = []; for (var i = 1; i <= maxRange; i++) { @@ -2121,10 +2339,12 @@ angular.module('ui.bootstrap.rating', []) if ( ! scope.readonly ) { scope.val = value; } + scope.onHover({value: value}); }; scope.reset = function() { scope.val = angular.copy(scope.value); + scope.onLeave(); }; scope.reset(); @@ -2160,6 +2380,11 @@ angular.module('ui.bootstrap.tabs', []) .controller('TabsetController', ['$scope', '$element', function TabsetCtrl($scope, $element) { + + //Expose the outer scope for tab content compiling, so it can compile + //on outer scope like it should + this.$outerScope = $scope.$parent; + var ctrl = this, tabs = ctrl.tabs = $scope.tabs = []; @@ -2218,6 +2443,7 @@ function TabsetCtrl($scope, $element) { return { restrict: 'EA', transclude: true, + replace: true, scope: {}, controller: 'TabsetController', templateUrl: 'template/tabs/tabset.html', @@ -2423,15 +2649,17 @@ function($parse, $http, $templateCache, $compile) { }; }]) -.directive('tabContentTransclude', ['$parse', function($parse) { +.directive('tabContentTransclude', ['$compile', '$parse', function($compile, $parse) { return { restrict: 'A', require: '^tabset', link: function(scope, elm, attrs, tabsetCtrl) { + var outerScope = tabsetCtrl.$outerScope; scope.$watch($parse(attrs.tabContentTransclude), function(tab) { elm.html(''); if (tab) { elm.append(tab.contentElement); + $compile(tab.contentElement)(outerScope); } }); } @@ -2633,10 +2861,8 @@ angular.module('ui.bootstrap.timepicker', []) function addMinutes( minutes ) { var dt = new Date( selected.getTime() + minutes * 60000 ); - if ( dt.getDate() !== selected.getDate()) { - dt.setDate( dt.getDate() - 1 ); - } - selected.setTime( dt.getTime() ); + selected.setHours( dt.getHours() ); + selected.setMinutes( dt.getMinutes() ); scope.model = new Date( selected ); } @@ -2697,7 +2923,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) require:'ngModel', link:function (originalScope, element, attrs, modelCtrl) { - var selected; + //SUPPORTED ATTRIBUTES (OPTIONS) //minimal no of characters that needs to be entered before typeahead kicks-in var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1; @@ -2705,16 +2931,26 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) //minimal wait time after last character typed before typehead kicks-in var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0; - //expressions used by typeahead - var parserResult = typeaheadParser.parse(attrs.typeahead); - //should it restrict model values to the ones selected from the popup only? var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false; + //binding to a variable that indicates if matches are being retrieved asynchronously var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop; + //a callback executed when a match is selected var onSelectCallback = $parse(attrs.typeaheadOnSelect); + var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; + + //INTERNAL VARIABLES + + //model setter executed upon match selection + var $setModelValue = $parse(attrs.ngModel).assign; + + //expressions used by typeahead + var parserResult = typeaheadParser.parse(attrs.typeahead); + + //pop-up element used to display matches var popUpEl = angular.element(''); popUpEl.attr({ @@ -2724,6 +2960,10 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) query: 'query', position: 'position' }); + //custom item template + if (angular.isDefined(attrs.typeaheadTemplateUrl)) { + popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); + } //create a child scope for the typeahead directive so we are not polluting original scope //with typeahead-specific data (matches, query etc.) @@ -2783,55 +3023,69 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) //we need to propagate user's query so we can higlight matches scope.query = undefined; + //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later + var timeoutPromise; + //plug into $parsers pipeline to open a typeahead on view changes initiated from DOM //$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue modelCtrl.$parsers.push(function (inputValue) { - var timeoutId; - resetMatches(); - if (selected) { - return inputValue; - } else { - if (inputValue && inputValue.length >= minSearch) { - if (waitTime > 0) { - if (timeoutId) { - $timeout.cancel(timeoutId);//cancel previous timeout - } - timeoutId = $timeout(function () { - getMatchesAsync(inputValue); - }, waitTime); - } else { - getMatchesAsync(inputValue); + if (inputValue && inputValue.length >= minSearch) { + if (waitTime > 0) { + if (timeoutPromise) { + $timeout.cancel(timeoutPromise);//cancel previous timeout } + timeoutPromise = $timeout(function () { + getMatchesAsync(inputValue); + }, waitTime); + } else { + getMatchesAsync(inputValue); } } return isEditable ? inputValue : undefined; }); - modelCtrl.$render = function () { + modelCtrl.$formatters.push(function (modelValue) { + + var candidateViewValue, emptyViewValue; var locals = {}; - locals[parserResult.itemName] = selected || modelCtrl.$viewValue; - element.val(parserResult.viewMapper(scope, locals) || modelCtrl.$viewValue); - selected = undefined; - }; + + if (inputFormatter) { + + locals['$model'] = modelValue; + return inputFormatter(originalScope, locals); + + } else { + locals[parserResult.itemName] = modelValue; + + //it might happen that we don't have enough info to properly render input value + //we need to check for this situation and simply return model value if we can't apply custom formatting + candidateViewValue = parserResult.viewMapper(originalScope, locals); + emptyViewValue = parserResult.viewMapper(originalScope, {}); + + return candidateViewValue!== emptyViewValue ? candidateViewValue : modelValue; + } + }); scope.select = function (activeIdx) { //called from within the $digest() cycle var locals = {}; var model, item; - locals[parserResult.itemName] = item = selected = scope.matches[activeIdx].model; - model = parserResult.modelMapper(scope, locals); - modelCtrl.$setViewValue(model); - modelCtrl.$render(); - onSelectCallback(scope, { + locals[parserResult.itemName] = item = scope.matches[activeIdx].model; + model = parserResult.modelMapper(originalScope, locals); + $setModelValue(originalScope, model); + + onSelectCallback(originalScope, { $item: item, $model: model, - $label: parserResult.viewMapper(scope, locals) + $label: parserResult.viewMapper(originalScope, locals) }); + //return focus to the input element if a mach was selected via a mouse click event + resetMatches(); element[0].focus(); }; @@ -2888,9 +3142,11 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) select:'&' }, replace:true, - templateUrl:'template/typeahead/typeahead.html', + templateUrl:'template/typeahead/typeahead-popup.html', link:function (scope, element, attrs) { + scope.templateUrl = attrs.templateUrl; + scope.isOpen = function () { return scope.matches.length > 0; }; @@ -2910,6 +3166,22 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) }; }) + .directive('typeaheadMatch', ['$http', '$templateCache', '$compile', '$parse', function ($http, $templateCache, $compile, $parse) { + return { + restrict:'E', + scope:{ + match:'=', + query:'=' + }, + link:function (scope, element, attrs) { + var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'template/typeahead/typeahead-match.html'; + $http.get(tplUrl, {cache: $templateCache}).success(function(tplContent){ + element.replaceWith($compile(tplContent.trim())(scope)); + }); + } + }; + }]) + .filter('typeaheadHighlight', function() { function escapeRegexp(queryToEscape) { @@ -2920,7 +3192,6 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) return query ? matchItem.replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : query; }; }); - angular.module("template/accordion/accordion-group.html", []).run(["$templateCache", function($templateCache) { $templateCache.put("template/accordion/accordion-group.html", "
\n" + @@ -2971,7 +3242,7 @@ angular.module("template/carousel/slide.html", []).run(["$templateCache", functi angular.module("template/datepicker/datepicker.html", []).run(["$templateCache", function($templateCache) { $templateCache.put("template/datepicker/datepicker.html", - "\n" + + "
\n" + " \n" + " \n" + " \n" + @@ -2987,7 +3258,7 @@ angular.module("template/datepicker/datepicker.html", []).run(["$templateCache", " \n" + " \n" + " \n" + " \n" + " \n" + @@ -2995,6 +3266,22 @@ angular.module("template/datepicker/datepicker.html", []).run(["$templateCache", ""); }]); +angular.module("template/datepicker/popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/datepicker/popup.html", + "
    \n" + + "
  • \n" + + "
  • \n" + + "
  • \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
  • \n" + + "
"); +}]); + angular.module("template/dialog/message.html", []).run(["$templateCache", function($templateCache) { $templateCache.put("template/dialog/message.html", "
\n" + @@ -3009,16 +3296,6 @@ angular.module("template/dialog/message.html", []).run(["$templateCache", functi ""); }]); -angular.module("template/modal/backdrop.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/modal/backdrop.html", - "
"); -}]); - -angular.module("template/modal/window.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/modal/window.html", - "
"); -}]); - angular.module("template/pagination/pager.html", []).run(["$templateCache", function($templateCache) { $templateCache.put("template/pagination/pager.html", "
\n" + @@ -3083,14 +3360,7 @@ angular.module("template/rating/rating.html", []).run(["$templateCache", functio $templateCache.put("template/rating/rating.html", "\n" + " val}\">\n" + - "\n" + - ""); -}]); - -angular.module("template/tabs/pane.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/tabs/pane.html", - "
\n" + - ""); + ""); }]); angular.module("template/tabs/tab.html", []).run(["$templateCache", function($templateCache) { @@ -3101,19 +3371,6 @@ angular.module("template/tabs/tab.html", []).run(["$templateCache", function($te ""); }]); -angular.module("template/tabs/tabs.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/tabs/tabs.html", - "
\n" + - " \n" + - "
\n" + - "
\n" + - ""); -}]); - angular.module("template/tabs/tabset.html", []).run(["$templateCache", function($templateCache) { $templateCache.put("template/tabs/tabset.html", "\n" + @@ -3124,7 +3381,7 @@ angular.module("template/tabs/tabset.html", []).run(["$templateCache", function( "
\n" + + " tab-content-transclude=\"tab\">\n" + "
\n" + "
\n" + "
\n" + @@ -3155,11 +3412,16 @@ angular.module("template/timepicker/timepicker.html", []).run(["$templateCache", "
{{ getWeekNumber(row) }}\n" + - " \n" + + " \n" + "
"); }]); -angular.module("template/typeahead/typeahead.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/typeahead/typeahead.html", +angular.module("template/typeahead/typeahead-match.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/typeahead/typeahead-match.html", + ""); +}]); + +angular.module("template/typeahead/typeahead-popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/typeahead/typeahead-popup.html", "
    \n" + - "
  • \n" + - " \n" + + "
  • \n" + + " \n" + "
  • \n" + "
"); -}]); \ No newline at end of file +}]); diff --git a/docs-web/src/main/webapp/partial/document.edit.html b/docs-web/src/main/webapp/partial/document.edit.html index 765f5edd..a5a8b6af 100644 --- a/docs-web/src/main/webapp/partial/document.edit.html +++ b/docs-web/src/main/webapp/partial/document.edit.html @@ -11,6 +11,12 @@
+
+ +
+ +
+
diff --git a/docs-web/src/main/webapp/partial/document.html b/docs-web/src/main/webapp/partial/document.html index 31ddf0a9..c88d02a8 100644 --- a/docs-web/src/main/webapp/partial/document.html +++ b/docs-web/src/main/webapp/partial/document.html @@ -18,7 +18,7 @@ {{ document.title }} - {{ document.create_date | date: 'short' }} + {{ document.create_date | date: 'yyyy-MM-dd' }} diff --git a/docs-web/src/main/webapp/partial/document.view.html b/docs-web/src/main/webapp/partial/document.view.html index 01230456..126cae11 100644 --- a/docs-web/src/main/webapp/partial/document.view.html +++ b/docs-web/src/main/webapp/partial/document.view.html @@ -6,7 +6,7 @@