Created
November 12, 2013 21:46
-
-
Save noeticpenguin/7439356 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*! | |
angular-xeditable - 0.1.7 | |
Edit-in-place for angular.js | |
Build date: 2013-10-26 | |
*/ | |
/* | |
angular-xeditable module | |
*/ | |
angular.module('xeditable', []) | |
.value('editableOptions', { | |
theme: 'default', //bs2, bs3, | |
buttons: 'right', | |
blur: 'cancel' | |
}); | |
/* | |
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); | |
}); | |
} | |
}); | |
}]); | |
/* | |
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>'); | |
} | |
}); | |
}]); | |
}()); | |
//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', '$document', 'editableUtils', '$rootScope', | |
function($q, $document, utils, $rootScope) { | |
// array of opened editable controls | |
var shown = []; | |
// bind click to body: cancel|submit editables | |
$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 self | |
if (shown[i].clicked) { | |
shown[i].clicked = false; | |
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.scope.$form.$hide(); }); | |
angular.forEach(toSubmit, function(v){ v.scope.$form.$submit(); }); | |
}); | |
} | |
}); | |
//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 = {}; | |
self.clicked = false; //used to check in document click handler if control was clicked or not | |
//to be overwritten by directive | |
self.inputTpl = ''; | |
self.directiveName = ''; | |
//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`. | |
* If control is part of form `blur` automatically set to `ignore`. | |
* | |
* @var {string|attribute} blur | |
* @memberOf editable-element | |
*/ | |
self.blur = 'ignore'; // can be 'cancel|submit|ignore' | |
//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'; | |
self.blur = 'ignore'; | |
} else { | |
self.buttons = self.attrs.buttons || editableOptions.buttons; | |
self.blur = self.attrs.blur || (self.buttons === 'no' ? 'cancel' : editableOptions.blur); | |
} | |
//moved to show() | |
//self.render(); | |
//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() + utils.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(utils.camelToDash(self.directiveName)); | |
if(self.single) { | |
self.editorEl.attr('editable-form', '$form'); | |
} | |
//apply `postrender` method of theme | |
if(angular.isFunction(theme.postrender)) { | |
theme.postrender.call(self); | |
} | |
}; | |
//show | |
self.show = function() { | |
// set value. copy not needed. | |
// self.scope.$data = angular.copy(valueGetter($scope.$parent)); | |
self.scope.$data = valueGetter($scope.$parent); | |
/* | |
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'); | |
// add to internal list | |
// setTimeout needed to prevent closing right after opening (e.g. when trigger by button) | |
setTimeout(function() { | |
if(utils.indexOf(shown, self) === -1) { | |
shown.push(self); | |
} | |
}, 0); | |
// onshow | |
return self.onshow(); | |
}; | |
//hide | |
self.hide = function() { | |
//console.log('editable hide', self.name); | |
self.editorEl.remove(); | |
$element.removeClass('editable-hide'); | |
// remove from internal list | |
utils.arrayRemove(shown, self); | |
// onhide | |
return self.onhide(); | |
// todo: to think is it really needed or not | |
/* | |
if($element[0].tagName === 'A') { | |
$element[0].focus(); | |
} | |
*/ | |
}; | |
// 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; | |
} | |
switch(e.keyCode) { | |
// hide on `escape` press | |
case 27: | |
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; | |
} | |
self.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() { | |
self.inputEl[0].focus(); | |
}, 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: | |
- attaches editableController to element | |
- used to generate editable directives | |
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; | |
var hasForm = false; | |
//if not inside form, but we have `e-form`: | |
//check if form exists somewhere in scope. If exists - bind, otherwise create. | |
if(ctrl[1]) { | |
eFormCtrl = ctrl[1]; | |
hasForm = true; | |
} else if(attrs.eForm) { | |
var getter = $parse(attrs.eForm)(scope); | |
if(getter) { //getter defined, form above | |
eFormCtrl = getter; | |
hasForm = true; | |
} else { //form below or not exist: 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); | |
//elem.after(self.editorEl); | |
//console.log('w:', scope.$$watchers.length); | |
//$compile(eCtrl.editorEl)(scope); | |
//scope.$form.$addEditable(eCtrl); | |
//console.log('w:', scope.$$watchers.length); | |
//eCtrl.editorEl.remove(); | |
//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', 'editablePromiseCollection', | |
function($parse, editablePromiseCollection) { | |
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) | |
}); | |
}, | |
/** | |
* 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(); | |
}); | |
}, | |
/** | |
* 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: {} | |
}, 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', | |
function($rootScope, $parse, editableFormController) { | |
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; | |
/* | |
Maybe it's better attach editable controller to form's controller not in pre() | |
but in controller itself. | |
This allows to use ng-init already in <form> tag, otherwise we can't (in FF). | |
*/ | |
//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(); | |
} | |
// 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(); | |
}); | |
}); | |
} | |
} | |
}; | |
} | |
}; | |
}]); | |
/* | |
Helpers | |
*/ | |
/* | |
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; | |
}]); | |
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'); | |
} | |
}; | |
}]); | |
/* | |
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">{{$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">{{$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">{{$error}}</div>', | |
buttonsTpl: '<span class="editable-buttons"></span>', | |
submitTpl: '<button type="submit" class="btn btn-primary"><span class="glyphicon glyphicon-ok"></span></button>', | |
cancelTpl: '<button type="button" class="btn btn-default" 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': | |
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; | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment