var app = angular.module('plunker', ['kendo.directives']);
app.controller('MainCtrl', function($scope, $compile) {
$scope.gridDataSource = [ { name: "Jane Doe" }, { name: "John Doe" } ];
$scope.isChecked = function(dataItem) {
console.log("isChecked called");
return dataItem.name == "Jane Doe";
}
});
<!DOCTYPE html>
<html ng-app="plunker">
<head>
<meta charset="utf-8">
<title>Checkbox Column Editor</title>
<link href="http://cdn.kendostatic.com/2013.3.1119/styles/kendo.common.min.css" rel="stylesheet">
<link href="http://cdn.kendostatic.com/2013.3.1119/styles/kendo.bootstrap.min.css" rel="stylesheet">
<style>.ng-cloak { display: none; }</style>
<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
<script src="//code.angularjs.org/1.2.11/angular.js"></script>
<script src="http://cdn.kendostatic.com/2013.3.1119/js/kendo.all.min.js"></script>
<!-- defect in 0.6.0
<script src="angular-kendo-0.6.0.js"></script>
-->
<script src="https://rawgithub.com/kendo-labs/angular-kendo/5ce9b23ff874fb4a174c7e70d3ef217e21208991/angular-kendo.js"></script>
<script>
document.write('<base href="' + document.location + '" />');
</script>
<script src="app.js"></script>
</head>
<body ng-controller="MainCtrl">
<div kendo-grid
k-columns='[
{ template: "<input type=\"checkbox\" ng-checked=\"isChecked(dataItem)\" />" },
{ field: "name" }
]'
k-editable="true"
k-sortable="true"
k-data-source="gridDataSource">
</div>
</body>
</html>
(function(f, define){
define([ "jquery", "angular", "kendo" ], f);
})(function($, angular, kendo) {
"use strict";
var _UID_ = kendo.attr("uid");
var module = angular.module('kendo.directives', []);
var parse, timeout, compile = function compile(){ return compile }, log;
function immediately(f) {
var save_timeout = timeout;
timeout = function(f) { return f() };
try {
return f();
} finally {
timeout = save_timeout;
}
}
// The following disables AngularJS built-in directives for <input> fields
// when a Kendo widget is defined. The reason we have to do this is:
//
// 1. user updates field.
//
// 2. widget triggers "change" event on the Widget object => angular-kendo
// gets notified, updates the model with the correct value!
//
// 3. widget triggers "change" event on the <input> field => AngularJS's
// built-in directive validates the *content* of the input field and
// updates the model again WITH THE WRONG VALUE.
//
// https://github.com/kendo-labs/angular-kendo/issues/135
// https://github.com/kendo-labs/angular-kendo/issues/152
module.config([ "$provide", function($provide){
function dismissAngular($delegate) {
var orig_compile = $delegate[0].compile;
$delegate[0].compile = function(element, attrs) {
for (var i in attrs) {
if (attrs.hasOwnProperty(i)) {
if (/^kendo/.test(i) && typeof $.fn[i] == "function") {
return; // HA!
}
}
}
return orig_compile.apply(this, arguments);
};
return $delegate;
}
$provide.decorator("inputDirective", [ "$delegate", dismissAngular ]);
$provide.decorator("selectDirective", [ "$delegate", dismissAngular ]);
}]);
var OPTIONS_NOW;
var factories = {
dataSource: (function() {
var types = {
TreeView: 'HierarchicalDataSource',
Scheduler: 'SchedulerDataSource'
};
var toDataSource = function(dataSource, type) {
return kendo.data[type].create(dataSource);
};
return function(scope, element, attrs, role) {
var type = types[role] || 'DataSource';
var ds = toDataSource(scope.$eval(attrs.kDataSource), type);
// Set $kendoDataSource in the element's data. 3rd parties can define their own dataSource creation
// directive and provide this data on the element.
element.data('$kendoDataSource', ds);
// not recursive -- this triggers when the whole data source changed
scope.$watch(attrs.kDataSource, function(mew, old){
if (mew !== old) {
var ds = toDataSource(mew, type);
element.data('$kendoDataSource', ds);
var widget = kendoWidgetInstance(element);
if (widget && typeof widget.setDataSource == "function") {
widget.setDataSource(ds);
}
}
});
return ds;
};
}()),
widget: (function() {
var ignoredAttributes = {
kDataSource: true,
kOptions: true,
kRebind: true
};
return function(scope, element, attrs, widget) {
var role = widget.replace(/^kendo/, '');
var options = angular.extend({}, scope.$eval(attrs.kOptions));
$.each(attrs, function(name, value) {
if (!ignoredAttributes[name]) {
var match = name.match(/^k(On)?([A-Z].*)/);
if (match) {
var optionName = match[2].charAt(0).toLowerCase() + match[2].slice(1);
if (match[1]) {
options[optionName] = value;
} else {
options[optionName] = angular.copy(scope.$eval(value));
if (options[optionName] === undefined && value.match(/^\w*$/)) {
log.warn(widget + '\'s ' + name + ' attribute resolved to undefined. Maybe you meant to use a string literal like: \'' + value + '\'?');
}
}
}
}
});
options.dataSource = element.inheritedData('$kendoDataSource') || options.dataSource;
// parse the datasource attribute
if (attrs.kDataSource) {
options.dataSource = factories.dataSource(scope, element, attrs, role);
}
options.$angular = true;
var object = $(element)[widget](OPTIONS_NOW = options).data(widget);
exposeWidget(object, scope, attrs, widget);
scope.$emit("kendoWidgetCreated", object);
return object;
};
}())
};
function exposeWidget(widget, scope, attrs, kendoWidget) {
if (attrs[kendoWidget]) {
// expose the widget object
var set = parse(attrs[kendoWidget]).assign;
if (set) {
// set the value of the expression to the kendo widget object to expose its api
set(scope, widget);
} else {
throw new Error( kendoWidget + ' attribute used but expression in it is not assignable: ' + attrs[kendoWidget]);
}
}
}
module.factory('directiveFactory', ['$timeout', '$parse', '$compile', '$log', function($timeout, $parse, $compile, $log) {
timeout = $timeout;
parse = $parse;
compile = $compile;
log = $log;
var KENDO_COUNT = 0;
var create = function(role) {
return {
// Parse the directive for attributes and classes
restrict: "ACE",
require: [ "?ngModel", "^?form" ],
scope: false,
// // XXX: Is this transclusion needed? We seem to do better without it.
// // https://github.com/kendo-labs/angular-kendo/issues/90
//
// transclude: true,
// controller: [ '$scope', '$attrs', '$element', '$transclude', function($scope, $attrs, $element, $transclude) {
// // Make the element's contents available to the kendo widget to allow creating some widgets from existing elements.
// $transclude(function(clone){
// $element.append(clone);
// });
// }],
link: function(scope, element, attrs, controllers) {
var ngModel = controllers[0];
var ngForm = controllers[1];
// we must remove data-kendo-widget-name attribute because
// it breaks kendo.widgetInstance; can generate all kinds
// of funny issues like
// https://github.com/kendo-labs/angular-kendo/issues/167
// $(element).removeData(role);
// console.log($(element).data(role)); // --> not undefined. now I'm pissed.
$(element)[0].removeAttribute("data-" + role.replace(/([A-Z])/g, "-$1"));
var originalElement = $(element)[0].cloneNode(true);
++KENDO_COUNT;
timeout(function() {
var widget = factories.widget(scope, element, attrs, role);
// if k-rebind attribute is provided, rebind the kendo widget when
// the watched value changes
if (attrs.kRebind) {
// watch for changes on the expression passed in the k-rebind attribute
scope.$watch(attrs.kRebind, function(newValue, oldValue) {
if (newValue !== oldValue) {
// create the kendo widget and bind it to the element.
try {
/****************************************************************
// XXX: this is a gross hack that might not even work with all
// widgets. we need to destroy the current widget and get its
// wrapper element out of the DOM, then make the original element
// visible so we can initialize a new widget on it.
//
// kRebind is probably impossible to get right at the moment.
****************************************************************/
var _wrapper = $(widget.wrapper)[0];
var _element = $(widget.element)[0];
widget.destroy();
if (_wrapper && _element) {
_wrapper.parentNode.replaceChild(_element, _wrapper);
var clone = originalElement.cloneNode(true);
$(element).replaceWith(clone);
element = $(clone);
}
widget = factories.widget(scope, element, attrs, role);
setupBindings();
} catch(ex) {
console.error(ex);
console.error(ex.stack);
}
}
}, true); // watch for object equality. Use native or simple values.
}
setupBindings();
var prev_destroy = null;
function setupBindings() {
// Cleanup after ourselves
if (prev_destroy) {
prev_destroy();
}
prev_destroy = scope.$on("$destroy", function() {
widget.destroy();
});
// 2 way binding: ngModel <-> widget.value()
if (ngModel) {
if (!widget.value) {
throw new Error('ng-model used but ' + role + ' does not define a value accessor');
}
// Angular will invoke $render when the view needs to be updated with the view value.
ngModel.$render = function() {
// Update the widget with the view value.
widget.value(ngModel.$modelValue);
};
// In order to be able to update the angular scope objects, we need to know when the change event is fired for a Kendo UI Widget.
var onChange = function(pristine){
function doit() {
if (pristine && ngForm) {
var formPristine = ngForm.$pristine;
}
ngModel.$setViewValue(widget.value());
if (pristine) {
ngModel.$setPristine();
if (formPristine) {
ngForm.$setPristine();
}
}
}
return function(e) {
if (scope.$root.$$phase === '$apply' || scope.$root.$$phase === '$digest') {
doit();
} else {
scope.$apply(doit);
}
};
};
bindBefore(widget, "change", onChange(false));
bindBefore(widget, "dataBound", onChange(true));
// if the model value is undefined, then we set the widget value to match ( == null/undefined )
if (widget.value() != ngModel.$modelValue) {
if (!ngModel.$isEmpty(ngModel.$modelValue)) {
widget.value(ngModel.$modelValue);
}
if (widget.value() != null && widget.value() != "" && widget.value() != ngModel.$modelValue) {
ngModel.$setViewValue(widget.value());
}
}
ngModel.$setPristine();
}
}
// mutation observers — propagate the original
// element's class to the widget wrapper.
(function(){
if (!(window.MutationObserver
&& widget.wrapper
&& $(widget.wrapper)[0] !== $(element)[0])) {
return;
}
var prevClassList = [].slice.call($(element)[0].classList);
var mo = new MutationObserver(function(changes, mo){
suspend(); // make sure we don't trigger a loop
changes.forEach(function(chg){
var w = $(widget.wrapper)[0];
switch (chg.attributeName) {
case "class":
// sync classes to the wrapper element
var currClassList = [].slice.call(chg.target.classList);
currClassList.forEach(function(cls){
if (prevClassList.indexOf(cls) < 0) {
w.classList.add(cls);
}
});
prevClassList.forEach(function(cls){
if (currClassList.indexOf(cls) < 0) {
w.classList.remove(cls);
}
});
prevClassList = currClassList;
break;
case "disabled":
if (typeof widget.enable == "function") {
widget.enable(!$(chg.target).attr("disabled"));
}
break;
case "readonly":
if (typeof widget.readonly == "function") {
widget.readonly(!!$(chg.target).attr("readonly"));
}
break;
}
});
resume();
});
function suspend() {
mo.disconnect();
}
function resume() {
mo.observe($(element)[0], { attributes: true });
}
resume();
bindBefore(widget, "destroy", suspend);
})();
--KENDO_COUNT;
if (KENDO_COUNT == 0) {
scope.$emit("kendoRendered");
}
});
}
};
};
return {
create: create
};
}]);
// create directives for every widget.
angular.forEach([ kendo.ui, kendo.dataviz && kendo.dataviz.ui ], function(namespace) {
angular.forEach(namespace, function(value, key) {
if (key.match(/^[A-Z]/) && key !== 'Widget') {
var widget = "kendo" + key;
module.directive(widget, [
"directiveFactory",
function(directiveFactory) {
return directiveFactory.create(widget);
}
]);
}
});
});
/* -----[ utils ]----- */
function kendoWidgetInstance(el) {
el = $(el);
return kendo.widgetInstance(el, kendo.ui) ||
kendo.widgetInstance(el, kendo.mobile.ui) ||
kendo.widgetInstance(el, kendo.dataviz.ui);
}
// XXX: using internal API (Widget::_events). Seems to be no way in Kendo to
// insert a handler to be executed before any existing ones, hence this hack.
// Use for a single event/handler combination.
function bindBefore(widget, name, handler, one) {
widget.bind.call(widget, name, handler, one);
var a = widget._events[name];
a.unshift(a.pop());
}
function digest(scope) {
if (!/^\$(digest|apply)$/.test(scope.$root.$$phase)) {
scope.$digest();
}
}
// defadvice will patch a class' method with another function. That
// function will be called in a context containing `next` (to call
// the next method) and `object` (a reference to the original
// object).
function defadvice(klass, methodName, func) {
if ($.isArray(klass)) {
return angular.forEach(klass, function(klass){
defadvice(klass, methodName, func);
});
}
var origMethod = klass.prototype[methodName];
klass.prototype[methodName] = function() {
var self = this, args = arguments;
return func.apply({
self: self,
next: function() {
return origMethod.apply(self, arguments.length > 0 ? arguments : args);
}
}, args);
};
}
var BEFORE = "$angular_beforeCreate";
var AFTER = "$angular_afterCreate";
/* -----[ Customize widgets for Angular ]----- */
// XXX: notice we can't override `init` in general for any widget,
// because kendo.ui.Widget === kendo.ui.Widget.prototype.init.
// Hence we resort to the beforeCreate/afterCreate hack.
defadvice(kendo.ui.Widget, "init", function(element, options){
if (!options && OPTIONS_NOW) options = OPTIONS_NOW;
OPTIONS_NOW = null;
var self = this.self;
if (options && options.$angular) {
// call before/after hooks only for widgets instantiated by angular-kendo
self.$angular_beforeCreate(element, options);
this.next();
self.$angular_afterCreate();
} else {
this.next();
}
});
// All event handlers that are strings are compiled the Angular way.
defadvice(kendo.ui.Widget, BEFORE, function(element, options) {
var self = this.self;
if (options && !$.isArray(options)) {
var scope = angular.element(element).scope();
for (var i = self.events.length; --i >= 0;) {
var event = self.events[i];
var handler = options[event];
if (handler && typeof handler == "string")
options[event] = self.$angular_makeEventHandler(event, scope, handler);
}
}
});
defadvice(kendo.ui.Widget, AFTER, function(){});
// most handers will only contain a kendoEvent in the scope.
defadvice(kendo.ui.Widget, "$angular_makeEventHandler", function(event, scope, handler){
handler = parse(handler);
return function(e) {
if (/^\$(apply|digest)$/.test(scope.$root.$$phase)) {
handler(scope, { kendoEvent: e });
} else {
scope.$apply(function() { handler(scope, { kendoEvent: e }) });
}
};
});
// for the Grid and ListView we add `data` and `selected` too.
defadvice([ kendo.ui.Grid, kendo.ui.ListView ], "$angular_makeEventHandler", function(event, scope, handler){
if (event != "change") return this.next();
handler = parse(handler);
return function(ev) {
var widget = ev.sender;
var options = widget.options;
var dataSource = widget.dataSource;
var cell, multiple, locals = { kendoEvent: ev }, elems, items, columns, colIdx;
if (angular.isString(options.selectable)) {
cell = options.selectable.indexOf('cell') !== -1;
multiple = options.selectable.indexOf('multiple') !== -1;
}
elems = locals.selected = this.select();
items = locals.data = [];
columns = locals.columns = [];
for (var i = 0; i < elems.length; i++) {
var item = cell ? elems[i].parentNode : elems[i];
var itemUid = $(item).attr(_UID_);
var dataItem = dataSource.getByUid(itemUid);
if (cell) {
if (angular.element.inArray(dataItem, items) < 0) {
items.push(dataItem);
}
colIdx = angular.element(elems[i]).index();
if (angular.element.inArray(colIdx, columns) < 0 ) {
columns.push(colIdx);
}
} else {
items.push(dataItem);
}
}
if (!multiple) {
locals.data = items[0];
locals.selected = elems[0];
}
scope.$apply(function() { handler(scope, locals) });
};
});
// for PanelBar, TabStrip and Splitter, hook on `contentLoad` to
// compile Angular templates.
defadvice([ kendo.ui.PanelBar, kendo.ui.TabStrip, kendo.ui.Splitter ], AFTER, function() {
this.next();
var self = this.self;
var scope = angular.element(self.element).scope();
if (scope) bindBefore(self, "contentLoad", function(ev){
// tabstrip/panelbar splitter
var contentElement = ev.contentElement || ev.pane;
compile(ev.contentElement)(scope);
digest(scope);
});
});
// on Draggable::_start compile the content as Angular template, if
// an $angular_scope method is provided.
defadvice(kendo.ui.Draggable, "_start", function(){
this.next();
var self = this.self;
if (self.hint) {
var scope = angular.element(self.currentTarget).scope();
if (scope) {
compile(self.hint)(scope);
digest(scope);
}
}
});
// If no `template` is supplied for Grid columns, provide an Angular
// template. The reason is that in this way AngularJS will take
// care to update the view as the data in scope changes.
defadvice(kendo.ui.Grid, BEFORE, function(element, options){
this.next();
if (options.columns) angular.forEach(options.columns, function(col){
if (col.field && !col.template && !col.format) {
col.template = "<span ng-bind='dataItem." + col.field + "'>#: " + col.field + "#</span>";
}
});
});
// for Grid, ListView and TreeView, provide a dataBound handler that
// recompiles Angular templates. We need to do this before the
// widget is initialized so that we catch the first dataBound event.
defadvice([ kendo.ui.Grid, kendo.ui.ListView, kendo.ui.TreeView ], BEFORE, function(element, options){
this.next();
var scope = angular.element(element).scope();
if (!scope) return;
var self = this.self;
var role = self.options.name;
var prev_dataBound = options.dataBound;
options.dataBound = function(ev) {
var widget = ev.sender;
var dataSource = widget.dataSource;
var dirty = false;
widget.items().each(function(){
// XXX HACK: the tree view will call dataBound multiple
// times, sometimes for LI-s containing nested items that
// may have been already compiled. Therefore in this
// situation we compile the ".k-in" element, which contains
// only the template for a single item.
var elementToCompile = role == "TreeView"
? $(this).find(".k-in").first()
: $(this);
if (!elementToCompile.hasClass("ng-scope")) {
var itemUid = $(this).attr(_UID_);
var item = dataSource.getByUid(itemUid);
var itemScope = scope.$new();
itemScope.dataItem = item;
compile(elementToCompile)(itemScope);
dirty = true;
}
});
try {
if (prev_dataBound) return prev_dataBound.apply(this, arguments);
} finally {
if (dirty) digest(scope);
}
};
});
// templates for autocomplete and combo box
defadvice([ kendo.ui.AutoComplete, kendo.ui.ComboBox ], BEFORE, function(element, options){
this.next();
var scope = angular.element(element).scope();
if (!scope) return;
var self = this.self;
var prev_dataBound = options.dataBound;
options.dataBound = function(ev) {
var widget = ev.sender;
var dataSource = widget.dataSource;
var dirty = false;
$(widget.items()).each(function(){
var el = $(this);
if (!el.hasClass("ng-scope")) {
var item = widget.dataItem(el.index());
var itemScope = scope.$new();
itemScope.dataItem = item;
compile(el)(itemScope);
dirty = true;
}
});
try {
if (prev_dataBound) return prev_dataBound.apply(this, arguments);
} finally {
if (dirty) digest(scope);
}
};
});
defadvice([ kendo.ui.AutoComplete, kendo.ui.ComboBox ], AFTER, function(){
this.next();
this.self.bind("dataBinding", function(ev){
$(ev.sender.items()).each(function(){
var scope = angular.element(this).scope();
if (scope) {
scope.$destroy();
}
});
});
});
defadvice([ kendo.ui.Grid, kendo.ui.ListView ], AFTER, function(){
this.next();
var self = this.self;
var scope = angular.element(self.element).scope();
if (!scope) return;
// itemChange triggers when a single item is changed through our
// DataSource mechanism.
self.bind("itemChange", function(ev) {
var dataSource = ev.sender.dataSource;
var itemElement = ev.item[0];
var itemScope = scope.$new();
itemScope.dataItem = dataSource.getByUid(ev.item.attr(_UID_));
compile(itemElement)(itemScope);
digest(itemScope);
});
// dataBinding triggers when new data is loaded. We use this to
// destroy() each item's scope.
//
// BUG with this code on: create a simple grid with popup editing.
// Click edit in some row (the window opens). Click Cancel (the
// window disappears). Click Edit again (the window opens).
// Click Update (the window disappears, without animation). The
// grid is now dead (does not respond to any more events). I
// cannot figure out why this happens, but until it's fixed, gonna
// leave this note here and this code commented out.
//
// self.bind("dataBinding", function(ev) {
// ev.sender.items().each(function(){
// if ($(this).attr(_UID_)) {
// var rowScope = angular.element(this).scope();
// if (rowScope) rowScope.$destroy();
// }
// });
// });
});
defadvice(kendo.ui.Grid, "_toolbar", function(){
this.next();
var self = this.self;
var scope = angular.element(self.element).scope();
if (scope) {
compile(self.wrapper.find(".k-grid-toolbar").first())(scope);
digest(scope);
}
});
defadvice(kendo.ui.Grid, "_thead", function(){
this.next();
var self = this.self;
var scope = angular.element(self.element).scope();
if (scope) {
compile(self.thead)(scope);
digest(scope);
}
});
defadvice(kendo.ui.editor.Toolbar, "render", function(){
this.next();
var self = this.self;
var scope = angular.element(self.element).scope();
if (scope) {
compile(self.element)(scope);
digest(scope);
}
});
defadvice(kendo.ui.Grid, AFTER, function(){
this.next();
var self = this.self;
var scope = angular.element(self.element).scope();
if (scope) {
if (self.options.detailTemplate) bindBefore(self, "detailInit", function(ev){
var detailScope = scope.$new();
detailScope.dataItem = ev.data;
compile(ev.detailCell)(detailScope);
digest(detailScope);
});
}
});
defadvice(kendo.ui.Editable, "refresh", function(){
this.next();
var self = this.self;
var model = self.options.model;
var scope = angular.element(self.element).scope();
if (!scope || !model) return;
scope = self.$angular_scope = scope.$new();
scope.dataItem = model;
// XXX: we need to disable the timeout here, or else the widget is
// created but immediately destroyed (focus lost).
immediately(function(){
compile(self.element)(scope);
digest(scope);
});
// and we still need to focus it.
self.element.find(":kendoFocusable").eq(0).focus();
});
defadvice(kendo.ui.Editable, "destroy", function(){
var self = this.self;
if (self.$angular_scope) {
self.$angular_scope.$destroy();
self.$angular_scope = null;
}
this.next();
timeout(function(){
var scope = angular.element(self.element).scope();
if (scope) {
compile(self.element)(scope);
digest(scope);
}
});
});
}, typeof define == 'function' && define.amd ? define : function(_, f){ f(jQuery, angular, kendo); });
// Local Variables:
// js-indent-level: 2
// js2-basic-offset: 2
// End:
(function(angular) {'use strict';
// declare all the module
angular.module('kendo.directives', []);
angular.module('kendo.directives', [], ['$provide', function($provide){
// Iterate over the kendo.ui and kendo.dataviz.ui namespace objects to get the Kendo UI widgets adding
// them to the 'widgets' array.
var widgets = [];
angular.forEach([kendo.ui, kendo.dataviz && kendo.dataviz.ui], function(namespace) {
angular.forEach(namespace, function(value, key) {
// add all widgets
if( key.match(/^[A-Z]/) && key !== 'Widget' ){
widgets.push("kendo" + key);
}
});
});
$provide.value('kendoWidgets', widgets);
}]);
angular.module('kendo.directives').provider('kendoDecorator', [ function() {
var provider = this, DECORATORS = '$kendoOptionsDecorators';
var globalOptionsDecorators = {};
// add an options decorator to be applied on all specified widget, not specific instances
provider.addGlobalOptionsDecorator = function(widgetName, decorator) {
if( angular.isString(widgetName) && angular.isFunction(decorator) ) {
globalOptionsDecorators[widgetName] = globalOptionsDecorators[widgetName] || [];
globalOptionsDecorators[widgetName].push(decorator);
return function() {
globalOptionsDecorators[widgetName].splice(globalOptionsDecorators[widgetName].indexOf(decorator), 1);
};
}
throw new Error('Illegal Arguments');
};
// get the global list of options decorators for a specified widget. Useful for ordering
provider.getGlobalOptionsDecorator = function(widgetName) {
return globalOptionsDecorators[widgetName] || [];
};
provider.$get = [function() {
// returns the decorators associated to the specified element
function getOptionsDecorators(element) {
var decorators = element.data(DECORATORS);
if( !angular.isArray(decorators) ) {
decorators = [];
element.data(DECORATORS, decorators);
}
return decorators;
}
function invokeDecorators(element, decorators, opts) {
for( var i = 0; i < decorators.length; i++ ) {
decorators[i](element, opts);
}
}
// invokes the provided element's decorators and global operators on the provided options object.
function decorateOptions(element, widgetName, opts) {
var i, decorators = provider.getGlobalOptionsDecorator(widgetName);
invokeDecorators(element, decorators, opts);
// get decorators for element
decorators = element.data(DECORATORS) || [];
invokeDecorators(element, decorators, opts);
}
function addOptionsDecorator(element, decorator) {
if( angular.isFunction(decorator) ) {
var decorators = getOptionsDecorators(element);
decorators.push(decorator);
return function() {
decorators.splice(decorators.indexOf(decorator), 1);
};
}
}
return {
getOptionsDecorator: getOptionsDecorators,
addOptionsDecorator: addOptionsDecorator,
decorateOptions: decorateOptions
};
}];
}]);
angular.module('kendo.directives').factory('widgetFactory', ['$parse', '$log', 'kendoDecorator', function($parse, $log, kendoDecorator) {
// k-* attributes that should not be $parsed or $evaluated by gatherOptions
var ignoredAttributes = {
kDataSource: true,
kOptions: true,
kRebind: true
};
var mixin = function(kendoWidget, scope, options, attrName, attrValue) {
// regexp for matching regular options attributes and event handler attributes
// The first matching group will be defined only when the attribute starts by k-on- for event handlers.
// The second matching group will contain the option name.
var matchExp = /k(On)?([A-Z].*)/;
// ignore attributes that do not map to widget configuration options
if( ignoredAttributes[attrName] ) {
return;
}
var match = attrName.match(matchExp), optionName, fn;
if( match ) {
// Lowercase the first letter to match the option name kendo expects.
optionName = match[2].charAt(0).toLowerCase() + match[2].slice(1);
if( match[1] ) {
// This is an event handler attribute (k-on-*)
// Parse the expression so that it get evaluated later.
fn = $parse(attrValue);
// Add a kendo event listener to the options.
options[optionName] = function(e) {
// Make sure this gets invoked in the angularjs lifecycle.
if(scope.$root.$$phase === '$apply' || scope.$root.$$phase === '$digest') {
fn({kendoEvent: e});
} else {
scope.$apply(function() {
// Invoke the parsed expression with a kendoEvent local that the expression can use.
fn(scope, {kendoEvent: e});
});
}
};
} else {
// Evaluate the angular expression and put its result in the widget's options object.
// Here we make a copy because the kendo widgets make changes to the objects passed in the options
// and kendo-refresh would not be able to refresh with the initial values otherwise.
options[optionName] = angular.copy(scope.$eval(attrValue));
if( options[optionName] === undefined && attrValue.match(/^\w*$/) ) {
// if the user put a single word as the attribute value and the expression evaluates to undefined,
// she may have wanted to use a string literal.
$log.warn(kendoWidget + '\'s ' + attrName + ' attribute resolved to undefined. Maybe you meant to use a string literal like: \'' + attrValue + '\'?');
}
}
}
};
// Gather the options from defaults and from attributes
var gatherOptions = function(scope, element, attrs, kendoWidget) {
// TODO: add kendoDefaults value service and use it to get a base options object?
// var options = kendoDefaults[kendoWidget];
// make a deep clone of the options object provided by the k-options attribute, if any.
var options = angular.element.extend(true, {}, scope.$eval(attrs.kOptions));
// Mixin the data from the element's k-* attributes in the options
angular.forEach(attrs, function(value, name) {
mixin(kendoWidget, scope, options, name, value);
});
// The kDataSource directive sets the $kendoDataSource data on the element it is put on.
// A datasource set in this way takes precedence over the one that could have been provided in options object passed
// in the directive's attribute and that is used as the initial options object.
options.dataSource = element.inheritedData('$kendoDataSource') || options.dataSource;
// decorate options, if any decorators have been registered on this element or any global ones are registered for
// the kendo widget
kendoDecorator.decorateOptions(element, kendoWidget, options);
return options;
};
// Create the kendo widget with gathered options
var create = function(scope, element, attrs, kendoWidget) {
// Create the options object
var options = gatherOptions(scope, element, attrs, kendoWidget);
// Bind the kendo widget to the element and return a reference to the widget.
return element[kendoWidget](options).data(kendoWidget);
};
return {
create: create
};
}]);
angular.module('kendo.directives').factory('directiveFactory', ['widgetFactory', '$timeout', '$parse',
function(widgetFactory, $timeout, $parse) {
function exposeWidget(widget, scope, attrs, kendoWidget) {
if( attrs[kendoWidget] ) {
// expose the widget object
var set = $parse(attrs[kendoWidget]).assign;
if( set ) {
// set the value of the expression to the kendo widget object to expose its api
set(scope, widget);
} else {
throw new Error( kendoWidget + ' attribute used but expression in it is not assignable: ' + attrs[kendoWidget]);
}
}
}
// $timeout tracking
var $timeoutPromise = null;
var unsetTimeoutPromise = function() { $timeoutPromise = null };
var create = function(kendoWidget) {
return {
// Parse the directive for attributes and classes
restrict: 'ACE',
transclude: true,
require: '?ngModel',
scope: false,
controller: [ '$scope', '$attrs', '$element', '$transclude', function($scope, $attrs, $element, $transclude) {
// Make the element's contents available to the kendo widget to allow creating some widgets from existing elements.
$transclude(function(clone){
$element.append(clone);
});
}],
link: function(scope, element, attrs, ngModel) {
var widget;
// Instead of having angular digest each component that needs to be setup
// Use the same timeout until the timeout has been executed, this will cause all
// directives to be evaluated in the next cycle, instead of over multiple cycles.
if (!$timeoutPromise)
$timeoutPromise = $timeout(unsetTimeoutPromise);
// Bind kendo widget to element only once interpolation on attributes is done.
// Using a $timeout with no delay simply makes sure the function will be executed next in the event queue
// after the current $digest cycle is finished. Other directives on the same element (select for example)
// will have been processed, and interpolation will have happened on the attributes.
$timeoutPromise.then( function() {
// create the kendo widget and bind it to the element.
widget = widgetFactory.create(scope, element, attrs, kendoWidget);
exposeWidget(widget, scope, attrs, kendoWidget);
// if k-rebind attribute is provided, rebind the kendo widget when
// the watched value changes
if( attrs.kRebind ) {
// watch for changes on the expression passed in the k-rebind attribute
scope.$watch(attrs.kRebind, function(newValue, oldValue) {
if(newValue !== oldValue) {
// create the kendo widget and bind it to the element.
widget = widgetFactory.create(scope, element, attrs, kendoWidget);
exposeWidget(widget, scope, attrs, kendoWidget);
}
}, true); // watch for object equality. Use native or simple values.
}
// Cleanup after ourselves
scope.$on( '$destroy', function() {
widget.destroy();
});
// if ngModel is on the element, we setup bi-directional data binding
if (ngModel) {
if( !widget.value ) {
throw new Error('ng-model used but ' + kendoWidget + ' does not define a value accessor');
}
// Angular will invoke $render when the view needs to be updated with the view value.
ngModel.$render = function() {
// Update the widget with the view value.
widget.value(ngModel.$viewValue || null);
};
// if the model value is undefined, then we set the widget value to match ( == null/undefined )
if (widget.value !== undefined) {
widget.value(ngModel.$viewValue || null);
}
// In order to be able to update the angular scope objects, we need to know when the change event is fired for a Kendo UI Widget.
widget.bind("change", function(e) {
if(scope.$root.$$phase === '$apply' || scope.$root.$$phase === '$digest') {
ngModel.$setViewValue(widget.value());
} else {
scope.$apply(function() {
ngModel.$setViewValue(widget.value());
});
}
});
}
});
}
};
};
return {
create: create
};
}
]);
(function(angular) {
var widgets = angular.injector(['kendo.directives']).get('kendoWidgets');
// loop through all the widgets and create a directive
angular.forEach(widgets, function(widget) {
angular.module('kendo.directives').directive(widget, ['directiveFactory',
function(directiveFactory) {
return directiveFactory.create(widget);
}
]);
});
}(angular));
// ## The kendoSource directive allows setting the Kendo UI DataSource of a widget directly from the HTML.
angular.module('kendo.directives').directive('kDataSource', [function(){
return {
// This is an attribute directive
restrict: 'A',
controller: ['$scope', '$attrs', '$element', function($scope, $attrs, $element){
var widgetType = getWidgetType($attrs);
var dataSourceType = getDataSourceType(widgetType);
// Set $kendoDataSource in the element's data. 3rd parties can define their own dataSource creation
// directive and provide this data on the element.
$element.data('$kendoDataSource', toDataSource($scope.$eval($attrs.kDataSource), dataSourceType));
// Keep the element's data up-to-date with changes.
$scope.$watch($attrs.kDataSource, function(newDataSource, oldDataSource){
if(newDataSource !== oldDataSource){
$element.data('$kendoDataSource', toDataSource(newDataSource, dataSourceType));
}
});
}]
};
// Returns the DataSource type based on the widgetType
function getDataSourceType(widgetType){
var hierarchicalDataSourceWidgets = ['TreeView'];
var schedulerDataSourceWidgets = ['Scheduler'];
if(jQuery.inArray(widgetType, hierarchicalDataSourceWidgets) !== -1){
return 'HierarchicalDataSource';
}
else if(jQuery.inArray(widgetType, schedulerDataSourceWidgets) !== -1){
return 'SchedulerDataSource';
}
else{
return 'DataSource';
}
}
// Returns the widgetType, eg: 'TreeView'
function getWidgetType(attributes){
for(var attribute in attributes){
if(attributes.hasOwnProperty(attribute) && attribute.match(/kendo/)){
return attribute.replace('kendo', '');
}
}
}
// Transforms the object into a Kendo UI DataSource.
function toDataSource(dataSource, dataSourceType){
// TODO: if ds is a $resource, wrap it in a kendo dataSource using an injected service
return kendo.data[dataSourceType].create(dataSource);
}
}]);
angular.module('kendo.directives').directive('kendoGrid', ['$compile', 'kendoDecorator', '$parse', function($compile, kendoDecorator, $parse) {
function dataBoundHandler(scope, element, rowDataVar) {
var grid = element.data('kendoGrid');
var rows = grid.tbody.children('tr');
// Here we mimic ng-repeat in that we create a scope for each row that we can then destroy in dataBinding event.
// Creating a scope for each row ensures you don't leak scopes when the
// kendo widget regenerates the dom on pagination for example.
rows.each(function(index, row) {
var rowScope = scope.$new();
// provide index of the row using the same $index var as ngRepeat
rowScope.$index = index;
// provide the data object for that row in the scope
rowScope[rowDataVar] = grid.dataItem(row);
// compile the row. You can now use angular templates in that row.
$compile(row)(rowScope);
});
}
function dataBindingHandler(element) {
// find all the rows that we compiled in dataBound handler
var rows = element.data('kendoGrid').tbody.children('tr.ng-scope');
// here we need to destroy the scopes that we created in dataBound handler to make sure no scopes are leaked.
rows.each(function(index, rowElement) {
var rowScope = angular.element(rowElement).scope();
// destroy the scope
rowScope.$destroy();
});
}
function createCompileRowsDecorator(scope, rowDataVar) {
return function(element, options) {
// keep a reference on the original event callbacks
var origDataBinding = options.dataBinding;
var origDataBound = options.dataBound;
// The kendoGrid invokes this handler after it has created row elements for the data.
options.dataBound = function() {
dataBoundHandler(scope, element, rowDataVar);
// invoke the original dataBound handler, if any
if(angular.isFunction(origDataBound)) {
origDataBound();
}
};
// The kendoGrid invokes this handler before it creates new rows in the dom
options.dataBinding = function() {
dataBindingHandler(element);
// invoke the original dataBinding handler, if any
if(angular.isFunction(origDataBinding)) {
origDataBinding();
}
};
};
}
function createChangeDecorator(scope, changeExpFn) {
return function(element, options) {
options.change = function(e) {
var cell, multiple, locals = { kendoEvent: e }, elems, items, columns, colIdx;
if( angular.isString(options.selectable) ) {
cell = options.selectable.indexOf('cell') !== -1;
multiple = options.selectable.indexOf('multiple') !== -1;
}
elems = locals.selected = this.select();
items = locals.data = [];
columns = locals.columns = [];
for (var i = 0; i < elems.length; i++) {
var dataItem = this.dataItem(cell ? elems[i].parentNode : elems[i]);
if( cell ) {
if (angular.element.inArray(dataItem, items) < 0) {
items.push(dataItem);
}
colIdx = angular.element(elems[i]).index();
if (angular.element.inArray(colIdx, columns) < 0 ) {
columns.push(colIdx);
}
} else {
items.push(dataItem);
}
}
if( !multiple ) {
locals.data = items[0];
locals.selected = elems[0];
}
// Make sure this gets invoked in the angularjs lifecycle.
scope.$apply(function() {
// Invoke the parsed expression with a kendoEvent local that the expression can use.
changeExpFn(scope, locals);
});
};
};
}
return {
restrict: 'ACE',
link: function(scope, element, attrs) {
kendoDecorator.addOptionsDecorator(element, createCompileRowsDecorator(scope, 'dataItem'));
// if k-on-change was defined, expose the selected rows/cells and not just the kendo event
if( attrs.kOnChange ) {
kendoDecorator.addOptionsDecorator(element, createChangeDecorator(scope, $parse(attrs.kOnChange)));
}
}
};
}]);
}(angular));