2 angular-xeditable - 0.1.9
3 Edit-in-place for angular.js
7 * Angular-xeditable module
10 angular.module('xeditable', [])
16 * @namespace editable-options
18 //todo: maybe better have editableDefaults, not options...
19 .value('editableOptions', {
21 * Theme. Possible values `bs3`, `bs2`, `default`.
24 * @memberOf editable-options
28 * Icon Set. Possible values `font-awesome`, `default`.
30 * @var {string} icon set
31 * @memberOf editable-options
35 * Whether to show buttons for single editalbe element.
36 * Possible values `right` (default), `no`.
38 * @var {string} buttons
39 * @memberOf editable-options
43 * Default value for `blur` attribute of single editable element.
44 * Can be `cancel|submit|ignore`.
46 * @var {string} blurElem
47 * @memberOf editable-options
51 * Default value for `blur` attribute of editable form.
52 * Can be `cancel|submit|ignore`.
54 * @var {string} blurForm
55 * @memberOf editable-options
59 * How input elements get activated. Possible values: `focus|select|none`.
61 * @var {string} activate
62 * @memberOf editable-options
66 * Whether to disable x-editable. Can be overloaded on each element.
68 * @var {boolean} isDisabled
69 * @memberOf editable-options
74 * Event, on which the edit mode gets activated.
77 * @var {string} activationEvent
78 * @memberOf editable-options
80 activationEvent: 'click'
85 Angular-ui bootstrap datepicker
86 http://angular-ui.github.io/bootstrap/#/datepicker
88 angular.module('xeditable').directive('editableBsdate', ['editableDirectiveFactory',
89 function(editableDirectiveFactory) {
90 return editableDirectiveFactory({
91 directiveName: 'editableBsdate',
92 inputTpl: '<div></div>',
94 /** This basically renders a datepicker as in the example shown in
95 ** http://angular-ui.github.io/bootstrap/#/datepicker
96 ** The attributes are all the same as in the bootstrap-ui datepicker with e- as prefix
98 this.parent.render.call(this);
100 var inputDatePicker = angular.element('<input type="text" class="form-control" ng-model="$data"/>');
101 var buttonDatePicker = angular.element('<button type="button" class="btn btn-default"><i class="glyphicon glyphicon-calendar"></i></button>');
102 var buttonWrapper = angular.element('<span class="input-group-btn"></span>');
104 inputDatePicker.attr('datepicker-popup', this.attrs.eDatepickerPopupXEditable || 'yyyy/MM/dd' );
105 inputDatePicker.attr('is-open', this.attrs.eIsOpen);
106 inputDatePicker.attr('date-disabled', this.attrs.eDateDisabled);
107 inputDatePicker.attr('datepicker-popup', this.attrs.eDatepickerPopup);
108 inputDatePicker.attr('datepicker-mode', this.attrs.eDatepickerMode || 'day');
109 inputDatePicker.attr('min-date', this.attrs.eMinDate);
110 inputDatePicker.attr('max-date', this.attrs.eMaxDate);
111 inputDatePicker.attr('show-weeks', this.attrs.eShowWeeks || true);
112 inputDatePicker.attr('starting-day', this.attrs.eStartingDay || 0);
113 inputDatePicker.attr('init-date', this.attrs.eInitDate || new Date());
114 inputDatePicker.attr('min-mode', this.attrs.eMinMode || 'day');
115 inputDatePicker.attr('max-mode', this.attrs.eMaxMode || 'year');
116 inputDatePicker.attr('format-day', this.attrs.eFormatDay || 'dd');
117 inputDatePicker.attr('format-month', this.attrs.eFormatMonth || 'MMMM');
118 inputDatePicker.attr('format-year', this.attrs.eFormatYear || 'yyyy');
119 inputDatePicker.attr('format-day-header', this.attrs.eFormatDayHeader || 'EEE');
120 inputDatePicker.attr('format-day-title', this.attrs.eFormatDayTitle || 'MMMM yyyy');
121 inputDatePicker.attr('format-month-title', this.attrs.eFormatMonthTitle || 'yyyy');
122 inputDatePicker.attr('year-range', this.attrs.eYearRange || 20);
123 inputDatePicker.attr('show-button-bar', this.attrs.eShowButtonBar || true);
124 inputDatePicker.attr('current-text', this.attrs.eCurrentText || 'Today');
125 inputDatePicker.attr('clear-text', this.attrs.eClearText || 'Clear');
126 inputDatePicker.attr('close-text', this.attrs.eCloseText || 'Done');
127 inputDatePicker.attr('close-on-date-selection', this.attrs.eCloseOnDateSelection || true);
128 inputDatePicker.attr('date-picker-append-to-body', this.attrs.eDatePickerAppendToBody || false);
129 inputDatePicker.attr('date-disabled', this.attrs.eDateDisabled);
131 buttonDatePicker.attr('ng-click',this.attrs.eNgClick);
133 buttonWrapper.append(buttonDatePicker);
134 this.inputEl.prepend(inputDatePicker);
135 this.inputEl.append(buttonWrapper);
137 this.inputEl.removeAttr('class');
138 this.inputEl.attr('class','input-group');
144 Angular-ui bootstrap editable timepicker
145 http://angular-ui.github.io/bootstrap/#/timepicker
147 angular.module('xeditable').directive('editableBstime', ['editableDirectiveFactory',
148 function(editableDirectiveFactory) {
149 return editableDirectiveFactory({
150 directiveName: 'editableBstime',
151 inputTpl: '<timepicker></timepicker>',
153 this.parent.render.call(this);
155 // timepicker can't update model when ng-model set directly to it
156 // see: https://github.com/angular-ui/bootstrap/issues/1141
157 // so we wrap it into DIV
158 var div = angular.element('<div class="well well-small" style="display:inline-block;"></div>');
160 // move ng-model to wrapping div
161 div.attr('ng-model', this.inputEl.attr('ng-model'));
162 this.inputEl.removeAttr('ng-model');
164 // move ng-change to wrapping div
165 if(this.attrs.eNgChange) {
166 div.attr('ng-change', this.inputEl.attr('ng-change'));
167 this.inputEl.removeAttr('ng-change');
171 this.inputEl.wrap(div);
176 angular.module('xeditable').directive('editableCheckbox', ['editableDirectiveFactory',
177 function(editableDirectiveFactory) {
178 return editableDirectiveFactory({
179 directiveName: 'editableCheckbox',
180 inputTpl: '<input type="checkbox">',
182 this.parent.render.call(this);
183 if(this.attrs.eTitle) {
184 this.inputEl.wrap('<label></label>');
185 this.inputEl.parent().append(this.attrs.eTitle);
188 autosubmit: function() {
190 self.inputEl.bind('change', function() {
191 setTimeout(function() {
192 self.scope.$apply(function() {
193 self.scope.$form.$submit();
202 angular.module('xeditable').directive('editableChecklist', [
203 'editableDirectiveFactory',
204 'editableNgOptionsParser',
205 function(editableDirectiveFactory, editableNgOptionsParser) {
206 return editableDirectiveFactory({
207 directiveName: 'editableChecklist',
208 inputTpl: '<span></span>',
211 this.parent.render.call(this);
212 var parsed = editableNgOptionsParser(this.attrs.eNgOptions);
213 var html = '<label ng-repeat="'+parsed.ngRepeat+'">'+
214 '<input type="checkbox" checklist-model="$parent.$data" checklist-value="'+parsed.locals.valueFn+'">'+
215 '<span ng-bind="'+parsed.locals.displayFn+'"></span></label>';
217 this.inputEl.removeAttr('ng-model');
218 this.inputEl.removeAttr('ng-options');
219 this.inputEl.html(html);
224 angular.module('xeditable').directive('editableCombodate', ['editableDirectiveFactory', 'editableCombodate',
225 function(editableDirectiveFactory, editableCombodate) {
226 return editableDirectiveFactory({
227 directiveName: 'editableCombodate',
228 inputTpl: '<input type="text">',
230 this.parent.render.call(this);
231 var combodate = editableCombodate.getInstance(this.inputEl, {value: new Date(this.scope.$data)});
234 combodate.$widget.find('select').bind('change', function(e) {
235 self.scope.$data = (new Date(combodate.getValue())).toISOString();
242 Input types: text|email|tel|number|url|search|color|date|datetime|time|month|week
247 var types = 'text|password|email|tel|number|url|search|color|date|datetime|time|month|week|file'.split('|');
251 // generate directives
252 angular.forEach(types, function(type) {
253 var directiveName = 'editable'+type.charAt(0).toUpperCase() + type.slice(1);
254 angular.module('xeditable').directive(directiveName, ['editableDirectiveFactory',
255 function(editableDirectiveFactory) {
256 return editableDirectiveFactory({
257 directiveName: directiveName,
258 inputTpl: '<input type="'+type+'">'
263 //`range` is bit specific
264 angular.module('xeditable').directive('editableRange', ['editableDirectiveFactory',
265 function(editableDirectiveFactory) {
266 return editableDirectiveFactory({
267 directiveName: 'editableRange',
268 inputTpl: '<input type="range" id="range" name="range">',
270 this.parent.render.call(this);
271 this.inputEl.after('<output>{{$data}}</output>');
280 angular.module('xeditable').directive('editableRadiolist', [
281 'editableDirectiveFactory',
282 'editableNgOptionsParser',
283 function(editableDirectiveFactory, editableNgOptionsParser) {
284 return editableDirectiveFactory({
285 directiveName: 'editableRadiolist',
286 inputTpl: '<span></span>',
288 this.parent.render.call(this);
289 var parsed = editableNgOptionsParser(this.attrs.eNgOptions);
290 var html = '<label ng-repeat="'+parsed.ngRepeat+'">'+
291 '<input type="radio" ng-disabled="' + this.attrs.eNgDisabled + '" ng-model="$parent.$data" value="{{'+parsed.locals.valueFn+'}}">'+
292 '<span ng-bind="'+parsed.locals.displayFn+'"></span></label>';
294 this.inputEl.removeAttr('ng-model');
295 this.inputEl.removeAttr('ng-options');
296 this.inputEl.html(html);
298 autosubmit: function() {
300 self.inputEl.bind('change', function() {
301 setTimeout(function() {
302 self.scope.$apply(function() {
303 self.scope.$form.$submit();
312 angular.module('xeditable').directive('editableSelect', ['editableDirectiveFactory',
313 function(editableDirectiveFactory) {
314 return editableDirectiveFactory({
315 directiveName: 'editableSelect',
316 inputTpl: '<select></select>',
317 autosubmit: function() {
319 self.inputEl.bind('change', function() {
320 self.scope.$apply(function() {
321 self.scope.$form.$submit();
328 angular.module('xeditable').directive('editableTextarea', ['editableDirectiveFactory',
329 function(editableDirectiveFactory) {
330 return editableDirectiveFactory({
331 directiveName: 'editableTextarea',
332 inputTpl: '<textarea></textarea>',
333 addListeners: function() {
335 self.parent.addListeners.call(self);
336 // submit textarea by ctrl+enter even with buttons
337 if (self.single && self.buttons !== 'no') {
341 autosubmit: function() {
343 self.inputEl.bind('keydown', function(e) {
344 if ((e.ctrlKey || e.metaKey) && (e.keyCode === 13)) {
345 self.scope.$apply(function() {
346 self.scope.$form.$submit();
355 * EditableController class.
356 * Attached to element with `editable-xxx` directive.
358 * @namespace editable-element
361 TODO: this file should be refactored to work more clear without closures!
363 angular.module('xeditable').factory('editableController',
364 ['$q', 'editableUtils',
365 function($q, editableUtils) {
367 //EditableController function
368 EditableController.$inject = ['$scope', '$attrs', '$element', '$parse', 'editableThemes', 'editableIcons', 'editableOptions', '$rootScope', '$compile', '$q'];
369 function EditableController($scope, $attrs, $element, $parse, editableThemes, editableIcons, editableOptions, $rootScope, $compile, $q) {
372 //if control is disabled - it does not participate in waiting process
378 self.elem = $element;
381 self.editorEl = null;
384 self.theme = editableThemes[editableOptions.theme] || editableThemes['default'];
387 //will be undefined if icon_set is default and theme is default
388 self.icon_set = editableOptions.icon_set === 'default' ? editableIcons.default[editableOptions.theme] : editableIcons.external[editableOptions.icon_set];
390 //to be overwritten by directive
392 self.directiveName = '';
394 // with majority of controls copy is not needed, but..
395 // copy MUST NOT be used for `select-multiple` with objects as items
396 // copy MUST be used for `checklist`
397 self.useCopy = false;
403 * Attributes defined with `e-*` prefix automatically transfered from original element to
405 * For example, if you set `<span editable-text="user.name" e-style="width: 100px"`>
406 * then input will appear as `<input style="width: 100px">`.
407 * See [demo](#text-customize).
409 * @var {any|attribute} e-*
410 * @memberOf editable-element
414 * Whether to show ok/cancel buttons. Values: `right|no`.
415 * If set to `no` control automatically submitted when value changed.
416 * If control is part of form buttons will never be shown.
418 * @var {string|attribute} buttons
419 * @memberOf editable-element
421 self.buttons = 'right';
423 * Action when control losses focus. Values: `cancel|submit|ignore`.
424 * Has sense only for single editable element.
425 * Otherwise, if control is part of form - you should set `blur` of form, not of individual element.
427 * @var {string|attribute} blur
428 * @memberOf editable-element
430 // no real `blur` property as it is transfered to editable form
433 self.init = function(single) {
434 self.single = single;
436 self.name = $attrs.eName || $attrs[self.directiveName];
438 if(!$attrs[directiveName] && !$attrs.eNgModel && ($attrs.eValue === undefined)) {
439 throw 'You should provide value for `'+directiveName+'` or `e-value` in editable element!';
442 if($attrs[self.directiveName]) {
443 valueGetter = $parse($attrs[self.directiveName]);
445 throw 'You should provide value for `'+self.directiveName+'` in editable element!';
448 // settings for single and non-single
450 // hide buttons for non-single
453 self.buttons = self.attrs.buttons || editableOptions.buttons;
456 //if name defined --> watch changes and update $data in form
458 self.scope.$watch('$data', function(newVal){
459 self.scope.$form.$data[$attrs.eName] = newVal;
464 * Called when control is shown.
465 * See [demo](#select-remote).
467 * @var {method|attribute} onshow
468 * @memberOf editable-element
471 self.onshow = function() {
472 return self.catchError($parse($attrs.onshow)($scope));
477 * Called when control is hidden after both save or cancel.
479 * @var {method|attribute} onhide
480 * @memberOf editable-element
483 self.onhide = function() {
484 return $parse($attrs.onhide)($scope);
489 * Called when control is cancelled.
491 * @var {method|attribute} oncancel
492 * @memberOf editable-element
494 if($attrs.oncancel) {
495 self.oncancel = function() {
496 return $parse($attrs.oncancel)($scope);
501 * Called during submit before value is saved to model.
502 * See [demo](#onbeforesave).
504 * @var {method|attribute} onbeforesave
505 * @memberOf editable-element
507 if ($attrs.onbeforesave) {
508 self.onbeforesave = function() {
509 return self.catchError($parse($attrs.onbeforesave)($scope));
514 * Called during submit after value is saved to model.
515 * See [demo](#onaftersave).
517 * @var {method|attribute} onaftersave
518 * @memberOf editable-element
520 if ($attrs.onaftersave) {
521 self.onaftersave = function() {
522 return self.catchError($parse($attrs.onaftersave)($scope));
526 // watch change of model to update editable element
527 // now only add/remove `editable-empty` class.
528 // Initially this method called with newVal = undefined, oldVal = undefined
529 // so no need initially call handleEmpty() explicitly
530 $scope.$parent.$watch($attrs[self.directiveName], function(newVal, oldVal) {
531 self.setLocalValue();
536 self.render = function() {
537 var theme = self.theme;
540 self.inputEl = angular.element(self.inputTpl);
543 self.controlsEl = angular.element(theme.controlsTpl);
544 self.controlsEl.append(self.inputEl);
547 if(self.buttons !== 'no') {
548 self.buttonsEl = angular.element(theme.buttonsTpl);
549 self.submitEl = angular.element(theme.submitTpl);
550 self.cancelEl = angular.element(theme.cancelTpl);
552 self.submitEl.find('span').addClass(self.icon_set.ok);
553 self.cancelEl.find('span').addClass(self.icon_set.cancel);
555 self.buttonsEl.append(self.submitEl).append(self.cancelEl);
556 self.controlsEl.append(self.buttonsEl);
558 self.inputEl.addClass('editable-has-buttons');
562 self.errorEl = angular.element(theme.errorTpl);
563 self.controlsEl.append(self.errorEl);
566 self.editorEl = angular.element(self.single ? theme.formTpl : theme.noformTpl);
567 self.editorEl.append(self.controlsEl);
569 // transfer `e-*|data-e-*|x-e-*` attributes
570 for(var k in $attrs.$attr) {
574 var transferAttr = false;
575 var nextLetter = k.substring(1, 2);
577 // if starts with `e` + uppercase letter
578 if(k.substring(0, 1) === 'e' && nextLetter === nextLetter.toUpperCase()) {
579 transferAttr = k.substring(1); // cut `e`
584 // exclude `form` and `ng-submit`,
585 if(transferAttr === 'Form' || transferAttr === 'NgSubmit') {
589 // convert back to lowercase style
590 transferAttr = transferAttr.substring(0, 1).toLowerCase() + editableUtils.camelToDash(transferAttr.substring(1));
592 // workaround for attributes without value (e.g. `multiple = "multiple"`)
593 // except for 'e-value'
594 var attrValue = (transferAttr !== 'value' && $attrs[k] === '') ? transferAttr : $attrs[k];
596 // set attributes to input
597 self.inputEl.attr(transferAttr, attrValue);
600 self.inputEl.addClass('editable-input');
601 self.inputEl.attr('ng-model', '$data');
603 // add directiveName class to editor, e.g. `editable-text`
604 self.editorEl.addClass(editableUtils.camelToDash(self.directiveName));
607 self.editorEl.attr('editable-form', '$form');
608 // transfer `blur` to form
609 self.editorEl.attr('blur', self.attrs.blur || (self.buttons === 'no' ? 'cancel' : editableOptions.blurElem));
612 //apply `postrender` method of theme
613 if(angular.isFunction(theme.postrender)) {
614 theme.postrender.call(self);
619 // with majority of controls copy is not needed, but..
620 // copy MUST NOT be used for `select-multiple` with objects as items
621 // copy MUST be used for `checklist`
622 self.setLocalValue = function() {
623 self.scope.$data = self.useCopy ?
624 angular.copy(valueGetter($scope.$parent)) :
625 valueGetter($scope.$parent);
629 self.show = function() {
630 // set value of scope.$data
631 self.setLocalValue();
634 Originally render() was inside init() method, but some directives polluting editorEl,
635 so it is broken on second openning.
636 Cloning is not a solution as jqLite can not clone with event handler's.
641 $element.after(self.editorEl);
643 // compile (needed to attach ng-* events from markup)
644 $compile(self.editorEl)($scope);
646 // attach listeners (`escape`, autosubmit, etc)
650 $element.addClass('editable-hide');
653 return self.onshow();
657 self.hide = function() {
659 self.editorEl.remove();
660 $element.removeClass('editable-hide');
663 return self.onhide();
667 self.cancel = function() {
670 // don't call hide() here as it called in form's code
674 Called after show to attach listeners
676 self.addListeners = function() {
677 // bind keyup for `escape`
678 self.inputEl.bind('keyup', function(e) {
683 // todo: move this to editable-form!
685 // hide on `escape` press
687 self.scope.$apply(function() {
688 self.scope.$form.$cancel();
694 // autosubmit when `no buttons`
695 if (self.single && self.buttons === 'no') {
699 // click - mark element as clicked to exclude in document click handler
700 self.editorEl.bind('click', function(e) {
701 // ignore right/middle button click
702 if (e.which && e.which !== 1) {
706 if (self.scope.$form.$visible) {
707 self.scope.$form._clicked = true;
713 self.setWaiting = function(value) {
715 // participate in waiting only if not disabled
716 inWaiting = !self.inputEl.attr('disabled') &&
717 !self.inputEl.attr('ng-disabled') &&
718 !self.inputEl.attr('ng-enabled');
720 self.inputEl.attr('disabled', 'disabled');
722 self.buttonsEl.find('button').attr('disabled', 'disabled');
727 self.inputEl.removeAttr('disabled');
728 if (self.buttonsEl) {
729 self.buttonsEl.find('button').removeAttr('disabled');
735 self.activate = function(start, end) {
736 setTimeout(function() {
737 var el = self.inputEl[0];
738 if (editableOptions.activate === 'focus' && el.focus) {
741 el.onfocus = function(){
743 setTimeout(function(){
744 that.setSelectionRange(start,end);
750 if (editableOptions.activate === 'select' && el.select) {
756 self.setError = function(msg) {
757 if(!angular.isObject(msg)) {
764 Checks that result is string or promise returned string and shows it as error message
765 Applied to onshow, onbeforesave, onaftersave
767 self.catchError = function(result, noPromise) {
768 if (angular.isObject(result) && noPromise !== true) {
769 $q.when(result).then(
770 //success and fail handlers are equal
771 angular.bind(this, function(r) {
772 this.catchError(r, true);
774 angular.bind(this, function(r) {
775 this.catchError(r, true);
779 } else if (noPromise && angular.isObject(result) && result.status &&
780 (result.status !== 200) && result.data && angular.isString(result.data)) {
781 this.setError(result.data);
782 //set result to string: to let form know that there was error
783 result = result.data;
784 } else if (angular.isString(result)) {
785 this.setError(result);
790 self.save = function() {
791 valueGetter.assign($scope.$parent,
792 self.useCopy ? angular.copy(self.scope.$data) : self.scope.$data);
794 // no need to call handleEmpty here as we are watching change of model value
795 // self.handleEmpty();
799 attach/detach `editable-empty` class to element
801 self.handleEmpty = function() {
802 var val = valueGetter($scope.$parent);
803 var isEmpty = val === null || val === undefined || val === "" || (angular.isArray(val) && val.length === 0);
804 $element.toggleClass('editable-empty', isEmpty);
808 Called when `buttons = "no"` to submit automatically
810 self.autosubmit = angular.noop;
812 self.onshow = angular.noop;
813 self.onhide = angular.noop;
814 self.oncancel = angular.noop;
815 self.onbeforesave = angular.noop;
816 self.onaftersave = angular.noop;
819 return EditableController;
823 editableFactory is used to generate editable directives (see `/directives` folder)
824 Inside it does several things:
825 - detect form for editable element. Form may be one of three types:
826 1. autogenerated form (for single editable elements)
827 2. wrapper form (element wrapped by <form> tag)
828 3. linked form (element has `e-form` attribute pointing to existing form)
830 - attach editableController to element
832 Depends on: editableController, editableFormFactory
834 angular.module('xeditable').factory('editableDirectiveFactory',
835 ['$parse', '$compile', 'editableThemes', '$rootScope', '$document', 'editableController', 'editableFormController', 'editableOptions',
836 function($parse, $compile, editableThemes, $rootScope, $document, editableController, editableFormController, editableOptions) {
839 return function(overwrites) {
843 require: [overwrites.directiveName, '?^form'],
844 controller: editableController,
845 link: function(scope, elem, attrs, ctrl) {
846 // editable controller
852 // this variable indicates is element is bound to some existing form,
853 // or it's single element who's form will be generated automatically
854 // By default consider single element without any linked form.ß
857 // element wrapped by form
861 } else if(attrs.eForm) { // element not wrapped by <form>, but we hane `e-form` attr
862 var getter = $parse(attrs.eForm)(scope);
863 if(getter) { // form exists in scope (above), e.g. editable column
866 } else { // form exists below or not exist at all: check document.forms
867 for(var i=0; i<$document[0].forms.length;i++){
868 if($document[0].forms[i].name === attrs.eForm) {
869 // form is below and not processed yet
879 if(hasForm && !attrs.eName) {
880 throw 'You should provide `e-name` for editable element inside form!';
884 //check for `editable-form` attr in form
887 throw 'You should provide `e-name` for editable element inside form!';
891 // store original props to `parent` before merge
892 angular.forEach(overwrites, function(v, k) {
893 if(eCtrl[k] !== undefined) {
894 eCtrl.parent[k] = eCtrl[k];
898 // merge overwrites to base editable controller
899 angular.extend(eCtrl, overwrites);
901 // x-editable can be disabled using editableOption or edit-disabled attribute
902 var disabled = angular.isDefined(attrs.editDisabled) ?
903 scope.$eval(attrs.editDisabled) :
904 editableOptions.isDisabled;
910 // init editable ctrl
911 eCtrl.init(!hasForm);
913 // publich editable controller as `$editable` to be referenced in html
914 scope.$editable = eCtrl;
916 // add `editable` class to element
917 elem.addClass('editable');
922 scope.$form = eFormCtrl;
923 if(!scope.$form.$addEditable) {
924 throw 'Form with editable elements should have `editable-form` attribute.';
926 scope.$form.$addEditable(eCtrl);
928 // future form (below): add editable controller to buffer and add to form later
929 $rootScope.$$editableBuffer = $rootScope.$$editableBuffer || {};
930 $rootScope.$$editableBuffer[attrs.eForm] = $rootScope.$$editableBuffer[attrs.eForm] || [];
931 $rootScope.$$editableBuffer[attrs.eForm].push(eCtrl);
932 scope.$form = null; //will be re-assigned later
936 // create editableform controller
937 scope.$form = editableFormController();
938 // add self to editable controller
939 scope.$form.$addEditable(eCtrl);
941 // if `e-form` provided, publish local $form in scope
943 scope.$parent[attrs.eForm] = scope.$form;
946 // bind click - if no external form defined
947 if(!attrs.eForm || attrs.eClickable) {
948 elem.addClass('editable-click');
949 elem.bind(editableOptions.activationEvent, function(e) {
952 scope.$apply(function(){
965 Returns editableForm controller
967 angular.module('xeditable').factory('editableFormController',
968 ['$parse', '$document', '$rootScope', 'editablePromiseCollection', 'editableUtils',
969 function($parse, $document, $rootScope, editablePromiseCollection, editableUtils) {
971 // array of opened editable forms
974 //Check if the child element correspond or is a descendant of the parent element
975 var isSelfOrDescendant = function (parent, child) {
976 if (child == parent) {
980 var node = child.parentNode;
981 while (node !== null) {
982 if (node == parent) {
985 node = node.parentNode;
990 //Check if it is a real blur : if the click event appear on a shown editable elem, this is not a blur.
991 var isBlur = function(shown, event) {
994 var editables = shown.$editables;
995 angular.forEach(editables, function(v){
996 var element = v.editorEl[0];
997 if (isSelfOrDescendant(element, event.target))
1004 // bind click to body: cancel|submit|ignore forms
1005 $document.bind('click', function(e) {
1006 // ignore right/middle button click
1007 if ((e.which && e.which !== 1) || e.isDefaultPrevented()) {
1013 for (var i=0; i<shown.length; i++) {
1016 if (shown[i]._clicked) {
1017 shown[i]._clicked = false;
1022 if (shown[i].$waiting) {
1026 if (shown[i]._blur === 'cancel' && isBlur(shown[i], e)) {
1027 toCancel.push(shown[i]);
1030 if (shown[i]._blur === 'submit' && isBlur(shown[i], e)) {
1031 toSubmit.push(shown[i]);
1035 if (toCancel.length || toSubmit.length) {
1036 $rootScope.$apply(function() {
1037 angular.forEach(toCancel, function(v){ v.$cancel(); });
1038 angular.forEach(toSubmit, function(v){ v.$submit(); });
1045 $addEditable: function(editable) {
1046 //console.log('add editable', editable.elem, editable.elem.bind);
1047 this.$editables.push(editable);
1049 //'on' is not supported in angular 1.0.8
1050 editable.elem.bind('$destroy', angular.bind(this, this.$removeEditable, editable));
1052 //bind editable's local $form to self (if not bound yet, below form)
1053 if (!editable.scope.$form) {
1054 editable.scope.$form = this;
1057 //if form already shown - call show() of new editable
1058 if (this.$visible) {
1059 editable.catchError(editable.show());
1063 $removeEditable: function(editable) {
1065 for(var i=0; i < this.$editables.length; i++) {
1066 if(this.$editables[i] === editable) {
1067 this.$editables.splice(i, 1);
1074 * Shows form with editable controls.
1077 * @memberOf editable-form
1080 if (this.$visible) {
1084 this.$visible = true;
1086 var pc = editablePromiseCollection();
1089 pc.when(this.$onshow());
1092 this.$setError(null, '');
1095 angular.forEach(this.$editables, function(editable) {
1096 pc.when(editable.show());
1099 //wait promises and activate
1101 onWait: angular.bind(this, this.$setWaiting),
1102 onTrue: angular.bind(this, this.$activate),
1103 onFalse: angular.bind(this, this.$activate),
1104 onString: angular.bind(this, this.$activate)
1107 // add to internal list of shown forms
1108 // setTimeout needed to prevent closing right after opening (e.g. when trigger by button)
1109 setTimeout(angular.bind(this, function() {
1110 // clear `clicked` to get ready for clicks on visible form
1111 this._clicked = false;
1112 if(editableUtils.indexOf(shown, this) === -1) {
1119 * Sets focus on form field specified by `name`.
1121 * @method $activate(name)
1122 * @param {string} name name of field
1123 * @memberOf editable-form
1125 $activate: function(name) {
1127 if (this.$editables.length) {
1129 if (angular.isString(name)) {
1130 for(i=0; i<this.$editables.length; i++) {
1131 if (this.$editables[i].name === name) {
1132 this.$editables[i].activate();
1138 //try activate error field
1139 for(i=0; i<this.$editables.length; i++) {
1140 if (this.$editables[i].error) {
1141 this.$editables[i].activate();
1146 //by default activate first field
1147 this.$editables[0].activate(this.$editables[0].elem[0].selectionStart, this.$editables[0].elem[0].selectionEnd);
1152 * Hides form with editable controls without saving.
1155 * @memberOf editable-form
1158 if (!this.$visible) {
1161 this.$visible = false;
1165 angular.forEach(this.$editables, function(editable) {
1169 // remove from internal list of shown forms
1170 editableUtils.arrayRemove(shown, this);
1174 * Triggers `oncancel` event and calls `$hide()`.
1177 * @memberOf editable-form
1179 $cancel: function() {
1180 if (!this.$visible) {
1185 // children's cancel
1186 angular.forEach(this.$editables, function(editable) {
1193 $setWaiting: function(value) {
1194 this.$waiting = !!value;
1195 // we can't just set $waiting variable and use it via ng-disabled in children
1196 // because in editable-row form is not accessible
1197 angular.forEach(this.$editables, function(editable) {
1198 editable.setWaiting(!!value);
1203 * Shows error message for particular field.
1205 * @method $setError(name, msg)
1206 * @param {string} name name of field
1207 * @param {string} msg error message
1208 * @memberOf editable-form
1210 $setError: function(name, msg) {
1211 angular.forEach(this.$editables, function(editable) {
1212 if(!name || editable.name === name) {
1213 editable.setError(msg);
1218 $submit: function() {
1219 if (this.$waiting) {
1224 this.$setError(null, '');
1226 //children onbeforesave
1227 var pc = editablePromiseCollection();
1228 angular.forEach(this.$editables, function(editable) {
1229 pc.when(editable.onbeforesave());
1233 onbeforesave result:
1234 - true/undefined: save data and close form
1235 - false: close form without saving
1236 - string: keep form open and show error
1239 onWait: angular.bind(this, this.$setWaiting),
1240 onTrue: angular.bind(this, checkSelf, true),
1241 onFalse: angular.bind(this, checkSelf, false),
1242 onString: angular.bind(this, this.$activate)
1246 function checkSelf(childrenTrue){
1247 var pc = editablePromiseCollection();
1248 pc.when(this.$onbeforesave());
1250 onWait: angular.bind(this, this.$setWaiting),
1251 onTrue: childrenTrue ? angular.bind(this, this.$save) : angular.bind(this, this.$hide),
1252 onFalse: angular.bind(this, this.$hide),
1253 onString: angular.bind(this, this.$activate)
1259 // write model for each editable
1260 angular.forEach(this.$editables, function(editable) {
1264 //call onaftersave of self and children
1265 var pc = editablePromiseCollection();
1266 pc.when(this.$onaftersave());
1267 angular.forEach(this.$editables, function(editable) {
1268 pc.when(editable.onaftersave());
1273 - true/undefined/false: just close form
1274 - string: keep form open and show error
1277 onWait: angular.bind(this, this.$setWaiting),
1278 onTrue: angular.bind(this, this.$hide),
1279 onFalse: angular.bind(this, this.$hide),
1280 onString: angular.bind(this, this.$activate)
1284 $onshow: angular.noop,
1285 $oncancel: angular.noop,
1286 $onhide: angular.noop,
1287 $onbeforesave: angular.noop,
1288 $onaftersave: angular.noop
1292 return angular.extend({
1295 * Form visibility flag.
1297 * @var {bool} $visible
1298 * @memberOf editable-form
1302 * Form waiting flag. It becomes `true` when form is loading or saving data.
1304 * @var {bool} $waiting
1305 * @memberOf editable-form
1316 * EditableForm directive. Should be defined in <form> containing editable controls.
1317 * It add some usefull methods to form variable exposed to scope by `name="myform"` attribute.
1319 * @namespace editable-form
1321 angular.module('xeditable').directive('editableForm',
1322 ['$rootScope', '$parse', 'editableFormController', 'editableOptions',
1323 function($rootScope, $parse, editableFormController, editableOptions) {
1327 //require: ['form', 'editableForm'],
1328 //controller: EditableFormController,
1329 compile: function() {
1331 pre: function(scope, elem, attrs, ctrl) {
1335 //if `editableForm` has value - publish smartly under this value
1336 //this is required only for single editor form that is created and removed
1337 if(attrs.editableForm) {
1338 if(scope[attrs.editableForm] && scope[attrs.editableForm].$show) {
1339 eForm = scope[attrs.editableForm];
1340 angular.extend(form, eForm);
1342 eForm = editableFormController();
1343 scope[attrs.editableForm] = eForm;
1344 angular.extend(eForm, form);
1346 } else { //just merge to form and publish if form has name
1347 eForm = editableFormController();
1348 angular.extend(form, eForm);
1351 //read editables from buffer (that appeared before FORM tag)
1352 var buf = $rootScope.$$editableBuffer;
1353 var name = form.$name;
1354 if(name && buf && buf[name]) {
1355 angular.forEach(buf[name], function(editable) {
1356 eForm.$addEditable(editable);
1361 post: function(scope, elem, attrs, ctrl) {
1364 if(attrs.editableForm && scope[attrs.editableForm] && scope[attrs.editableForm].$show) {
1365 eForm = scope[attrs.editableForm];
1371 * Called when form is shown.
1373 * @var {method|attribute} onshow
1374 * @memberOf editable-form
1377 eForm.$onshow = angular.bind(eForm, $parse(attrs.onshow), scope);
1381 * Called when form hides after both save or cancel.
1383 * @var {method|attribute} onhide
1384 * @memberOf editable-form
1387 eForm.$onhide = angular.bind(eForm, $parse(attrs.onhide), scope);
1391 * Called when form is cancelled.
1393 * @var {method|attribute} oncancel
1394 * @memberOf editable-form
1396 if(attrs.oncancel) {
1397 eForm.$oncancel = angular.bind(eForm, $parse(attrs.oncancel), scope);
1401 * Whether form initially rendered in shown state.
1403 * @var {bool|attribute} shown
1404 * @memberOf editable-form
1406 if(attrs.shown && $parse(attrs.shown)(scope)) {
1411 * Action when form losses focus. Values: `cancel|submit|ignore`.
1412 * Default is `ignore`.
1414 * @var {string|attribute} blur
1415 * @memberOf editable-form
1417 eForm._blur = attrs.blur || editableOptions.blurForm;
1419 // onbeforesave, onaftersave
1420 if(!attrs.ngSubmit && !attrs.submit) {
1422 * Called after all children `onbeforesave` callbacks but before saving form values
1424 * If at least one children callback returns `non-string` - it will not not be called.
1425 * See [editable-form demo](#editable-form) for details.
1427 * @var {method|attribute} onbeforesave
1428 * @memberOf editable-form
1431 if(attrs.onbeforesave) {
1432 eForm.$onbeforesave = function() {
1433 return $parse(attrs.onbeforesave)(scope, {$data: eForm.$data});
1438 * Called when form values are saved to model.
1439 * See [editable-form demo](#editable-form) for details.
1441 * @var {method|attribute} onaftersave
1442 * @memberOf editable-form
1445 if(attrs.onaftersave) {
1446 eForm.$onaftersave = function() {
1447 return $parse(attrs.onaftersave)(scope, {$data: eForm.$data});
1451 elem.bind('submit', function(event) {
1452 event.preventDefault();
1453 scope.$apply(function() {
1460 // click - mark form as clicked to exclude in document click handler
1461 elem.bind('click', function(e) {
1462 // ignore right/middle button click
1463 if (e.which && e.which !== 1) {
1467 if (eForm.$visible) {
1468 eForm._clicked = true;
1478 * editablePromiseCollection
1480 * Collect results of function calls. Shows waiting if there are promises.
1481 * Finally, applies callbacks if:
1482 * - onTrue(): all results are true and all promises resolved to true
1483 * - onFalse(): at least one result is false or promise resolved to false
1484 * - onString(): at least one result is string or promise rejected or promise resolved to string
1487 angular.module('xeditable').factory('editablePromiseCollection', ['$q', function($q) {
1489 function promiseCollection() {
1494 when: function(result, noPromise) {
1495 if (result === false) {
1496 this.hasFalse = true;
1497 } else if (!noPromise && angular.isObject(result)) {
1498 this.promises.push($q.when(result));
1499 } else if (angular.isString(result)){
1500 this.hasString = true;
1501 } else { //result === true || result === undefined || result === null
1505 //callbacks: onTrue, onFalse, onString
1506 then: function(callbacks) {
1507 callbacks = callbacks || {};
1508 var onTrue = callbacks.onTrue || angular.noop;
1509 var onFalse = callbacks.onFalse || angular.noop;
1510 var onString = callbacks.onString || angular.noop;
1511 var onWait = callbacks.onWait || angular.noop;
1515 if (this.promises.length) {
1517 $q.all(this.promises).then(
1521 //check all results via same `when` method (without checking promises)
1522 angular.forEach(results, function(result) {
1523 self.when(result, true);
1537 function applyCallback() {
1538 if (!self.hasString && !self.hasFalse) {
1540 } else if (!self.hasString && self.hasFalse) {
1551 return promiseCollection;
1558 angular.module('xeditable').factory('editableUtils', [function() {
1560 indexOf: function (array, obj) {
1561 if (array.indexOf) return array.indexOf(obj);
1563 for ( var i = 0; i < array.length; i++) {
1564 if (obj === array[i]) return i;
1569 arrayRemove: function (array, value) {
1570 var index = this.indexOf(array, value);
1572 array.splice(index, 1);
1577 // copy from https://github.com/angular/angular.js/blob/master/src/Angular.js
1578 camelToDash: function(str) {
1579 var SNAKE_CASE_REGEXP = /[A-Z]/g;
1580 return str.replace(SNAKE_CASE_REGEXP, function(letter, pos) {
1581 return (pos ? '-' : '') + letter.toLowerCase();
1585 dashToCamel: function(str) {
1586 var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g;
1587 var MOZ_HACK_REGEXP = /^moz([A-Z])/;
1589 replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) {
1590 return offset ? letter.toUpperCase() : letter;
1592 replace(MOZ_HACK_REGEXP, 'Moz$1');
1598 * editableNgOptionsParser
1600 * see: https://github.com/angular/angular.js/blob/master/src/ng/directive/select.js#L131
1602 angular.module('xeditable').factory('editableNgOptionsParser', [function() {
1603 //0000111110000000000022220000000000000000000000333300000000000000444444444444444000000000555555555555555000000066666666666666600000000000000007777000000000000000000088888
1604 var NG_OPTIONS_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+(.*?)(?:\s+track\s+by\s+(.*?))?$/;
1606 function parser(optionsExp) {
1609 if (! (match = optionsExp.match(NG_OPTIONS_REGEXP))) {
1610 throw 'ng-options parse error';
1614 displayFn = match[2] || match[1],
1615 valueName = match[4] || match[6],
1617 groupByFn = match[3] || '',
1618 valueFn = match[2] ? match[1] : valueName,
1619 valuesFn = match[7],
1621 trackFn = track ? match[8] : null;
1624 if (keyName === undefined) { // array
1625 ngRepeat = valueName + ' in ' + valuesFn;
1626 if (track !== undefined) {
1627 ngRepeat += ' track by '+trackFn;
1630 ngRepeat = '('+keyName+', '+valueName+') in '+valuesFn;
1633 // group not supported yet
1637 valueName: valueName,
1640 displayFn: displayFn
1651 * angular version of https://github.com/vitalets/combodate
1653 angular.module('xeditable').factory('editableCombodate', [function() {
1654 function Combodate(element, options) {
1655 this.$element = angular.element(element);
1657 if(this.$element[0].nodeName != 'INPUT') {
1658 throw 'Combodate should be applied to INPUT element';
1662 //in this format value stored in original input
1663 format: 'YYYY-MM-DD HH:mm',
1664 //in this format items in dropdowns are displayed
1665 template: 'D / MMM / YYYY H : mm',
1666 //initial value, can be `new Date()`
1670 yearDescending: true,
1673 firstItem: 'empty', //'name', 'empty', 'none'
1676 roundTime: true, // whether to round minutes and seconds if step > 1
1677 smartDays: true // whether days in combo depend on selected month: 31, 30, 28
1680 this.options = angular.extend({}, this.defaults, options);
1684 Combodate.prototype = {
1685 constructor: Combodate,
1688 //key regexp moment.method
1690 month: ['M', 'month'],
1691 year: ['Y', 'year'],
1692 hour: ['[Hh]', 'hours'],
1693 minute: ['m', 'minutes'],
1694 second: ['s', 'seconds'],
1698 this.$widget = angular.element('<span class="combodate"></span>').html(this.getTemplate());
1702 if (this.options.smartDays) {
1704 this.$widget.find('select').bind('change', function(e) {
1705 // update days count if month or year changes
1706 if (angular.element(e.target).hasClass('month') || angular.element(e.target).hasClass('year')) {
1707 combo.fillCombo('day');
1712 this.$widget.find('select').css('width', 'auto');
1714 // hide original input and insert widget
1715 this.$element.css('display', 'none').after(this.$widget);
1717 // set initial value
1718 this.setValue(this.$element.val() || this.options.value);
1722 Replace tokens in template with <select> elements
1724 getTemplate: function() {
1725 var tpl = this.options.template;
1726 var customClass = this.options.customClass;
1729 angular.forEach(this.map, function(v, k) {
1731 var r = new RegExp(v+'+');
1732 var token = v.length > 1 ? v.substring(1, 2) : v;
1734 tpl = tpl.replace(r, '{'+token+'}');
1737 //replace spaces with
1738 tpl = tpl.replace(/ /g, ' ');
1741 angular.forEach(this.map, function(v, k) {
1743 var token = v.length > 1 ? v.substring(1, 2) : v;
1745 tpl = tpl.replace('{'+token+'}', '<select class="'+k+' '+customClass+'"></select>');
1752 Initialize combos that presents in template
1754 initCombos: function() {
1755 for (var k in this.map) {
1756 var c = this.$widget[0].querySelectorAll('.'+k);
1757 // set properties like this.$day, this.$month etc.
1758 this['$'+k] = c.length ? angular.element(c) : null;
1765 Fill combo with items
1767 fillCombo: function(k) {
1768 var $combo = this['$'+k];
1773 // define method name to fill items, e.g `fillDays`
1774 var f = 'fill' + k.charAt(0).toUpperCase() + k.slice(1);
1775 var items = this[f]();
1776 var value = $combo.val();
1779 for(var i=0; i<items.length; i++) {
1780 $combo.append('<option value="'+items[i][0]+'">'+items[i][1]+'</option>');
1787 Initialize items of combos. Handles `firstItem` option
1789 fillCommon: function(key) {
1790 var values = [], relTime;
1792 if(this.options.firstItem === 'name') {
1793 //need both to support moment ver < 2 and >= 2
1794 relTime = moment.relativeTime || moment.langData()._relativeTime;
1795 var header = typeof relTime[key] === 'function' ? relTime[key](1, true, key, false) : relTime[key];
1796 //take last entry (see momentjs lang files structure)
1797 header = header.split(' ').reverse()[0];
1798 values.push(['', header]);
1799 } else if(this.options.firstItem === 'empty') {
1800 values.push(['', '']);
1809 fillDay: function() {
1810 var items = this.fillCommon('d'), name, i,
1811 twoDigit = this.options.template.indexOf('DD') !== -1,
1814 // detect days count (depends on month and year)
1815 // originally https://github.com/vitalets/combodate/pull/7
1816 if (this.options.smartDays && this.$month && this.$year) {
1817 var month = parseInt(this.$month.val(), 10);
1818 var year = parseInt(this.$year.val(), 10);
1820 if (!isNaN(month) && !isNaN(year)) {
1821 daysCount = moment([year, month]).daysInMonth();
1825 for (i = 1; i <= daysCount; i++) {
1826 name = twoDigit ? this.leadZero(i) : i;
1827 items.push([i, name]);
1835 fillMonth: function() {
1836 var items = this.fillCommon('M'), name, i,
1837 longNames = this.options.template.indexOf('MMMM') !== -1,
1838 shortNames = this.options.template.indexOf('MMM') !== -1,
1839 twoDigit = this.options.template.indexOf('MM') !== -1;
1841 for(i=0; i<=11; i++) {
1843 //see https://github.com/timrwood/momentjs.com/pull/36
1844 name = moment().date(1).month(i).format('MMMM');
1845 } else if(shortNames) {
1846 name = moment().date(1).month(i).format('MMM');
1847 } else if(twoDigit) {
1848 name = this.leadZero(i+1);
1852 items.push([i, name]);
1860 fillYear: function() {
1861 var items = [], name, i,
1862 longNames = this.options.template.indexOf('YYYY') !== -1;
1864 for(i=this.options.maxYear; i>=this.options.minYear; i--) {
1865 name = longNames ? i : (i+'').substring(2);
1866 items[this.options.yearDescending ? 'push' : 'unshift']([i, name]);
1869 items = this.fillCommon('y').concat(items);
1877 fillHour: function() {
1878 var items = this.fillCommon('h'), name, i,
1879 h12 = this.options.template.indexOf('h') !== -1,
1880 h24 = this.options.template.indexOf('H') !== -1,
1881 twoDigit = this.options.template.toLowerCase().indexOf('hh') !== -1,
1883 max = h12 ? 12 : 23;
1885 for(i=min; i<=max; i++) {
1886 name = twoDigit ? this.leadZero(i) : i;
1887 items.push([i, name]);
1895 fillMinute: function() {
1896 var items = this.fillCommon('m'), name, i,
1897 twoDigit = this.options.template.indexOf('mm') !== -1;
1899 for(i=0; i<=59; i+= this.options.minuteStep) {
1900 name = twoDigit ? this.leadZero(i) : i;
1901 items.push([i, name]);
1909 fillSecond: function() {
1910 var items = this.fillCommon('s'), name, i,
1911 twoDigit = this.options.template.indexOf('ss') !== -1;
1913 for(i=0; i<=59; i+= this.options.secondStep) {
1914 name = twoDigit ? this.leadZero(i) : i;
1915 items.push([i, name]);
1923 fillAmpm: function() {
1924 var ampmL = this.options.template.indexOf('a') !== -1,
1925 ampmU = this.options.template.indexOf('A') !== -1,
1927 ['am', ampmL ? 'am' : 'AM'],
1928 ['pm', ampmL ? 'pm' : 'PM']
1934 Returns current date value from combos.
1935 If format not specified - `options.format` used.
1936 If format = `null` - Moment object returned.
1938 getValue: function(format) {
1939 var dt, values = {},
1941 notSelected = false;
1943 //getting selected values
1944 angular.forEach(this.map, function(v, k) {
1948 var def = k === 'day' ? 1 : 0;
1950 values[k] = that['$'+k] ? parseInt(that['$'+k].val(), 10) : def;
1952 if(isNaN(values[k])) {
1958 //if at least one visible combo not selected - return empty string
1963 //convert hours 12h --> 24h
1965 //12:00 pm --> 12:00 (24-h format, midday), 12:00 am --> 00:00 (24-h format, midnight, start of day)
1966 if(values.hour === 12) {
1967 values.hour = this.$ampm.val() === 'am' ? 0 : 12;
1969 values.hour = this.$ampm.val() === 'am' ? values.hour : values.hour+12;
1973 dt = moment([values.year, values.month, values.day, values.hour, values.minute, values.second]);
1975 //highlight invalid date
1978 format = format === undefined ? this.options.format : format;
1979 if(format === null) {
1980 return dt.isValid() ? dt : null;
1982 return dt.isValid() ? dt.format(format) : '';
1986 setValue: function(value) {
1991 // parse in strict mode (third param `true`)
1992 var dt = typeof value === 'string' ? moment(value, this.options.format, true) : moment(value),
1996 //function to find nearest value in select options
1997 function getNearest($select, value) {
1999 angular.forEach($select.children('option'), function(opt, i){
2000 var optValue = angular.element(opt).attr('value');
2002 if(optValue === '') return;
2003 var distance = Math.abs(optValue - value);
2004 if(typeof delta.distance === 'undefined' || distance < delta.distance) {
2005 delta = {value: optValue, distance: distance};
2012 //read values from date object
2013 angular.forEach(this.map, function(v, k) {
2017 values[k] = dt[v[1]]();
2021 //12:00 pm --> 12:00 (24-h format, midday), 12:00 am --> 00:00 (24-h format, midnight, start of day)
2022 if(values.hour >= 12) {
2024 if(values.hour > 12) {
2029 if(values.hour === 0) {
2035 angular.forEach(values, function(v, k) {
2036 //call val() for each existing combo, e.g. this.$hour.val()
2039 if(k === 'minute' && that.options.minuteStep > 1 && that.options.roundTime) {
2040 v = getNearest(that['$'+k], v);
2043 if(k === 'second' && that.options.secondStep > 1 && that.options.roundTime) {
2044 v = getNearest(that['$'+k], v);
2051 // update days count
2052 if (this.options.smartDays) {
2053 this.fillCombo('day');
2056 this.$element.val(dt.format(this.options.format)).triggerHandler('change');
2061 highlight combos if date is invalid
2063 highlight: function(dt) {
2065 if(this.options.errorClass) {
2066 this.$widget.addClass(this.options.errorClass);
2068 //store original border color
2069 if(!this.borderColor) {
2070 this.borderColor = this.$widget.find('select').css('border-color');
2072 this.$widget.find('select').css('border-color', 'red');
2075 if(this.options.errorClass) {
2076 this.$widget.removeClass(this.options.errorClass);
2078 this.$widget.find('select').css('border-color', this.borderColor);
2083 leadZero: function(v) {
2084 return v <= 9 ? '0' + v : v;
2087 destroy: function() {
2088 this.$widget.remove();
2089 this.$element.removeData('combodate').show();
2095 getInstance: function(element, options) {
2096 return new Combodate(element, options);
2107 angular.module('xeditable').factory('editableIcons', function() {
2110 //Icon-set to use, defaults to bootstrap icons
2113 ok: 'icon-ok icon-white',
2114 cancel: 'icon-remove'
2117 ok: 'glyphicon glyphicon-ok',
2118 cancel: 'glyphicon glyphicon-remove'
2124 cancel: 'fa fa-times'
2138 Note: in postrender() `this` is instance of editableController
2140 angular.module('xeditable').factory('editableThemes', function() {
2144 formTpl: '<form class="editable-wrap"></form>',
2145 noformTpl: '<span class="editable-wrap"></span>',
2146 controlsTpl: '<span class="editable-controls"></span>',
2148 errorTpl: '<div class="editable-error" ng-show="$error" ng-bind="$error"></div>',
2149 buttonsTpl: '<span class="editable-buttons"></span>',
2150 submitTpl: '<button type="submit">save</button>',
2151 cancelTpl: '<button type="button" ng-click="$form.$cancel()">cancel</button>'
2156 formTpl: '<form class="form-inline editable-wrap" role="form"></form>',
2157 noformTpl: '<span class="editable-wrap"></span>',
2158 controlsTpl: '<div class="editable-controls controls control-group" ng-class="{\'error\': $error}"></div>',
2160 errorTpl: '<div class="editable-error help-block" ng-show="$error" ng-bind="$error"></div>',
2161 buttonsTpl: '<span class="editable-buttons"></span>',
2162 submitTpl: '<button type="submit" class="btn btn-primary"><span></span></button>',
2163 cancelTpl: '<button type="button" class="btn" ng-click="$form.$cancel()">'+
2171 formTpl: '<form class="form-inline editable-wrap" role="form"></form>',
2172 noformTpl: '<span class="editable-wrap"></span>',
2173 controlsTpl: '<div class="editable-controls form-group" ng-class="{\'has-error\': $error}"></div>',
2175 errorTpl: '<div class="editable-error help-block" ng-show="$error" ng-bind="$error"></div>',
2176 buttonsTpl: '<span class="editable-buttons"></span>',
2177 submitTpl: '<button type="submit" class="btn btn-primary"><span></span></button>',
2178 cancelTpl: '<button type="button" class="btn btn-default" ng-click="$form.$cancel()">'+
2182 //bs3 specific prop to change buttons class: btn-sm, btn-lg
2184 //bs3 specific prop to change standard inputs class: input-sm, input-lg
2186 postrender: function() {
2187 //apply `form-control` class to std inputs
2188 switch(this.directiveName) {
2189 case 'editableText':
2190 case 'editableSelect':
2191 case 'editableTextarea':
2192 case 'editableEmail':
2194 case 'editableNumber':
2196 case 'editableSearch':
2197 case 'editableDate':
2198 case 'editableDatetime':
2199 case 'editableBsdate':
2200 case 'editableTime':
2201 case 'editableMonth':
2202 case 'editableWeek':
2203 this.inputEl.addClass('form-control');
2204 if(this.theme.inputClass) {
2205 // don`t apply `input-sm` and `input-lg` to select multiple
2206 // should be fixed in bs itself!
2207 if(this.inputEl.attr('multiple') &&
2208 (this.theme.inputClass === 'input-sm' || this.theme.inputClass === 'input-lg')) {
2211 this.inputEl.addClass(this.theme.inputClass);
2214 case 'editableCheckbox':
2215 this.editorEl.addClass('checkbox');
2218 //apply buttonsClass (bs3 specific!)
2219 if(this.buttonsEl && this.theme.buttonsClass) {
2220 this.buttonsEl.find('button').addClass(this.theme.buttonsClass);