PATH:
home
/
letacommog
/
crmleta
/
consumerportal
/
libraries
/
angularjsaddons
/
angular-xeditable
/
js
/*! angular-xeditable - 0.1.8 Edit-in-place for angular.js Build date: 2014-01-10 */ /** * Angular-xeditable module * */ angular.module( 'xeditable', [] ) /** * Default options. * * @namespace editable-options */ //todo: maybe better have editableDefaults, not options... .value( 'editableOptions', { /** * Theme. Possible values `bs3`, `bs2`, `default`. * * @var {string} theme * @memberOf editable-options */ theme: 'default', /** * Whether to show buttons for single editalbe element. * Possible values `right` (default), `no`. * * @var {string} buttons * @memberOf editable-options */ buttons: 'right', /** * Default value for `blur` attribute of single editable element. * Can be `cancel|submit|ignore`. * * @var {string} blurElem * @memberOf editable-options */ blurElem: 'cancel', /** * Default value for `blur` attribute of editable form. * Can be `cancel|submit|ignore`. * * @var {string} blurForm * @memberOf editable-options */ blurForm: 'ignore', /** * How input elements get activated. Possible values: `focus|select|none`. * * @var {string} activate * @memberOf editable-options */ activate: 'focus' } ); /* Angular-ui bootstrap datepicker http://angular-ui.github.io/bootstrap/#/datepicker */ angular.module( 'xeditable' ) .directive( 'editableBsdate', [ 'editableDirectiveFactory', function ( editableDirectiveFactory ) { return editableDirectiveFactory( { directiveName: 'editableBsdate', inputTpl: '<input type="text">' } ); } ] ); /* Angular-ui bootstrap editable timepicker http://angular-ui.github.io/bootstrap/#/timepicker */ angular.module( 'xeditable' ) .directive( 'editableBstime', [ 'editableDirectiveFactory', function ( editableDirectiveFactory ) { return editableDirectiveFactory( { directiveName: 'editableBstime', inputTpl: '<timepicker></timepicker>', render: function () { this.parent.render.call( this ); // timepicker can't update model when ng-model set directly to it // see: https://github.com/angular-ui/bootstrap/issues/1141 // so we wrap it into DIV var div = angular.element( '<div class="well well-small" style="display:inline-block;"></div>' ); // move ng-model to wrapping div div.attr( 'ng-model', this.inputEl.attr( 'ng-model' ) ); this.inputEl.removeAttr( 'ng-model' ); // move ng-change to wrapping div if ( this.attrs.eNgChange ) { div.attr( 'ng-change', this.inputEl.attr( 'ng-change' ) ); this.inputEl.removeAttr( 'ng-change' ); } // wrap this.inputEl.wrap( div ); } } ); } ] ); //checkbox angular.module( 'xeditable' ) .directive( 'editableCheckbox', [ 'editableDirectiveFactory', function ( editableDirectiveFactory ) { return editableDirectiveFactory( { directiveName: 'editableCheckbox', inputTpl: '<input type="checkbox">', render: function () { this.parent.render.call( this ); if ( this.attrs.eTitle ) { this.inputEl.wrap( '<label></label>' ); this.inputEl.after( angular.element( '<span></span>' ) .text( this.attrs.eTitle ) ); } }, autosubmit: function () { var self = this; self.inputEl.bind( 'change', function () { setTimeout( function () { self.scope.$apply( function () { self.scope.$form.$submit(); } ); }, 500 ); } ); } } ); } ] ); // checklist angular.module( 'xeditable' ) .directive( 'editableChecklist', [ 'editableDirectiveFactory', 'editableNgOptionsParser', function ( editableDirectiveFactory, editableNgOptionsParser ) { return editableDirectiveFactory( { directiveName: 'editableChecklist', inputTpl: '<span></span>', useCopy: true, render: function () { this.parent.render.call( this ); var parsed = editableNgOptionsParser( this.attrs.eNgOptions ); var html = '<label ng-repeat="' + parsed.ngRepeat + '">' + '<input type="checkbox" checklist-model="$parent.$data" checklist-value="' + parsed.locals.valueFn + '">' + '<span ng-bind="' + parsed.locals.displayFn + '"></span></label>'; this.inputEl.removeAttr( 'ng-model' ); this.inputEl.removeAttr( 'ng-options' ); this.inputEl.html( html ); } } ); } ] ); /* Input types: text|email|tel|number|url|search|color|date|datetime|time|month|week */ ( function () { var types = 'text|email|tel|number|url|search|color|date|datetime|time|month|week'.split( '|' ); //todo: datalist // generate directives angular.forEach( types, function ( type ) { var directiveName = 'editable' + type.charAt( 0 ) .toUpperCase() + type.slice( 1 ); angular.module( 'xeditable' ) .directive( directiveName, [ 'editableDirectiveFactory', function ( editableDirectiveFactory ) { return editableDirectiveFactory( { directiveName: directiveName, inputTpl: '<input type="' + type + '">' } ); } ] ); } ); //`range` is bit specific angular.module( 'xeditable' ) .directive( 'editableRange', [ 'editableDirectiveFactory', function ( editableDirectiveFactory ) { return editableDirectiveFactory( { directiveName: 'editableRange', inputTpl: '<input type="range" id="range" name="range">', render: function () { this.parent.render.call( this ); this.inputEl.after( '<output>{{$data}}</output>' ); } } ); } ] ); }() ); // radiolist angular.module( 'xeditable' ) .directive( 'editableRadiolist', [ 'editableDirectiveFactory', 'editableNgOptionsParser', function ( editableDirectiveFactory, editableNgOptionsParser ) { return editableDirectiveFactory( { directiveName: 'editableRadiolist', inputTpl: '<span></span>', render: function () { this.parent.render.call( this ); var parsed = editableNgOptionsParser( this.attrs.eNgOptions ); var html = '<label ng-repeat="' + parsed.ngRepeat + '">' + '<input type="radio" ng-model="$parent.$data" value="{{' + parsed.locals.valueFn + '}}">' + '<span ng-bind="' + parsed.locals.displayFn + '"></span></label>'; this.inputEl.removeAttr( 'ng-model' ); this.inputEl.removeAttr( 'ng-options' ); this.inputEl.html( html ); }, autosubmit: function () { var self = this; self.inputEl.bind( 'change', function () { setTimeout( function () { self.scope.$apply( function () { self.scope.$form.$submit(); } ); }, 500 ); } ); } } ); } ] ); //select angular.module( 'xeditable' ) .directive( 'editableSelect', [ 'editableDirectiveFactory', function ( editableDirectiveFactory ) { return editableDirectiveFactory( { directiveName: 'editableSelect', inputTpl: '<select></select>', autosubmit: function () { var self = this; self.inputEl.bind( 'change', function () { self.scope.$apply( function () { self.scope.$form.$submit(); } ); } ); } } ); } ] ); //textarea angular.module( 'xeditable' ) .directive( 'editableTextarea', [ 'editableDirectiveFactory', function ( editableDirectiveFactory ) { return editableDirectiveFactory( { directiveName: 'editableTextarea', inputTpl: '<textarea></textarea>', addListeners: function () { var self = this; self.parent.addListeners.call( self ); // submit textarea by ctrl+enter even with buttons if ( self.single && self.buttons !== 'no' ) { self.autosubmit(); } }, autosubmit: function () { var self = this; self.inputEl.bind( 'keydown', function ( e ) { if ( ( e.ctrlKey || e.metaKey ) && ( e.keyCode === 13 ) ) { self.scope.$apply( function () { self.scope.$form.$submit(); } ); } } ); } } ); } ] ); /** * EditableController class. * Attached to element with `editable-xxx` directive. * * @namespace editable-element */ /* TODO: this file should be refactored to work more clear without closures! */ angular.module( 'xeditable' ) .factory( 'editableController', [ '$q', 'editableUtils', function ( $q, editableUtils ) { //EditableController function EditableController.$inject = [ '$scope', '$attrs', '$element', '$parse', 'editableThemes', 'editableOptions', '$rootScope', '$compile', '$q' ]; function EditableController( $scope, $attrs, $element, $parse, editableThemes, editableOptions, $rootScope, $compile, $q ) { var valueGetter; //if control is disabled - it does not participate in waiting process var inWaiting; var self = this; self.scope = $scope; self.elem = $element; self.attrs = $attrs; self.inputEl = null; self.editorEl = null; self.single = true; self.error = ''; self.theme = editableThemes[ editableOptions.theme ] || editableThemes[ 'default' ]; self.parent = {}; //to be overwritten by directive self.inputTpl = ''; self.directiveName = ''; // with majority of controls copy is not needed, but.. // copy MUST NOT be used for `select-multiple` with objects as items // copy MUST be used for `checklist` self.useCopy = false; //runtime (defaults) self.single = null; /** * Attributes defined with `e-*` prefix automatically transfered from original element to * control. * For example, if you set `<span editable-text="user.name" e-style="width: 100px"`> * then input will appear as `<input style="width: 100px">`. * See [demo](#text-customize). * * @var {any|attribute} e-* * @memberOf editable-element */ /** * Whether to show ok/cancel buttons. Values: `right|no`. * If set to `no` control automatically submitted when value changed. * If control is part of form buttons will never be shown. * * @var {string|attribute} buttons * @memberOf editable-element */ self.buttons = 'right'; /** * Action when control losses focus. Values: `cancel|submit|ignore`. * Has sense only for single editable element. * Otherwise, if control is part of form - you should set `blur` of form, not of individual element. * * @var {string|attribute} blur * @memberOf editable-element */ // no real `blur` property as it is transfered to editable form //init self.init = function ( single ) { self.single = single; self.name = $attrs.eName || $attrs[ self.directiveName ]; /* if(!$attrs[directiveName] && !$attrs.eNgModel && ($attrs.eValue === undefined)) { throw 'You should provide value for `'+directiveName+'` or `e-value` in editable element!'; } */ if ( $attrs[ self.directiveName ] ) { valueGetter = $parse( $attrs[ self.directiveName ] ); } else { throw 'You should provide value for `' + self.directiveName + '` in editable element!'; } // settings for single and non-single if ( !self.single ) { // hide buttons for non-single self.buttons = 'no'; } else { self.buttons = self.attrs.buttons || editableOptions.buttons; } //if name defined --> watch changes and update $data in form if ( $attrs.eName ) { self.scope.$watch( '$data', function ( newVal ) { self.scope.$form.$data[ $attrs.eName ] = newVal; } ); } /** * Called when control is shown. * See [demo](#select-remote). * * @var {method|attribute} onshow * @memberOf editable-element */ if ( $attrs.onshow ) { self.onshow = function () { return self.catchError( $parse( $attrs.onshow )( $scope ) ); }; } /** * Called when control is hidden after both save or cancel. * * @var {method|attribute} onhide * @memberOf editable-element */ if ( $attrs.onhide ) { self.onhide = function () { return $parse( $attrs.onhide )( $scope ); }; } /** * Called when control is cancelled. * * @var {method|attribute} oncancel * @memberOf editable-element */ if ( $attrs.oncancel ) { self.oncancel = function () { return $parse( $attrs.oncancel )( $scope ); }; } /** * Called during submit before value is saved to model. * See [demo](#onbeforesave). * * @var {method|attribute} onbeforesave * @memberOf editable-element */ if ( $attrs.onbeforesave ) { self.onbeforesave = function () { return self.catchError( $parse( $attrs.onbeforesave )( $scope ) ); }; } /** * Called during submit after value is saved to model. * See [demo](#onaftersave). * * @var {method|attribute} onaftersave * @memberOf editable-element */ if ( $attrs.onaftersave ) { self.onaftersave = function () { return self.catchError( $parse( $attrs.onaftersave )( $scope ) ); }; } // watch change of model to update editable element // now only add/remove `editable-empty` class. // Initially this method called with newVal = undefined, oldVal = undefined // so no need initially call handleEmpty() explicitly $scope.$parent.$watch( $attrs[ self.directiveName ], function ( newVal, oldVal ) { self.handleEmpty(); } ); }; self.render = function () { var theme = self.theme; //build input self.inputEl = angular.element( self.inputTpl ); //build controls self.controlsEl = angular.element( theme.controlsTpl ); self.controlsEl.append( self.inputEl ); //build buttons if ( self.buttons !== 'no' ) { self.buttonsEl = angular.element( theme.buttonsTpl ); self.submitEl = angular.element( theme.submitTpl ); self.cancelEl = angular.element( theme.cancelTpl ); self.buttonsEl.append( self.submitEl ) .append( self.cancelEl ); self.controlsEl.append( self.buttonsEl ); self.inputEl.addClass( 'editable-has-buttons' ); } //build error self.errorEl = angular.element( theme.errorTpl ); self.controlsEl.append( self.errorEl ); //build editor self.editorEl = angular.element( self.single ? theme.formTpl : theme.noformTpl ); self.editorEl.append( self.controlsEl ); // transfer `e-*|data-e-*|x-e-*` attributes for ( var k in $attrs.$attr ) { if ( k.length <= 1 ) { continue; } var transferAttr = false; var nextLetter = k.substring( 1, 2 ); // if starts with `e` + uppercase letter if ( k.substring( 0, 1 ) === 'e' && nextLetter === nextLetter.toUpperCase() ) { transferAttr = k.substring( 1 ); // cut `e` } else { continue; } // exclude `form` and `ng-submit`, if ( transferAttr === 'Form' || transferAttr === 'NgSubmit' ) { continue; } // convert back to lowercase style transferAttr = transferAttr.substring( 0, 1 ) .toLowerCase() + editableUtils.camelToDash( transferAttr.substring( 1 ) ); // workaround for attributes without value (e.g. `multiple = "multiple"`) var attrValue = ( $attrs[ k ] === '' ) ? transferAttr : $attrs[ k ]; // set attributes to input self.inputEl.attr( transferAttr, attrValue ); } self.inputEl.addClass( 'editable-input' ); self.inputEl.attr( 'ng-model', '$data' ); // add directiveName class to editor, e.g. `editable-text` self.editorEl.addClass( editableUtils.camelToDash( self.directiveName ) ); if ( self.single ) { self.editorEl.attr( 'editable-form', '$form' ); // transfer `blur` to form self.editorEl.attr( 'blur', self.attrs.blur || ( self.buttons === 'no' ? 'cancel' : editableOptions.blurElem ) ); } //apply `postrender` method of theme if ( angular.isFunction( theme.postrender ) ) { theme.postrender.call( self ); } }; // with majority of controls copy is not needed, but.. // copy MUST NOT be used for `select-multiple` with objects as items // copy MUST be used for `checklist` self.setLocalValue = function () { self.scope.$data = self.useCopy ? angular.copy( valueGetter( $scope.$parent ) ) : valueGetter( $scope.$parent ); }; //show self.show = function () { // set value of scope.$data self.setLocalValue(); /* Originally render() was inside init() method, but some directives polluting editorEl, so it is broken on second openning. Cloning is not a solution as jqLite can not clone with event handler's. */ self.render(); // insert into DOM $element.after( self.editorEl ); // compile (needed to attach ng-* events from markup) $compile( self.editorEl )( $scope ); // attach listeners (`escape`, autosubmit, etc) self.addListeners(); // hide element $element.addClass( 'editable-hide' ); // onshow return self.onshow(); }; //hide self.hide = function () { self.editorEl.remove(); $element.removeClass( 'editable-hide' ); // onhide return self.onhide(); }; // cancel self.cancel = function () { // oncancel self.oncancel(); // don't call hide() here as it called in form's code }; /* Called after show to attach listeners */ self.addListeners = function () { // bind keyup for `escape` self.inputEl.bind( 'keyup', function ( e ) { if ( !self.single ) { return; } // todo: move this to editable-form! switch ( e.keyCode ) { // hide on `escape` press case 27: self.scope.$apply( function () { self.scope.$form.$cancel(); } ); break; } } ); // autosubmit when `no buttons` if ( self.single && self.buttons === 'no' ) { self.autosubmit(); } // click - mark element as clicked to exclude in document click handler self.editorEl.bind( 'click', function ( e ) { // ignore right/middle button click if ( e.which !== 1 ) { return; } if ( self.scope.$form.$visible ) { self.scope.$form._clicked = true; } } ); }; // setWaiting self.setWaiting = function ( value ) { if ( value ) { // participate in waiting only if not disabled inWaiting = !self.inputEl.attr( 'disabled' ) && !self.inputEl.attr( 'ng-disabled' ) && !self.inputEl.attr( 'ng-enabled' ); if ( inWaiting ) { self.inputEl.attr( 'disabled', 'disabled' ); if ( self.buttonsEl ) { self.buttonsEl.find( 'button' ) .attr( 'disabled', 'disabled' ); } } } else { if ( inWaiting ) { self.inputEl.removeAttr( 'disabled' ); if ( self.buttonsEl ) { self.buttonsEl.find( 'button' ) .removeAttr( 'disabled' ); } } } }; self.activate = function () { setTimeout( function () { var el = self.inputEl[ 0 ]; if ( editableOptions.activate === 'focus' && el.focus ) { el.focus(); } if ( editableOptions.activate === 'select' && el.select ) { el.select(); } }, 0 ); }; self.setError = function ( msg ) { if ( !angular.isObject( msg ) ) { $scope.$error = msg; self.error = msg; } }; /* Checks that result is string or promise returned string and shows it as error message Applied to onshow, onbeforesave, onaftersave */ self.catchError = function ( result, noPromise ) { if ( angular.isObject( result ) && noPromise !== true ) { $q.when( result ) .then( //success and fail handlers are equal angular.bind( this, function ( r ) { this.catchError( r, true ); } ), angular.bind( this, function ( r ) { this.catchError( r, true ); } ) ); //check $http error } else if ( noPromise && angular.isObject( result ) && result.status && ( result.status !== 200 ) && result.data && angular.isString( result.data ) ) { this.setError( result.data ); //set result to string: to let form know that there was error result = result.data; } else if ( angular.isString( result ) ) { this.setError( result ); } return result; }; self.save = function () { valueGetter.assign( $scope.$parent, angular.copy( self.scope.$data ) ); // no need to call handleEmpty here as we are watching change of model value // self.handleEmpty(); }; /* attach/detach `editable-empty` class to element */ self.handleEmpty = function () { var val = valueGetter( $scope.$parent ); var isEmpty = val === null || val === undefined || val === "" || ( angular.isArray( val ) && val.length === 0 ); $element.toggleClass( 'editable-empty', isEmpty ); }; /* Called when `buttons = "no"` to submit automatically */ self.autosubmit = angular.noop; self.onshow = angular.noop; self.onhide = angular.noop; self.oncancel = angular.noop; self.onbeforesave = angular.noop; self.onaftersave = angular.noop; } return EditableController; } ] ); /* editableFactory is used to generate editable directives (see `/directives` folder) Inside it does several things: - detect form for editable element. Form may be one of three types: 1. autogenerated form (for single editable elements) 2. wrapper form (element wrapped by <form> tag) 3. linked form (element has `e-form` attribute pointing to existing form) - attach editableController to element Depends on: editableController, editableFormFactory */ angular.module( 'xeditable' ) .factory( 'editableDirectiveFactory', [ '$parse', '$compile', 'editableThemes', '$rootScope', '$document', 'editableController', 'editableFormController', function ( $parse, $compile, editableThemes, $rootScope, $document, editableController, editableFormController ) { //directive object return function ( overwrites ) { return { restrict: 'A', scope: true, require: [ overwrites.directiveName, '?^form' ], controller: editableController, link: function ( scope, elem, attrs, ctrl ) { // editable controller var eCtrl = ctrl[ 0 ]; // form controller var eFormCtrl; // this variable indicates is element is bound to some existing form, // or it's single element who's form will be generated automatically // By default consider single element without any linked form.ß var hasForm = false; // element wrapped by form if ( ctrl[ 1 ] ) { eFormCtrl = ctrl[ 1 ]; hasForm = true; } else if ( attrs.eForm ) { // element not wrapped by <form>, but we hane `e-form` attr var getter = $parse( attrs.eForm )( scope ); if ( getter ) { // form exists in scope (above), e.g. editable column eFormCtrl = getter; hasForm = true; } else { // form exists below or not exist at all: check document.forms for ( var i = 0; i < $document[ 0 ].forms.length; i++ ) { if ( $document[ 0 ].forms[ i ].name === attrs.eForm ) { // form is below and not processed yet eFormCtrl = null; hasForm = true; break; } } } } /* if(hasForm && !attrs.eName) { throw 'You should provide `e-name` for editable element inside form!'; } */ //check for `editable-form` attr in form /* if(eFormCtrl && ) { throw 'You should provide `e-name` for editable element inside form!'; } */ // store original props to `parent` before merge angular.forEach( overwrites, function ( v, k ) { if ( eCtrl[ k ] !== undefined ) { eCtrl.parent[ k ] = eCtrl[ k ]; } } ); // merge overwrites to base editable controller angular.extend( eCtrl, overwrites ); // init editable ctrl eCtrl.init( !hasForm ); // publich editable controller as `$editable` to be referenced in html scope.$editable = eCtrl; // add `editable` class to element elem.addClass( 'editable' ); // hasForm if ( hasForm ) { if ( eFormCtrl ) { scope.$form = eFormCtrl; if ( !scope.$form.$addEditable ) { throw 'Form with editable elements should have `editable-form` attribute.'; } scope.$form.$addEditable( eCtrl ); } else { // future form (below): add editable controller to buffer and add to form later $rootScope.$$editableBuffer = $rootScope.$$editableBuffer || {}; $rootScope.$$editableBuffer[ attrs.eForm ] = $rootScope.$$editableBuffer[ attrs.eForm ] || []; $rootScope.$$editableBuffer[ attrs.eForm ].push( eCtrl ); scope.$form = null; //will be re-assigned later } // !hasForm } else { // create editableform controller scope.$form = editableFormController(); // add self to editable controller scope.$form.$addEditable( eCtrl ); // if `e-form` provided, publish local $form in scope if ( attrs.eForm ) { scope.$parent[ attrs.eForm ] = scope.$form; } // bind click - if no external form defined if ( !attrs.eForm ) { elem.addClass( 'editable-click' ); elem.bind( 'click', function ( e ) { e.preventDefault(); e.editable = eCtrl; scope.$apply( function () { scope.$form.$show(); } ); } ); } } } }; }; } ] ); /* Returns editableForm controller */ angular.module( 'xeditable' ) .factory( 'editableFormController', [ '$parse', '$document', '$rootScope', 'editablePromiseCollection', 'editableUtils', function ( $parse, $document, $rootScope, editablePromiseCollection, editableUtils ) { // array of opened editable forms var shown = []; // bind click to body: cancel|submit|ignore forms $document.bind( 'click', function ( e ) { // ignore right/middle button click if ( e.which !== 1 ) { return; } var toCancel = []; var toSubmit = []; for ( var i = 0; i < shown.length; i++ ) { // exclude clicked if ( shown[ i ]._clicked ) { shown[ i ]._clicked = false; continue; } // exclude waiting if ( shown[ i ].$waiting ) { continue; } if ( shown[ i ]._blur === 'cancel' ) { toCancel.push( shown[ i ] ); } if ( shown[ i ]._blur === 'submit' ) { toSubmit.push( shown[ i ] ); } } if ( toCancel.length || toSubmit.length ) { $rootScope.$apply( function () { angular.forEach( toCancel, function ( v ) { v.$cancel(); } ); angular.forEach( toSubmit, function ( v ) { v.$submit(); } ); } ); } } ); var base = { $addEditable: function ( editable ) { //console.log('add editable', editable.elem, editable.elem.bind); this.$editables.push( editable ); //'on' is not supported in angular 1.0.8 editable.elem.bind( '$destroy', angular.bind( this, this.$removeEditable, editable ) ); //bind editable's local $form to self (if not bound yet, below form) if ( !editable.scope.$form ) { editable.scope.$form = this; } //if form already shown - call show() of new editable if ( this.$visible ) { editable.catchError( editable.show() ); } }, $removeEditable: function ( editable ) { //arrayRemove for ( var i = 0; i < this.$editables.length; i++ ) { if ( this.$editables[ i ] === editable ) { this.$editables.splice( i, 1 ); return; } } }, /** * Shows form with editable controls. * * @method $show() * @memberOf editable-form */ $show: function () { if ( this.$visible ) { return; } this.$visible = true; var pc = editablePromiseCollection(); //own show pc.when( this.$onshow() ); //clear errors this.$setError( null, '' ); //children show angular.forEach( this.$editables, function ( editable ) { pc.when( editable.show() ); } ); //wait promises and activate pc.then( { onWait: angular.bind( this, this.$setWaiting ), onTrue: angular.bind( this, this.$activate ), onFalse: angular.bind( this, this.$activate ), onString: angular.bind( this, this.$activate ) } ); // add to internal list of shown forms // setTimeout needed to prevent closing right after opening (e.g. when trigger by button) setTimeout( angular.bind( this, function () { // clear `clicked` to get ready for clicks on visible form this._clicked = false; if ( editableUtils.indexOf( shown, this ) === -1 ) { shown.push( this ); } } ), 0 ); }, /** * Sets focus on form field specified by `name`. * * @method $activate(name) * @param {string} name name of field * @memberOf editable-form */ $activate: function ( name ) { var i; if ( this.$editables.length ) { //activate by name if ( angular.isString( name ) ) { for ( i = 0; i < this.$editables.length; i++ ) { if ( this.$editables[ i ].name === name ) { this.$editables[ i ].activate(); return; } } } //try activate error field for ( i = 0; i < this.$editables.length; i++ ) { if ( this.$editables[ i ].error ) { this.$editables[ i ].activate(); return; } } //by default activate first field this.$editables[ 0 ].activate(); } }, /** * Hides form with editable controls without saving. * * @method $hide() * @memberOf editable-form */ $hide: function () { if ( !this.$visible ) { return; } this.$visible = false; // self hide this.$onhide(); // children's hide angular.forEach( this.$editables, function ( editable ) { editable.hide(); } ); // remove from internal list of shown forms editableUtils.arrayRemove( shown, this ); }, /** * Triggers `oncancel` event and calls `$hide()`. * * @method $cancel() * @memberOf editable-form */ $cancel: function () { if ( !this.$visible ) { return; } // self cancel this.$oncancel(); // children's cancel angular.forEach( this.$editables, function ( editable ) { editable.cancel(); } ); // self hide this.$hide(); }, $setWaiting: function ( value ) { this.$waiting = !!value; // we can't just set $waiting variable and use it via ng-disabled in children // because in editable-row form is not accessible angular.forEach( this.$editables, function ( editable ) { editable.setWaiting( !!value ); } ); }, /** * Shows error message for particular field. * * @method $setError(name, msg) * @param {string} name name of field * @param {string} msg error message * @memberOf editable-form */ $setError: function ( name, msg ) { angular.forEach( this.$editables, function ( editable ) { if ( !name || editable.name === name ) { editable.setError( msg ); } } ); }, $submit: function () { if ( this.$waiting ) { return; } //clear errors this.$setError( null, '' ); //children onbeforesave var pc = editablePromiseCollection(); angular.forEach( this.$editables, function ( editable ) { pc.when( editable.onbeforesave() ); } ); /* onbeforesave result: - true/undefined: save data and close form - false: close form without saving - string: keep form open and show error */ pc.then( { onWait: angular.bind( this, this.$setWaiting ), onTrue: angular.bind( this, checkSelf, true ), onFalse: angular.bind( this, checkSelf, false ), onString: angular.bind( this, this.$activate ) } ); //save function checkSelf( childrenTrue ) { var pc = editablePromiseCollection(); pc.when( this.$onbeforesave() ); pc.then( { onWait: angular.bind( this, this.$setWaiting ), onTrue: childrenTrue ? angular.bind( this, this.$save ) : angular.bind( this, this.$hide ), onFalse: angular.bind( this, this.$hide ), onString: angular.bind( this, this.$activate ) } ); } }, $save: function () { // write model for each editable angular.forEach( this.$editables, function ( editable ) { editable.save(); } ); //call onaftersave of self and children var pc = editablePromiseCollection(); pc.when( this.$onaftersave() ); angular.forEach( this.$editables, function ( editable ) { pc.when( editable.onaftersave() ); } ); /* onaftersave result: - true/undefined/false: just close form - string: keep form open and show error */ pc.then( { onWait: angular.bind( this, this.$setWaiting ), onTrue: angular.bind( this, this.$hide ), onFalse: angular.bind( this, this.$hide ), onString: angular.bind( this, this.$activate ) } ); }, $onshow: angular.noop, $oncancel: angular.noop, $onhide: angular.noop, $onbeforesave: angular.noop, $onaftersave: angular.noop }; return function () { return angular.extend( { $editables: [], /** * Form visibility flag. * * @var {bool} $visible * @memberOf editable-form */ $visible: false, /** * Form waiting flag. It becomes `true` when form is loading or saving data. * * @var {bool} $waiting * @memberOf editable-form */ $waiting: false, $data: {}, _clicked: false, _blur: null }, base ); }; } ] ); /** * EditableForm directive. Should be defined in <form> containing editable controls. * It add some usefull methods to form variable exposed to scope by `name="myform"` attribute. * * @namespace editable-form */ angular.module( 'xeditable' ) .directive( 'editableForm', [ '$rootScope', '$parse', 'editableFormController', 'editableOptions', function ( $rootScope, $parse, editableFormController, editableOptions ) { return { restrict: 'A', require: [ 'form' ], //require: ['form', 'editableForm'], //controller: EditableFormController, compile: function () { return { pre: function ( scope, elem, attrs, ctrl ) { var form = ctrl[ 0 ]; var eForm; //if `editableForm` has value - publish smartly under this value //this is required only for single editor form that is created and removed if ( attrs.editableForm ) { if ( scope[ attrs.editableForm ] && scope[ attrs.editableForm ].$show ) { eForm = scope[ attrs.editableForm ]; angular.extend( form, eForm ); } else { eForm = editableFormController(); scope[ attrs.editableForm ] = eForm; angular.extend( eForm, form ); } } else { //just merge to form and publish if form has name eForm = editableFormController(); angular.extend( form, eForm ); } //read editables from buffer (that appeared before FORM tag) var buf = $rootScope.$$editableBuffer; var name = form.$name; if ( name && buf && buf[ name ] ) { angular.forEach( buf[ name ], function ( editable ) { eForm.$addEditable( editable ); } ); delete buf[ name ]; } }, post: function ( scope, elem, attrs, ctrl ) { var eForm; if ( attrs.editableForm && scope[ attrs.editableForm ] && scope[ attrs.editableForm ].$show ) { eForm = scope[ attrs.editableForm ]; } else { eForm = ctrl[ 0 ]; } /** * Called when form is shown. * * @var {method|attribute} onshow * @memberOf editable-form */ if ( attrs.onshow ) { eForm.$onshow = angular.bind( eForm, $parse( attrs.onshow ), scope ); } /** * Called when form hides after both save or cancel. * * @var {method|attribute} onhide * @memberOf editable-form */ if ( attrs.onhide ) { eForm.$onhide = angular.bind( eForm, $parse( attrs.onhide ), scope ); } /** * Called when form is cancelled. * * @var {method|attribute} oncancel * @memberOf editable-form */ if ( attrs.oncancel ) { eForm.$oncancel = angular.bind( eForm, $parse( attrs.oncancel ), scope ); } /** * Whether form initially rendered in shown state. * * @var {bool|attribute} shown * @memberOf editable-form */ if ( attrs.shown && $parse( attrs.shown )( scope ) ) { eForm.$show(); } /** * Action when form losses focus. Values: `cancel|submit|ignore`. * Default is `ignore`. * * @var {string|attribute} blur * @memberOf editable-form */ eForm._blur = attrs.blur || editableOptions.blurForm; // onbeforesave, onaftersave if ( !attrs.ngSubmit && !attrs.submit ) { /** * Called after all children `onbeforesave` callbacks but before saving form values * to model. * If at least one children callback returns `non-string` - it will not not be called. * See [editable-form demo](#editable-form) for details. * * @var {method|attribute} onbeforesave * @memberOf editable-form * */ if ( attrs.onbeforesave ) { eForm.$onbeforesave = function () { return $parse( attrs.onbeforesave )( scope, { $data: eForm.$data } ); }; } /** * Called when form values are saved to model. * See [editable-form demo](#editable-form) for details. * * @var {method|attribute} onaftersave * @memberOf editable-form * */ if ( attrs.onaftersave ) { eForm.$onaftersave = function () { return $parse( attrs.onaftersave )( scope, { $data: eForm.$data } ); }; } elem.bind( 'submit', function ( event ) { event.preventDefault(); scope.$apply( function () { eForm.$submit(); } ); } ); } // click - mark form as clicked to exclude in document click handler elem.bind( 'click', function ( e ) { // ignore right/middle button click if ( e.which !== 1 ) { return; } if ( eForm.$visible ) { eForm._clicked = true; } } ); } }; } }; } ] ); /** * editablePromiseCollection * * Collect results of function calls. Shows waiting if there are promises. * Finally, applies callbacks if: * - onTrue(): all results are true and all promises resolved to true * - onFalse(): at least one result is false or promise resolved to false * - onString(): at least one result is string or promise rejected or promise resolved to string */ angular.module( 'xeditable' ) .factory( 'editablePromiseCollection', [ '$q', function ( $q ) { function promiseCollection() { return { promises: [], hasFalse: false, hasString: false, when: function ( result, noPromise ) { if ( result === false ) { this.hasFalse = true; } else if ( !noPromise && angular.isObject( result ) ) { this.promises.push( $q.when( result ) ); } else if ( angular.isString( result ) ) { this.hasString = true; } else { //result === true || result === undefined || result === null return; } }, //callbacks: onTrue, onFalse, onString then: function ( callbacks ) { callbacks = callbacks || {}; var onTrue = callbacks.onTrue || angular.noop; var onFalse = callbacks.onFalse || angular.noop; var onString = callbacks.onString || angular.noop; var onWait = callbacks.onWait || angular.noop; var self = this; if ( this.promises.length ) { onWait( true ); $q.all( this.promises ) .then( //all resolved function ( results ) { onWait( false ); //check all results via same `when` method (without checking promises) angular.forEach( results, function ( result ) { self.when( result, true ); } ); applyCallback(); }, //some rejected function ( error ) { onWait( false ); onString(); } ); } else { applyCallback(); } function applyCallback() { if ( !self.hasString && !self.hasFalse ) { onTrue(); } else if ( !self.hasString && self.hasFalse ) { onFalse(); } else { onString(); } } } }; } return promiseCollection; } ] ); /** * editableUtils */ angular.module( 'xeditable' ) .factory( 'editableUtils', [ function () { return { indexOf: function ( array, obj ) { if ( array.indexOf ) return array.indexOf( obj ); for ( var i = 0; i < array.length; i++ ) { if ( obj === array[ i ] ) return i; } return -1; }, arrayRemove: function ( array, value ) { var index = this.indexOf( array, value ); if ( index >= 0 ) { array.splice( index, 1 ); } return value; }, // copy from https://github.com/angular/angular.js/blob/master/src/Angular.js camelToDash: function ( str ) { var SNAKE_CASE_REGEXP = /[A-Z]/g; return str.replace( SNAKE_CASE_REGEXP, function ( letter, pos ) { return ( pos ? '-' : '' ) + letter.toLowerCase(); } ); }, dashToCamel: function ( str ) { var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; var MOZ_HACK_REGEXP = /^moz([A-Z])/; return str. replace( SPECIAL_CHARS_REGEXP, function ( _, separator, letter, offset ) { return offset ? letter.toUpperCase() : letter; } ) . replace( MOZ_HACK_REGEXP, 'Moz$1' ); } }; } ] ); /** * editableNgOptionsParser * * see: https://github.com/angular/angular.js/blob/master/src/ng/directive/select.js#L131 */ angular.module( 'xeditable' ) .factory( 'editableNgOptionsParser', [ function () { //0000111110000000000022220000000000000000000000333300000000000000444444444444444000000000555555555555555000000066666666666666600000000000000007777000000000000000000088888 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+(.*?))?$/; function parser( optionsExp ) { var match; if ( !( match = optionsExp.match( NG_OPTIONS_REGEXP ) ) ) { throw 'ng-options parse error'; } var displayFn = match[ 2 ] || match[ 1 ], valueName = match[ 4 ] || match[ 6 ], keyName = match[ 5 ], groupByFn = match[ 3 ] || '', valueFn = match[ 2 ] ? match[ 1 ] : valueName, valuesFn = match[ 7 ], track = match[ 8 ], trackFn = track ? match[ 8 ] : null; var ngRepeat; if ( keyName === undefined ) { // array ngRepeat = valueName + ' in ' + valuesFn; if ( track !== undefined ) { ngRepeat += ' track by ' + trackFn; } } else { // object ngRepeat = '(' + keyName + ', ' + valueName + ') in ' + valuesFn; } // group not supported yet return { ngRepeat: ngRepeat, locals: { valueName: valueName, keyName: keyName, valueFn: valueFn, displayFn: displayFn } }; } return parser; } ] ); /* Editable themes: - default - bootstrap 2 - bootstrap 3 Note: in postrender() `this` is instance of editableController */ angular.module( 'xeditable' ) .factory( 'editableThemes', function () { var themes = { //default 'default': { formTpl: '<form class="editable-wrap"></form>', noformTpl: '<span class="editable-wrap"></span>', controlsTpl: '<span class="editable-controls"></span>', inputTpl: '', errorTpl: '<div class="editable-error" ng-show="$error" ng-bind="$error"></div>', buttonsTpl: '<span class="editable-buttons"></span>', submitTpl: '<button type="submit">save</button>', cancelTpl: '<button type="button" ng-click="$form.$cancel()">cancel</button>' }, //bs2 'bs2': { formTpl: '<form class="form-inline editable-wrap" role="form"></form>', noformTpl: '<span class="editable-wrap"></span>', controlsTpl: '<div class="editable-controls controls control-group" ng-class="{\'error\': $error}"></div>', inputTpl: '', errorTpl: '<div class="editable-error help-block" ng-show="$error" ng-bind="$error"></div>', buttonsTpl: '<span class="editable-buttons"></span>', submitTpl: '<button type="submit" class="btn btn-primary"><span class="icon-ok icon-white"></span></button>', cancelTpl: '<button type="button" class="btn" ng-click="$form.$cancel()">' + '<span class="icon-remove"></span>' + '</button>' }, //bs3 'bs3': { formTpl: '<form class="form-inline editable-wrap" role="form"></form>', noformTpl: '<span class="editable-wrap"></span>', controlsTpl: '<div class="editable-controls form-group" ng-class="{\'has-error\': $error}"></div>', inputTpl: '', errorTpl: '<div class="editable-error help-block" ng-show="$error" ng-bind="$error"></div>', buttonsTpl: '<span class="editable-buttons"></span>', submitTpl: '<button type="submit" class="btn btn-primary height34"><span class="glyphicon glyphicon-ok"></span></button>', cancelTpl: '<button type="button" class="btn btn-default height34" ng-click="$form.$cancel()">' + '<span class="glyphicon glyphicon-remove"></span>' + '</button>', //bs3 specific prop to change buttons class: btn-sm, btn-lg buttonsClass: '', //bs3 specific prop to change standard inputs class: input-sm, input-lg inputClass: '', postrender: function () { //apply `form-control` class to std inputs switch ( this.directiveName ) { case 'editableText': case 'editableSelect': case 'editableTextarea': case 'editableEmail': case 'editableTel': case 'editableNumber': case 'editableUrl': case 'editableSearch': case 'editableDate': case 'editableDatetime': case 'editableTime': case 'editableMonth': case 'editableWeek': this.inputEl.addClass( 'form-control' ); if ( this.theme.inputClass ) { // don`t apply `input-sm` and `input-lg` to select multiple // should be fixed in bs itself! if ( this.inputEl.attr( 'multiple' ) && ( this.theme.inputClass === 'input-sm' || this.theme.inputClass === 'input-lg' ) ) { break; } this.inputEl.addClass( this.theme.inputClass ); } break; } //apply buttonsClass (bs3 specific!) if ( this.buttonsEl && this.theme.buttonsClass ) { this.buttonsEl.find( 'button' ) .addClass( this.theme.buttonsClass ); } } } }; return themes; } );
[+]
..
[-] xeditable.js
[edit]
[-] xeditable.min.js
[edit]