Built motion from commit 99feb03.|0.0.140
[motion.git] / public / bower_components / angular-xeditable / xeditable.js
1 /*!
2 angular-xeditable - 0.1.9
3 Edit-in-place for angular.js
4 Build date: 2015-03-26 
5 */
6 /**
7  * Angular-xeditable module 
8  *
9  */
10 angular.module('xeditable', [])
11
12
13 /**
14  * Default options. 
15  *
16  * @namespace editable-options
17  */
18 //todo: maybe better have editableDefaults, not options...
19 .value('editableOptions', {
20   /**
21    * Theme. Possible values `bs3`, `bs2`, `default`.
22    * 
23    * @var {string} theme
24    * @memberOf editable-options
25    */  
26   theme: 'default',
27   /**
28    * Icon Set. Possible values `font-awesome`, `default`.
29    * 
30    * @var {string} icon set
31    * @memberOf editable-options
32    */  
33   icon_set: 'default',
34   /**
35    * Whether to show buttons for single editalbe element.  
36    * Possible values `right` (default), `no`.
37    * 
38    * @var {string} buttons
39    * @memberOf editable-options
40    */    
41   buttons: 'right',
42   /**
43    * Default value for `blur` attribute of single editable element.  
44    * Can be `cancel|submit|ignore`.
45    * 
46    * @var {string} blurElem
47    * @memberOf editable-options
48    */
49   blurElem: 'cancel',
50   /**
51    * Default value for `blur` attribute of editable form.  
52    * Can be `cancel|submit|ignore`.
53    * 
54    * @var {string} blurForm
55    * @memberOf editable-options
56    */
57   blurForm: 'ignore',
58   /**
59    * How input elements get activated. Possible values: `focus|select|none`.
60    *
61    * @var {string} activate
62    * @memberOf editable-options
63    */
64   activate: 'focus',
65   /**
66    * Whether to disable x-editable. Can be overloaded on each element.
67    *
68    * @var {boolean} isDisabled
69    * @memberOf editable-options
70    */
71    isDisabled: false,
72   
73   /**
74    * Event, on which the edit mode gets activated. 
75    * Can be any event.
76    *
77    * @var {string} activationEvent
78    * @memberOf editable-options
79    */
80   activationEvent: 'click'
81
82 });
83
84 /*
85 Angular-ui bootstrap datepicker
86 http://angular-ui.github.io/bootstrap/#/datepicker
87 */
88 angular.module('xeditable').directive('editableBsdate', ['editableDirectiveFactory',
89         function(editableDirectiveFactory) {
90                 return editableDirectiveFactory({
91                         directiveName: 'editableBsdate',
92                         inputTpl: '<div></div>',
93                         render: function() {
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
97                                 **/
98                                 this.parent.render.call(this);
99
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>');
103
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);
130
131                                 buttonDatePicker.attr('ng-click',this.attrs.eNgClick);
132
133                                 buttonWrapper.append(buttonDatePicker);
134                                 this.inputEl.prepend(inputDatePicker);
135                                 this.inputEl.append(buttonWrapper);
136
137                                 this.inputEl.removeAttr('class');
138                                 this.inputEl.attr('class','input-group');
139
140                         }
141     });
142 }]);
143 /*
144 Angular-ui bootstrap editable timepicker
145 http://angular-ui.github.io/bootstrap/#/timepicker
146 */
147 angular.module('xeditable').directive('editableBstime', ['editableDirectiveFactory',
148   function(editableDirectiveFactory) {
149     return editableDirectiveFactory({
150       directiveName: 'editableBstime',
151       inputTpl: '<timepicker></timepicker>',
152       render: function() {
153         this.parent.render.call(this);
154
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>');
159
160         // move ng-model to wrapping div
161         div.attr('ng-model', this.inputEl.attr('ng-model'));
162         this.inputEl.removeAttr('ng-model');
163
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');
168         }
169
170         // wrap
171         this.inputEl.wrap(div);
172       }
173     });
174 }]);
175 //checkbox
176 angular.module('xeditable').directive('editableCheckbox', ['editableDirectiveFactory',
177   function(editableDirectiveFactory) {
178     return editableDirectiveFactory({
179       directiveName: 'editableCheckbox',
180       inputTpl: '<input type="checkbox">',
181       render: function() {
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);
186         }
187       },
188       autosubmit: function() {
189         var self = this;
190         self.inputEl.bind('change', function() {
191           setTimeout(function() {
192             self.scope.$apply(function() {
193               self.scope.$form.$submit();
194             });
195           }, 500);
196         });
197       }
198     });
199 }]);
200
201 // checklist
202 angular.module('xeditable').directive('editableChecklist', [
203   'editableDirectiveFactory',
204   'editableNgOptionsParser',
205   function(editableDirectiveFactory, editableNgOptionsParser) {
206     return editableDirectiveFactory({
207       directiveName: 'editableChecklist',
208       inputTpl: '<span></span>',
209       useCopy: true,
210       render: function() {
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>';
216
217         this.inputEl.removeAttr('ng-model');
218         this.inputEl.removeAttr('ng-options');
219         this.inputEl.html(html);
220       }
221     });
222 }]);
223
224 angular.module('xeditable').directive('editableCombodate', ['editableDirectiveFactory', 'editableCombodate',
225   function(editableDirectiveFactory, editableCombodate) {
226     return editableDirectiveFactory({
227       directiveName: 'editableCombodate',
228       inputTpl: '<input type="text">',
229       render: function() {
230         this.parent.render.call(this);
231         var combodate = editableCombodate.getInstance(this.inputEl, {value: new Date(this.scope.$data)});
232
233         var self = this;
234         combodate.$widget.find('select').bind('change', function(e) {
235           self.scope.$data = (new Date(combodate.getValue())).toISOString();
236         });
237       }
238     });
239   }
240 ]);
241 /*
242 Input types: text|email|tel|number|url|search|color|date|datetime|time|month|week
243 */
244
245 (function() {
246
247   var types = 'text|password|email|tel|number|url|search|color|date|datetime|time|month|week|file'.split('|');
248
249   //todo: datalist
250   
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+'">'
259         });
260     }]);
261   });
262
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">',
269         render: function() {
270           this.parent.render.call(this);
271           this.inputEl.after('<output>{{$data}}</output>');
272         }        
273       });
274   }]);
275
276 }());
277
278
279 // radiolist
280 angular.module('xeditable').directive('editableRadiolist', [
281   'editableDirectiveFactory',
282   'editableNgOptionsParser',
283   function(editableDirectiveFactory, editableNgOptionsParser) {
284     return editableDirectiveFactory({
285       directiveName: 'editableRadiolist',
286       inputTpl: '<span></span>',
287       render: function() {
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>';
293
294         this.inputEl.removeAttr('ng-model');
295         this.inputEl.removeAttr('ng-options');
296         this.inputEl.html(html);
297       },
298       autosubmit: function() {
299         var self = this;
300         self.inputEl.bind('change', function() {
301           setTimeout(function() {
302             self.scope.$apply(function() {
303               self.scope.$form.$submit();
304             });
305           }, 500);
306         });
307       }
308     });
309 }]);
310
311 //select
312 angular.module('xeditable').directive('editableSelect', ['editableDirectiveFactory',
313   function(editableDirectiveFactory) {
314     return editableDirectiveFactory({
315       directiveName: 'editableSelect',
316       inputTpl: '<select></select>',
317       autosubmit: function() {
318         var self = this;
319         self.inputEl.bind('change', function() {
320           self.scope.$apply(function() {
321             self.scope.$form.$submit();
322           });
323         });
324       }
325     });
326 }]);
327 //textarea
328 angular.module('xeditable').directive('editableTextarea', ['editableDirectiveFactory',
329   function(editableDirectiveFactory) {
330     return editableDirectiveFactory({
331       directiveName: 'editableTextarea',
332       inputTpl: '<textarea></textarea>',
333       addListeners: function() {
334         var self = this;
335         self.parent.addListeners.call(self);
336         // submit textarea by ctrl+enter even with buttons
337         if (self.single && self.buttons !== 'no') {
338           self.autosubmit();
339         }
340       },
341       autosubmit: function() {
342         var self = this;
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();
347             });
348           }
349         });
350       }
351     });
352 }]);
353
354 /**
355  * EditableController class. 
356  * Attached to element with `editable-xxx` directive.
357  *
358  * @namespace editable-element
359  */
360 /*
361 TODO: this file should be refactored to work more clear without closures!
362 */
363 angular.module('xeditable').factory('editableController', 
364   ['$q', 'editableUtils',
365   function($q, editableUtils) {
366
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) {
370     var valueGetter;
371
372     //if control is disabled - it does not participate in waiting process
373     var inWaiting;
374
375     var self = this;
376
377     self.scope = $scope;
378     self.elem = $element;
379     self.attrs = $attrs;
380     self.inputEl = null;
381     self.editorEl = null;
382     self.single = true;
383     self.error = '';
384     self.theme =  editableThemes[editableOptions.theme] || editableThemes['default'];
385     self.parent = {};
386
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];
389
390     //to be overwritten by directive
391     self.inputTpl = '';
392     self.directiveName = '';
393
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;
398
399     //runtime (defaults)
400     self.single = null;
401
402     /**
403      * Attributes defined with `e-*` prefix automatically transfered from original element to
404      * control.  
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).
408      * 
409      * @var {any|attribute} e-*
410      * @memberOf editable-element
411      */ 
412
413     /**
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. 
417      * 
418      * @var {string|attribute} buttons
419      * @memberOf editable-element
420      */    
421     self.buttons = 'right'; 
422     /**
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.
426      * 
427      * @var {string|attribute} blur
428      * @memberOf editable-element
429      */     
430     // no real `blur` property as it is transfered to editable form
431
432     //init
433     self.init = function(single) {
434       self.single = single;
435
436       self.name = $attrs.eName || $attrs[self.directiveName];
437       /*
438       if(!$attrs[directiveName] && !$attrs.eNgModel && ($attrs.eValue === undefined)) {
439         throw 'You should provide value for `'+directiveName+'` or `e-value` in editable element!';
440       }
441       */
442       if($attrs[self.directiveName]) {
443         valueGetter = $parse($attrs[self.directiveName]);
444       } else {
445         throw 'You should provide value for `'+self.directiveName+'` in editable element!';
446       }
447
448       // settings for single and non-single
449       if (!self.single) {
450         // hide buttons for non-single
451         self.buttons = 'no';
452       } else {
453         self.buttons = self.attrs.buttons || editableOptions.buttons;
454       }
455
456       //if name defined --> watch changes and update $data in form
457       if($attrs.eName) {
458         self.scope.$watch('$data', function(newVal){
459           self.scope.$form.$data[$attrs.eName] = newVal;
460         });
461       }
462
463       /**
464        * Called when control is shown.  
465        * See [demo](#select-remote).
466        * 
467        * @var {method|attribute} onshow
468        * @memberOf editable-element
469        */
470       if($attrs.onshow) {
471         self.onshow = function() {
472           return self.catchError($parse($attrs.onshow)($scope));
473         };
474       }
475
476       /**
477        * Called when control is hidden after both save or cancel.  
478        * 
479        * @var {method|attribute} onhide
480        * @memberOf editable-element
481        */
482       if($attrs.onhide) {
483         self.onhide = function() {
484           return $parse($attrs.onhide)($scope);
485         };
486       }
487
488       /**
489        * Called when control is cancelled.  
490        * 
491        * @var {method|attribute} oncancel
492        * @memberOf editable-element
493        */
494       if($attrs.oncancel) {
495         self.oncancel = function() {
496           return $parse($attrs.oncancel)($scope);
497         };
498       }          
499
500       /**
501        * Called during submit before value is saved to model.  
502        * See [demo](#onbeforesave).
503        * 
504        * @var {method|attribute} onbeforesave
505        * @memberOf editable-element
506        */
507       if ($attrs.onbeforesave) {
508         self.onbeforesave = function() {
509           return self.catchError($parse($attrs.onbeforesave)($scope));
510         };
511       }
512
513       /**
514        * Called during submit after value is saved to model.  
515        * See [demo](#onaftersave).
516        * 
517        * @var {method|attribute} onaftersave
518        * @memberOf editable-element
519        */
520       if ($attrs.onaftersave) {
521         self.onaftersave = function() {
522           return self.catchError($parse($attrs.onaftersave)($scope));
523         };
524       }
525
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();
532         self.handleEmpty();
533       });
534     };
535
536     self.render = function() {
537       var theme = self.theme;
538
539       //build input
540       self.inputEl = angular.element(self.inputTpl);
541
542       //build controls
543       self.controlsEl = angular.element(theme.controlsTpl);
544       self.controlsEl.append(self.inputEl);
545
546       //build buttons
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);
551         if(self.icon_set) {
552           self.submitEl.find('span').addClass(self.icon_set.ok);
553           self.cancelEl.find('span').addClass(self.icon_set.cancel);
554         }
555         self.buttonsEl.append(self.submitEl).append(self.cancelEl);
556         self.controlsEl.append(self.buttonsEl);
557         
558         self.inputEl.addClass('editable-has-buttons');
559       }
560
561       //build error
562       self.errorEl = angular.element(theme.errorTpl);
563       self.controlsEl.append(self.errorEl);
564
565       //build editor
566       self.editorEl = angular.element(self.single ? theme.formTpl : theme.noformTpl);
567       self.editorEl.append(self.controlsEl);
568
569       // transfer `e-*|data-e-*|x-e-*` attributes
570       for(var k in $attrs.$attr) {
571         if(k.length <= 1) {
572           continue;
573         }
574         var transferAttr = false;
575         var nextLetter = k.substring(1, 2);
576
577         // if starts with `e` + uppercase letter
578         if(k.substring(0, 1) === 'e' && nextLetter === nextLetter.toUpperCase()) {
579           transferAttr = k.substring(1); // cut `e`
580         } else {
581           continue;
582         }
583         
584         // exclude `form` and `ng-submit`, 
585         if(transferAttr === 'Form' || transferAttr === 'NgSubmit') {
586           continue;
587         }
588
589         // convert back to lowercase style
590         transferAttr = transferAttr.substring(0, 1).toLowerCase() + editableUtils.camelToDash(transferAttr.substring(1));  
591
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];
595
596         // set attributes to input
597         self.inputEl.attr(transferAttr, attrValue);
598       }
599
600       self.inputEl.addClass('editable-input');
601       self.inputEl.attr('ng-model', '$data');
602
603       // add directiveName class to editor, e.g. `editable-text`
604       self.editorEl.addClass(editableUtils.camelToDash(self.directiveName));
605
606       if(self.single) {
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));
610       }
611
612       //apply `postrender` method of theme
613       if(angular.isFunction(theme.postrender)) {
614         theme.postrender.call(self);
615       }
616
617     };
618
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);
626     };
627
628     //show
629     self.show = function() {
630       // set value of scope.$data
631       self.setLocalValue();
632
633       /*
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.
637       */
638       self.render();
639
640       // insert into DOM
641       $element.after(self.editorEl);
642
643       // compile (needed to attach ng-* events from markup)
644       $compile(self.editorEl)($scope);
645
646       // attach listeners (`escape`, autosubmit, etc)
647       self.addListeners();
648
649       // hide element
650       $element.addClass('editable-hide');
651
652       // onshow
653       return self.onshow();
654     };
655
656     //hide
657     self.hide = function() {
658       
659       self.editorEl.remove();
660       $element.removeClass('editable-hide');
661
662       // onhide
663       return self.onhide();
664     };
665
666     // cancel
667     self.cancel = function() {
668       // oncancel
669       self.oncancel();
670       // don't call hide() here as it called in form's code
671     };
672
673     /*
674     Called after show to attach listeners
675     */
676     self.addListeners = function() {
677       // bind keyup for `escape`
678       self.inputEl.bind('keyup', function(e) {
679           if(!self.single) {
680             return;
681           }
682
683           // todo: move this to editable-form!
684           switch(e.keyCode) {
685             // hide on `escape` press
686             case 27:
687               self.scope.$apply(function() {
688                 self.scope.$form.$cancel();
689               });
690             break;
691           }
692       });
693
694       // autosubmit when `no buttons`
695       if (self.single && self.buttons === 'no') {
696         self.autosubmit();
697       }
698
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) {
703           return;
704         }
705
706         if (self.scope.$form.$visible) {
707           self.scope.$form._clicked = true;
708         }
709       });
710     };
711
712     // setWaiting
713     self.setWaiting = function(value) {
714       if (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');
719         if (inWaiting) {
720           self.inputEl.attr('disabled', 'disabled');
721           if(self.buttonsEl) {
722             self.buttonsEl.find('button').attr('disabled', 'disabled');
723           }
724         }
725       } else {
726         if (inWaiting) {
727           self.inputEl.removeAttr('disabled');
728           if (self.buttonsEl) {
729             self.buttonsEl.find('button').removeAttr('disabled');
730           }
731         }
732       }
733     };
734
735     self.activate = function(start, end) {
736       setTimeout(function() {
737         var el = self.inputEl[0];
738         if (editableOptions.activate === 'focus' && el.focus) {
739           if(start){
740             end = end || start;
741             el.onfocus = function(){
742               var that = this;
743               setTimeout(function(){
744                 that.setSelectionRange(start,end);
745               });
746             };
747           }
748           el.focus();
749         }
750         if (editableOptions.activate === 'select' && el.select) {
751           el.select();
752         }
753       }, 0);
754     };
755
756     self.setError = function(msg) {
757       if(!angular.isObject(msg)) {
758         $scope.$error = msg;
759         self.error = msg;
760       }
761     };
762
763     /*
764     Checks that result is string or promise returned string and shows it as error message
765     Applied to onshow, onbeforesave, onaftersave
766     */
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);
773           }),
774           angular.bind(this, function(r) {
775             this.catchError(r, true);
776           })
777         );
778       //check $http error
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);
786       }
787       return result;
788     };
789
790     self.save = function() {
791       valueGetter.assign($scope.$parent,
792           self.useCopy ? angular.copy(self.scope.$data) : self.scope.$data);
793
794       // no need to call handleEmpty here as we are watching change of model value
795       // self.handleEmpty();
796     };
797
798     /*
799     attach/detach `editable-empty` class to element
800     */
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);
805     };
806
807     /*
808     Called when `buttons = "no"` to submit automatically
809     */
810     self.autosubmit = angular.noop;
811
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;
817   }
818
819   return EditableController;
820 }]);
821
822 /*
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)
829
830 - attach editableController to element
831
832 Depends on: editableController, editableFormFactory
833 */
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) {
837
838   //directive object
839   return function(overwrites) {
840     return {
841       restrict: 'A',
842       scope: true,
843       require: [overwrites.directiveName, '?^form'],
844       controller: editableController,
845       link: function(scope, elem, attrs, ctrl) {
846         // editable controller
847         var eCtrl = ctrl[0];
848
849         // form controller
850         var eFormCtrl;
851
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.ß
855         var hasForm = false;
856      
857         // element wrapped by form
858         if(ctrl[1]) {
859           eFormCtrl = ctrl[1];
860           hasForm = true;
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
864             eFormCtrl = getter;
865             hasForm = true;
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
870                 eFormCtrl = null;
871                 hasForm = true;
872                 break;
873               }
874             }
875           }
876         }
877
878         /*
879         if(hasForm && !attrs.eName) {
880           throw 'You should provide `e-name` for editable element inside form!';
881         }
882         */
883
884         //check for `editable-form` attr in form
885         /*
886         if(eFormCtrl && ) {
887           throw 'You should provide `e-name` for editable element inside form!';
888         }
889         */
890
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];
895           }
896         });
897
898         // merge overwrites to base editable controller
899         angular.extend(eCtrl, overwrites);
900
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;
905
906         if (disabled) {
907           return;
908         }
909         
910         // init editable ctrl
911         eCtrl.init(!hasForm);
912
913         // publich editable controller as `$editable` to be referenced in html
914         scope.$editable = eCtrl;
915
916         // add `editable` class to element
917         elem.addClass('editable');
918
919         // hasForm
920         if(hasForm) {
921           if(eFormCtrl) {
922             scope.$form = eFormCtrl;
923             if(!scope.$form.$addEditable) {
924               throw 'Form with editable elements should have `editable-form` attribute.';
925             }
926             scope.$form.$addEditable(eCtrl);
927           } else {
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
933           }
934         // !hasForm
935         } else {
936           // create editableform controller
937           scope.$form = editableFormController();
938           // add self to editable controller
939           scope.$form.$addEditable(eCtrl);
940
941           // if `e-form` provided, publish local $form in scope
942           if(attrs.eForm) {
943             scope.$parent[attrs.eForm] = scope.$form;
944           }
945
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) {
950               e.preventDefault();
951               e.editable = eCtrl;
952               scope.$apply(function(){
953                 scope.$form.$show();
954               });
955             });
956           }
957         }
958
959       }
960     };
961   };
962 }]);
963
964 /*
965 Returns editableForm controller
966 */
967 angular.module('xeditable').factory('editableFormController', 
968   ['$parse', '$document', '$rootScope', 'editablePromiseCollection', 'editableUtils',
969   function($parse, $document, $rootScope, editablePromiseCollection, editableUtils) {
970
971   // array of opened editable forms
972   var shown = [];
973
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) {
977       return true;
978     }
979
980     var node = child.parentNode;
981     while (node !== null) {
982       if (node == parent) {
983         return true;
984       }
985       node = node.parentNode;
986     }
987     return false;
988   };
989   
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) {
992     var isBlur = true;
993
994     var editables = shown.$editables;
995     angular.forEach(editables, function(v){
996       var element = v.editorEl[0];
997       if (isSelfOrDescendant(element, event.target))
998         isBlur = false;
999       
1000     });
1001     return isBlur;
1002   };
1003   
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()) {
1008       return;
1009     }
1010
1011     var toCancel = [];
1012     var toSubmit = [];
1013     for (var i=0; i<shown.length; i++) {
1014
1015       // exclude clicked
1016       if (shown[i]._clicked) {
1017         shown[i]._clicked = false;
1018         continue;
1019       }
1020
1021       // exclude waiting
1022       if (shown[i].$waiting) {
1023         continue;
1024       }
1025
1026       if (shown[i]._blur === 'cancel' && isBlur(shown[i], e)) {
1027         toCancel.push(shown[i]);
1028       }
1029
1030       if (shown[i]._blur === 'submit' && isBlur(shown[i], e)) {
1031         toSubmit.push(shown[i]);
1032       }
1033     }
1034
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(); });
1039       });
1040     }
1041   });
1042  
1043
1044   var base = {
1045     $addEditable: function(editable) {
1046       //console.log('add editable', editable.elem, editable.elem.bind);
1047       this.$editables.push(editable);
1048
1049       //'on' is not supported in angular 1.0.8
1050       editable.elem.bind('$destroy', angular.bind(this, this.$removeEditable, editable));
1051
1052       //bind editable's local $form to self (if not bound yet, below form) 
1053       if (!editable.scope.$form) {
1054         editable.scope.$form = this;
1055       }
1056
1057       //if form already shown - call show() of new editable
1058       if (this.$visible) {
1059         editable.catchError(editable.show());
1060       }
1061     },
1062
1063     $removeEditable: function(editable) {
1064       //arrayRemove
1065       for(var i=0; i < this.$editables.length; i++) {
1066         if(this.$editables[i] === editable) {
1067           this.$editables.splice(i, 1);
1068           return;
1069         }
1070       }
1071     },
1072
1073     /**
1074      * Shows form with editable controls.
1075      * 
1076      * @method $show()
1077      * @memberOf editable-form
1078      */
1079     $show: function() {
1080       if (this.$visible) {
1081         return;
1082       }
1083
1084       this.$visible = true;
1085
1086       var pc = editablePromiseCollection();
1087
1088       //own show
1089       pc.when(this.$onshow());
1090
1091       //clear errors
1092       this.$setError(null, '');
1093
1094       //children show
1095       angular.forEach(this.$editables, function(editable) {
1096         pc.when(editable.show());
1097       });
1098
1099       //wait promises and activate
1100       pc.then({
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)
1105       });
1106
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) {
1113           shown.push(this);
1114         }
1115       }), 0);      
1116     },
1117
1118     /**
1119      * Sets focus on form field specified by `name`.
1120      * 
1121      * @method $activate(name)
1122      * @param {string} name name of field
1123      * @memberOf editable-form
1124      */
1125     $activate: function(name) {
1126       var i;
1127       if (this.$editables.length) {
1128         //activate by name
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();
1133               return;
1134             }
1135           }
1136         }
1137
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();
1142             return;
1143           }
1144         }
1145
1146         //by default activate first field
1147         this.$editables[0].activate(this.$editables[0].elem[0].selectionStart, this.$editables[0].elem[0].selectionEnd);
1148       }
1149     },
1150
1151     /**
1152      * Hides form with editable controls without saving.
1153      * 
1154      * @method $hide()
1155      * @memberOf editable-form
1156      */
1157     $hide: function() {
1158       if (!this.$visible) {
1159         return;
1160       }      
1161       this.$visible = false;
1162       // self hide
1163       this.$onhide();
1164       // children's hide
1165       angular.forEach(this.$editables, function(editable) {
1166         editable.hide();
1167       });
1168
1169       // remove from internal list of shown forms
1170       editableUtils.arrayRemove(shown, this);
1171     },
1172
1173     /**
1174      * Triggers `oncancel` event and calls `$hide()`.
1175      * 
1176      * @method $cancel()
1177      * @memberOf editable-form
1178      */
1179     $cancel: function() {
1180       if (!this.$visible) {
1181         return;
1182       }      
1183       // self cancel
1184       this.$oncancel();
1185       // children's cancel      
1186       angular.forEach(this.$editables, function(editable) {
1187         editable.cancel();
1188       });
1189       // self hide
1190       this.$hide();
1191     },    
1192
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);
1199       });
1200     },
1201
1202     /**
1203      * Shows error message for particular field.
1204      * 
1205      * @method $setError(name, msg)
1206      * @param {string} name name of field
1207      * @param {string} msg error message
1208      * @memberOf editable-form
1209      */
1210     $setError: function(name, msg) {
1211       angular.forEach(this.$editables, function(editable) {
1212         if(!name || editable.name === name) {
1213           editable.setError(msg);
1214         }
1215       });
1216     },
1217
1218     $submit: function() {
1219       if (this.$waiting) {
1220         return;
1221       } 
1222
1223       //clear errors
1224       this.$setError(null, '');
1225
1226       //children onbeforesave
1227       var pc = editablePromiseCollection();
1228       angular.forEach(this.$editables, function(editable) {
1229         pc.when(editable.onbeforesave());
1230       });
1231
1232       /*
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
1237       */
1238       pc.then({
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)
1243       });
1244
1245       //save
1246       function checkSelf(childrenTrue){
1247         var pc = editablePromiseCollection();
1248         pc.when(this.$onbeforesave());
1249         pc.then({
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)
1254         });
1255       }
1256     },
1257
1258     $save: function() {
1259       // write model for each editable
1260       angular.forEach(this.$editables, function(editable) {
1261         editable.save();
1262       });
1263
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());
1269       });
1270
1271       /*
1272       onaftersave result:
1273       - true/undefined/false: just close form
1274       - string: keep form open and show error
1275       */
1276       pc.then({
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)
1281       });
1282     },
1283
1284     $onshow: angular.noop,
1285     $oncancel: angular.noop,
1286     $onhide: angular.noop,
1287     $onbeforesave: angular.noop,
1288     $onaftersave: angular.noop
1289   };
1290
1291   return function() {
1292     return angular.extend({
1293       $editables: [],
1294       /**
1295        * Form visibility flag.
1296        * 
1297        * @var {bool} $visible
1298        * @memberOf editable-form
1299        */
1300       $visible: false,
1301       /**
1302        * Form waiting flag. It becomes `true` when form is loading or saving data.
1303        * 
1304        * @var {bool} $waiting
1305        * @memberOf editable-form
1306        */
1307       $waiting: false,
1308       $data: {},
1309       _clicked: false,
1310       _blur: null
1311     }, base);
1312   };
1313 }]);
1314
1315 /**
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.
1318  *
1319  * @namespace editable-form
1320  */
1321 angular.module('xeditable').directive('editableForm',
1322   ['$rootScope', '$parse', 'editableFormController', 'editableOptions',
1323   function($rootScope, $parse, editableFormController, editableOptions) {
1324     return {
1325       restrict: 'A',
1326       require: ['form'],
1327       //require: ['form', 'editableForm'],
1328       //controller: EditableFormController,
1329       compile: function() {
1330         return {
1331           pre: function(scope, elem, attrs, ctrl) {
1332             var form = ctrl[0];
1333             var eForm;
1334
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);
1341               } else {
1342                 eForm = editableFormController();
1343                 scope[attrs.editableForm] = eForm;
1344                 angular.extend(eForm, form);
1345               }
1346             } else { //just merge to form and publish if form has name
1347               eForm = editableFormController();
1348               angular.extend(form, eForm);
1349             }
1350
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);
1357               });
1358               delete buf[name];
1359             }
1360           },
1361           post: function(scope, elem, attrs, ctrl) {
1362             var eForm;
1363
1364             if(attrs.editableForm && scope[attrs.editableForm] && scope[attrs.editableForm].$show) {
1365               eForm = scope[attrs.editableForm];
1366             } else {
1367               eForm = ctrl[0];
1368             }
1369
1370             /**
1371              * Called when form is shown.
1372              * 
1373              * @var {method|attribute} onshow 
1374              * @memberOf editable-form
1375              */
1376             if(attrs.onshow) {
1377               eForm.$onshow = angular.bind(eForm, $parse(attrs.onshow), scope);
1378             }
1379
1380             /**
1381              * Called when form hides after both save or cancel.
1382              * 
1383              * @var {method|attribute} onhide 
1384              * @memberOf editable-form
1385              */
1386             if(attrs.onhide) {
1387               eForm.$onhide = angular.bind(eForm, $parse(attrs.onhide), scope);
1388             }
1389
1390             /**
1391              * Called when form is cancelled.
1392              * 
1393              * @var {method|attribute} oncancel
1394              * @memberOf editable-form
1395              */
1396             if(attrs.oncancel) {
1397               eForm.$oncancel = angular.bind(eForm, $parse(attrs.oncancel), scope);
1398             }
1399
1400             /**
1401              * Whether form initially rendered in shown state.
1402              *
1403              * @var {bool|attribute} shown
1404              * @memberOf editable-form
1405              */
1406             if(attrs.shown && $parse(attrs.shown)(scope)) {
1407               eForm.$show();
1408             }
1409
1410             /**
1411              * Action when form losses focus. Values: `cancel|submit|ignore`.
1412              * Default is `ignore`.
1413              * 
1414              * @var {string|attribute} blur
1415              * @memberOf editable-form
1416              */
1417             eForm._blur = attrs.blur || editableOptions.blurForm;
1418
1419             // onbeforesave, onaftersave
1420             if(!attrs.ngSubmit && !attrs.submit) {
1421               /**
1422                * Called after all children `onbeforesave` callbacks but before saving form values
1423                * to model.  
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.
1426                * 
1427                * @var {method|attribute} onbeforesave
1428                * @memberOf editable-form
1429                * 
1430                */
1431               if(attrs.onbeforesave) {
1432                 eForm.$onbeforesave = function() {
1433                   return $parse(attrs.onbeforesave)(scope, {$data: eForm.$data});
1434                 };
1435               }
1436
1437               /**
1438                * Called when form values are saved to model.  
1439                * See [editable-form demo](#editable-form) for details.
1440                * 
1441                * @var {method|attribute} onaftersave 
1442                * @memberOf editable-form
1443                * 
1444                */
1445               if(attrs.onaftersave) {
1446                 eForm.$onaftersave = function() {
1447                   return $parse(attrs.onaftersave)(scope, {$data: eForm.$data});
1448                 };
1449               }
1450
1451               elem.bind('submit', function(event) {
1452                 event.preventDefault();
1453                 scope.$apply(function() {
1454                   eForm.$submit();
1455                 });
1456               });
1457             }
1458
1459
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) {
1464                 return;
1465               }
1466
1467               if (eForm.$visible) {
1468                 eForm._clicked = true;
1469               }
1470             });   
1471
1472           }
1473         };
1474       }
1475     };
1476 }]);
1477 /**
1478  * editablePromiseCollection
1479  *  
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
1485  */
1486
1487 angular.module('xeditable').factory('editablePromiseCollection', ['$q', function($q) { 
1488
1489   function promiseCollection() {
1490     return {
1491       promises: [],
1492       hasFalse: false,
1493       hasString: false,
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
1502           return;
1503         }
1504       },
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;
1512
1513         var self = this;
1514
1515         if (this.promises.length) {
1516           onWait(true);
1517           $q.all(this.promises).then(
1518             //all resolved       
1519             function(results) {
1520               onWait(false);
1521               //check all results via same `when` method (without checking promises)
1522               angular.forEach(results, function(result) {
1523                 self.when(result, true);  
1524               });
1525               applyCallback();
1526             },
1527             //some rejected
1528             function(error) { 
1529               onWait(false);
1530               onString();
1531             }
1532             );
1533         } else {
1534           applyCallback();
1535         }
1536
1537         function applyCallback() {
1538           if (!self.hasString && !self.hasFalse) {
1539             onTrue();
1540           } else if (!self.hasString && self.hasFalse) {
1541             onFalse();
1542           } else {
1543             onString();
1544           }
1545         }
1546
1547       }
1548     };
1549   }
1550
1551   return promiseCollection;
1552
1553 }]);
1554
1555 /**
1556  * editableUtils
1557  */
1558  angular.module('xeditable').factory('editableUtils', [function() {
1559   return {
1560     indexOf: function (array, obj) {
1561       if (array.indexOf) return array.indexOf(obj);
1562
1563       for ( var i = 0; i < array.length; i++) {
1564         if (obj === array[i]) return i;
1565       }
1566       return -1;
1567     },
1568
1569     arrayRemove: function (array, value) {
1570       var index = this.indexOf(array, value);
1571       if (index >= 0) {
1572         array.splice(index, 1);
1573       }
1574       return value;
1575     },
1576
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();
1582       });
1583     },
1584
1585     dashToCamel: function(str) {
1586       var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g;
1587       var MOZ_HACK_REGEXP = /^moz([A-Z])/;
1588       return str.
1589       replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) {
1590         return offset ? letter.toUpperCase() : letter;
1591       }).
1592       replace(MOZ_HACK_REGEXP, 'Moz$1');
1593     }
1594   };
1595 }]);
1596
1597 /**
1598  * editableNgOptionsParser
1599  *
1600  * see: https://github.com/angular/angular.js/blob/master/src/ng/directive/select.js#L131
1601  */
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+(.*?))?$/;
1605
1606   function parser(optionsExp) {
1607     var match;
1608
1609     if (! (match = optionsExp.match(NG_OPTIONS_REGEXP))) {
1610       throw 'ng-options parse error';
1611     }
1612
1613     var 
1614     displayFn = match[2] || match[1],
1615     valueName = match[4] || match[6],
1616     keyName = match[5],
1617     groupByFn = match[3] || '',
1618     valueFn = match[2] ? match[1] : valueName,
1619     valuesFn = match[7],
1620     track = match[8],
1621     trackFn = track ? match[8] : null;
1622
1623     var ngRepeat;
1624     if (keyName === undefined) { // array
1625       ngRepeat = valueName + ' in ' + valuesFn;
1626       if (track !== undefined) {
1627         ngRepeat += ' track by '+trackFn;
1628       }
1629     } else { // object
1630       ngRepeat = '('+keyName+', '+valueName+') in '+valuesFn;
1631     }
1632     
1633     // group not supported yet
1634     return {
1635       ngRepeat: ngRepeat,
1636       locals: {
1637         valueName: valueName,
1638         keyName: keyName,
1639         valueFn: valueFn,
1640         displayFn: displayFn
1641       }
1642     };
1643   }
1644
1645   return parser;
1646 }]);
1647
1648 /**
1649  * editableCombodate
1650  *
1651  * angular version of https://github.com/vitalets/combodate
1652  */
1653 angular.module('xeditable').factory('editableCombodate', [function() {
1654   function Combodate(element, options) {
1655     this.$element = angular.element(element);
1656
1657     if(this.$element[0].nodeName != 'INPUT') {
1658       throw 'Combodate should be applied to INPUT element';
1659     }
1660
1661     this.defaults = {
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()`
1667       value: null,
1668       minYear: 1970,
1669       maxYear: 2015,
1670       yearDescending: true,
1671       minuteStep: 5,
1672       secondStep: 1,
1673       firstItem: 'empty', //'name', 'empty', 'none'
1674       errorClass: null,
1675       customClass: '',
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
1678     };
1679
1680     this.options = angular.extend({}, this.defaults, options);
1681     this.init();
1682   }
1683
1684   Combodate.prototype = {
1685     constructor: Combodate,
1686     init: function () {
1687       this.map = {
1688         //key   regexp    moment.method
1689         day:    ['D',    'date'], 
1690         month:  ['M',    'month'], 
1691         year:   ['Y',    'year'], 
1692         hour:   ['[Hh]', 'hours'],
1693         minute: ['m',    'minutes'], 
1694         second: ['s',    'seconds'],
1695         ampm:   ['[Aa]', ''] 
1696       };
1697       
1698       this.$widget = angular.element('<span class="combodate"></span>').html(this.getTemplate());
1699       
1700       this.initCombos();
1701       
1702       if (this.options.smartDays) {
1703         var combo = this;
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');
1708           }
1709         });        
1710       }
1711
1712       this.$widget.find('select').css('width', 'auto');
1713
1714       // hide original input and insert widget                                       
1715       this.$element.css('display', 'none').after(this.$widget);
1716       
1717       // set initial value
1718       this.setValue(this.$element.val() || this.options.value);
1719     },
1720     
1721     /*
1722      Replace tokens in template with <select> elements 
1723      */         
1724      getTemplate: function() {
1725       var tpl = this.options.template;
1726       var customClass = this.options.customClass;
1727
1728       //first pass
1729       angular.forEach(this.map, function(v, k) {
1730         v = v[0]; 
1731         var r = new RegExp(v+'+');
1732         var token = v.length > 1 ? v.substring(1, 2) : v;
1733         
1734         tpl = tpl.replace(r, '{'+token+'}');
1735       });
1736
1737       //replace spaces with &nbsp;
1738       tpl = tpl.replace(/ /g, '&nbsp;');
1739
1740       //second pass
1741       angular.forEach(this.map, function(v, k) {
1742         v = v[0];
1743         var token = v.length > 1 ? v.substring(1, 2) : v;
1744
1745         tpl = tpl.replace('{'+token+'}', '<select class="'+k+' '+customClass+'"></select>');
1746       });   
1747
1748       return tpl;
1749     },
1750     
1751     /*
1752      Initialize combos that presents in template 
1753      */        
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;
1759         // fill with items
1760         this.fillCombo(k);
1761       }
1762     },
1763
1764     /*
1765      Fill combo with items 
1766      */        
1767      fillCombo: function(k) {
1768       var $combo = this['$'+k];
1769       if (!$combo) {
1770         return;
1771       }
1772
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();
1777
1778       $combo.html('');
1779       for(var i=0; i<items.length; i++) {
1780         $combo.append('<option value="'+items[i][0]+'">'+items[i][1]+'</option>');
1781       }
1782
1783       $combo.val(value);
1784     },
1785
1786     /*
1787      Initialize items of combos. Handles `firstItem` option 
1788      */
1789      fillCommon: function(key) {
1790       var values = [], relTime;
1791
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(['', '']);
1801       }
1802       return values;
1803     },  
1804
1805
1806     /*
1807     fill day
1808     */
1809     fillDay: function() {
1810       var items = this.fillCommon('d'), name, i,
1811       twoDigit = this.options.template.indexOf('DD') !== -1,
1812       daysCount = 31;
1813
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);
1819
1820         if (!isNaN(month) && !isNaN(year)) {
1821           daysCount = moment([year, month]).daysInMonth();
1822         }
1823       }
1824
1825       for (i = 1; i <= daysCount; i++) {
1826         name = twoDigit ? this.leadZero(i) : i;
1827         items.push([i, name]);
1828       }
1829       return items;
1830     },
1831     
1832     /*
1833     fill month
1834     */
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;
1840
1841       for(i=0; i<=11; i++) {
1842         if(longNames) {
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);
1849         } else {
1850           name = i+1;
1851         }
1852         items.push([i, name]);
1853       } 
1854       return items;
1855     },
1856     
1857     /*
1858     fill year
1859     */
1860     fillYear: function() {
1861       var items = [], name, i, 
1862       longNames = this.options.template.indexOf('YYYY') !== -1;
1863
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]);
1867       }
1868       
1869       items = this.fillCommon('y').concat(items);
1870       
1871       return items;
1872     },
1873     
1874     /*
1875     fill hour
1876     */
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,
1882       min = h12 ? 1 : 0, 
1883       max = h12 ? 12 : 23;
1884
1885       for(i=min; i<=max; i++) {
1886         name = twoDigit ? this.leadZero(i) : i;
1887         items.push([i, name]);
1888       } 
1889       return items;
1890     },
1891
1892     /*
1893     fill minute
1894     */
1895     fillMinute: function() {
1896       var items = this.fillCommon('m'), name, i,
1897       twoDigit = this.options.template.indexOf('mm') !== -1;
1898
1899       for(i=0; i<=59; i+= this.options.minuteStep) {
1900         name = twoDigit ? this.leadZero(i) : i;
1901         items.push([i, name]);
1902       }
1903       return items;
1904     },
1905     
1906     /*
1907     fill second
1908     */
1909     fillSecond: function() {
1910       var items = this.fillCommon('s'), name, i,
1911       twoDigit = this.options.template.indexOf('ss') !== -1;
1912
1913       for(i=0; i<=59; i+= this.options.secondStep) {
1914         name = twoDigit ? this.leadZero(i) : i;
1915         items.push([i, name]);
1916       }    
1917       return items;
1918     },
1919     
1920     /*
1921     fill ampm
1922     */
1923     fillAmpm: function() {
1924       var ampmL = this.options.template.indexOf('a') !== -1,
1925       ampmU = this.options.template.indexOf('A') !== -1,            
1926       items = [
1927       ['am', ampmL ? 'am' : 'AM'],
1928       ['pm', ampmL ? 'pm' : 'PM']
1929       ];
1930       return items;
1931     },
1932
1933     /*
1934      Returns current date value from combos. 
1935      If format not specified - `options.format` used.
1936      If format = `null` - Moment object returned.
1937      */
1938      getValue: function(format) {
1939       var dt, values = {}, 
1940       that = this,
1941       notSelected = false;
1942
1943       //getting selected values    
1944       angular.forEach(this.map, function(v, k) {
1945         if(k === 'ampm') {
1946           return;
1947         }
1948         var def = k === 'day' ? 1 : 0;
1949
1950         values[k] = that['$'+k] ? parseInt(that['$'+k].val(), 10) : def; 
1951         
1952         if(isNaN(values[k])) {
1953          notSelected = true;
1954          return false; 
1955        }
1956      });
1957       
1958       //if at least one visible combo not selected - return empty string
1959       if(notSelected) {
1960        return '';
1961      }
1962
1963       //convert hours 12h --> 24h 
1964       if(this.$ampm) {
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;                    
1968         } else {
1969           values.hour = this.$ampm.val() === 'am' ? values.hour : values.hour+12;
1970         }
1971       }
1972       
1973       dt = moment([values.year, values.month, values.day, values.hour, values.minute, values.second]);
1974       
1975       //highlight invalid date
1976       this.highlight(dt);
1977
1978       format = format === undefined ? this.options.format : format;
1979       if(format === null) {
1980        return dt.isValid() ? dt : null; 
1981      } else {
1982        return dt.isValid() ? dt.format(format) : ''; 
1983      }
1984    },
1985
1986    setValue: function(value) {
1987     if(!value) {
1988       return;
1989     }
1990
1991       // parse in strict mode (third param `true`)
1992       var dt = typeof value === 'string' ? moment(value, this.options.format, true) : moment(value),
1993       that = this,
1994       values = {};
1995       
1996       //function to find nearest value in select options
1997       function getNearest($select, value) {
1998         var delta = {};
1999         angular.forEach($select.children('option'), function(opt, i){
2000           var optValue = angular.element(opt).attr('value');
2001
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};
2006           } 
2007         }); 
2008         return delta.value;
2009       }
2010       
2011       if(dt.isValid()) {
2012         //read values from date object
2013         angular.forEach(this.map, function(v, k) {
2014           if(k === 'ampm') {
2015             return; 
2016           }
2017           values[k] = dt[v[1]]();
2018         });
2019
2020         if(this.$ampm) {
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) {
2023             values.ampm = 'pm';
2024             if(values.hour > 12) {
2025               values.hour -= 12;
2026             }
2027           } else {
2028             values.ampm = 'am';
2029             if(values.hour === 0) {
2030               values.hour = 12;
2031             }
2032           }
2033         }
2034
2035         angular.forEach(values, function(v, k) {
2036           //call val() for each existing combo, e.g. this.$hour.val()
2037           if(that['$'+k]) {
2038
2039             if(k === 'minute' && that.options.minuteStep > 1 && that.options.roundTime) {
2040              v = getNearest(that['$'+k], v);
2041            }
2042            
2043            if(k === 'second' && that.options.secondStep > 1 && that.options.roundTime) {
2044              v = getNearest(that['$'+k], v);
2045            }                       
2046            
2047            that['$'+k].val(v);
2048          }
2049        });
2050
2051         // update days count
2052         if (this.options.smartDays) {
2053           this.fillCombo('day');
2054         }
2055
2056         this.$element.val(dt.format(this.options.format)).triggerHandler('change');
2057       }
2058     },
2059     
2060     /*
2061      highlight combos if date is invalid
2062      */
2063      highlight: function(dt) {
2064       if(!dt.isValid()) {
2065         if(this.options.errorClass) {
2066           this.$widget.addClass(this.options.errorClass);
2067         } else {
2068           //store original border color
2069           if(!this.borderColor) {
2070             this.borderColor = this.$widget.find('select').css('border-color'); 
2071           }
2072           this.$widget.find('select').css('border-color', 'red');
2073         }
2074       } else {
2075         if(this.options.errorClass) {
2076           this.$widget.removeClass(this.options.errorClass);
2077         } else {
2078           this.$widget.find('select').css('border-color', this.borderColor);
2079         }  
2080       }
2081     },
2082     
2083     leadZero: function(v) {
2084       return v <= 9 ? '0' + v : v; 
2085     },
2086     
2087     destroy: function() {
2088       this.$widget.remove();
2089       this.$element.removeData('combodate').show();
2090     }
2091
2092   };
2093
2094   return {
2095     getInstance: function(element, options) {
2096       return new Combodate(element, options);
2097     }
2098   };
2099 }]);
2100
2101 /*
2102 Editable icons:
2103 - default
2104 - font-awesome
2105
2106 */
2107 angular.module('xeditable').factory('editableIcons', function() {
2108
2109   var icons = {
2110     //Icon-set to use, defaults to bootstrap icons
2111     default: {
2112       'bs2': {
2113         ok: 'icon-ok icon-white',
2114         cancel: 'icon-remove'
2115       },
2116       'bs3': {
2117         ok: 'glyphicon glyphicon-ok',
2118         cancel: 'glyphicon glyphicon-remove'
2119       }
2120     },
2121     external: {
2122       'font-awesome': {
2123         ok: 'fa fa-check',
2124         cancel: 'fa fa-times'
2125       }
2126     }
2127   };
2128
2129   return icons;
2130 });
2131
2132 /*
2133 Editable themes:
2134 - default
2135 - bootstrap 2
2136 - bootstrap 3
2137
2138 Note: in postrender() `this` is instance of editableController
2139 */
2140 angular.module('xeditable').factory('editableThemes', function() {
2141   var themes = {
2142     //default
2143     'default': {
2144       formTpl:      '<form class="editable-wrap"></form>',
2145       noformTpl:    '<span class="editable-wrap"></span>',
2146       controlsTpl:  '<span class="editable-controls"></span>',
2147       inputTpl:     '',
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>'
2152     },
2153
2154     //bs2
2155     'bs2': {
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>',
2159       inputTpl:    '',
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()">'+
2164                       '<span></span>'+
2165                    '</button>'
2166
2167     },
2168
2169     //bs3
2170     'bs3': {
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>',
2174       inputTpl:    '',
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()">'+
2179                      '<span></span>'+
2180                    '</button>',
2181
2182       //bs3 specific prop to change buttons class: btn-sm, btn-lg
2183       buttonsClass: '',
2184       //bs3 specific prop to change standard inputs class: input-sm, input-lg
2185       inputClass: '',
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':
2193           case 'editableTel':
2194           case 'editableNumber':
2195           case 'editableUrl':
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')) {
2209                   break;
2210               }
2211               this.inputEl.addClass(this.theme.inputClass);
2212             }
2213           break;
2214           case 'editableCheckbox':
2215               this.editorEl.addClass('checkbox');
2216         }
2217
2218         //apply buttonsClass (bs3 specific!)
2219         if(this.buttonsEl && this.theme.buttonsClass) {
2220           this.buttonsEl.find('button').addClass(this.theme.buttonsClass);
2221         }
2222       }
2223     }
2224   };
2225
2226   return themes;
2227 });