angular-ux-datagrid.js |
|
! ux-angularjs-datagrid v.1.4.11 (c) 2016, Obogo https://github.com/obogo/ux-angularjs-datagrid License: MIT. |
(function (exports, global) {
if (typeof define === "function" && define.amd) {
define(exports);
} else if (typeof module !== "undefined" && module.exports) {
module.exports = exports;
} else {
global.ux = exports;
}
|
! ux-angularjs-datagrid v.1.4.11 (c) 2016, Obogo https://github.com/obogo/ux-angularjs-datagrid License: MIT. |
(function(exports, global) {
global["util"] = exports;
var define, internal, finalize = function() {};
(function() {
var get, defined, pending, definitions, initDefinition, $cachelyToken = "~", $depsRequiredByDefinitionToken = ".";
get = Function[$cachelyToken] = Function[$cachelyToken] || function(name) {
if (!get[name]) {
get[name] = {};
}
return get[name];
};
definitions = get("c");
defined = get("d");
pending = get("p");
initDefinition = function(name) {
var args = arguments;
var val = args[1];
if (typeof val === "function") {
defined[name] = val();
} else {
definitions[name] = args[2];
definitions[name][$depsRequiredByDefinitionToken] = val;
}
};
define = internal = function() {
initDefinition.apply(null, arguments);
};
resolve = function(name, fn) {
pending[name] = true;
var deps = fn[$depsRequiredByDefinitionToken];
var args = [];
var i, len;
var dependencyName;
if (deps) {
len = deps.length;
for (i = 0; i < len; i++) {
dependencyName = deps[i];
if (definitions[dependencyName]) {
if (pending.hasOwnProperty(dependencyName)) {
throw new Error('Cyclical reference: "' + name + '" referencing "' + dependencyName + '"');
}
resolve(dependencyName, definitions[dependencyName]);
delete definitions[dependencyName];
}
}
}
if (!defined.hasOwnProperty(name)) {
for (i = 0; i < len; i++) {
dependencyName = deps[i];
args.push(defined.hasOwnProperty(dependencyName) && defined[dependencyName]);
}
defined[name] = fn.apply(null, args);
}
delete pending[name];
};
finalize = function() {
for (var name in definitions) {
resolve(name, definitions[name]);
}
};
return define;
})();
|
! ################# YOUR CODE STARTS HERE #################### // ! node_modules/hbjs/src/utils/formatters/toArray.js |
define("toArray", [ "isArguments", "isArray", "isUndefined" ], function(isArguments, isArray, isUndefined) {
var toArray = function(value) {
if (isArguments(value)) {
return Array.prototype.slice.call(value, 0) || [];
}
try {
if (isArray(value)) {
return value;
}
if (!isUndefined(value)) {
return [].concat(value);
}
} catch (e) {}
return [];
};
return toArray;
});
|
! util/hb/src/api.js |
define("dg.api", [ "isMatch", "apply", "toArray", "sort", "dispatcher", "matchAll" ], function(isMatch, apply, toArray, sort, dispatcher, matchAll) {
exports.isMatch = isMatch;
exports.apply = apply;
exports.dispatcher = dispatcher;
exports.matchAll = matchAll;
exports.array = {
toArray: toArray,
sort: sort
};
});
|
! node_modules/hbjs/src/utils/validators/isRegExp.js |
define("isRegExp", function() {
var isRegExp = function(value) {
return Object.prototype.toString.call(value) === "[object RegExp]";
};
return isRegExp;
});
|
! node_modules/hbjs/src/utils/validators/isDate.js |
define("isDate", function() {
var isDate = function(val) {
return val instanceof Date;
};
return isDate;
});
|
! node_modules/hbjs/src/utils/data/apply.js |
define("apply", [ "isFunction" ], function(isFunction) {
return function(func, scope, args) {
if (!isFunction(func)) {
return;
}
args = args || [];
switch (args.length) {
case 0:
return func.call(scope);
case 1:
return func.call(scope, args[0]);
case 2:
return func.call(scope, args[0], args[1]);
case 3:
return func.call(scope, args[0], args[1], args[2]);
case 4:
return func.call(scope, args[0], args[1], args[2], args[3]);
case 5:
return func.call(scope, args[0], args[1], args[2], args[3], args[4]);
case 6:
return func.call(scope, args[0], args[1], args[2], args[3], args[4], args[5]);
}
return func.apply(scope, args);
};
});
|
! node_modules/hbjs/src/utils/validators/isFunction.js |
define("isFunction", function() {
var isFunction = function(val) {
return typeof val === "function";
};
return isFunction;
});
|
! node_modules/hbjs/src/utils/validators/isMatch.js |
define("isMatch", [ "isRegExp", "isDate" ], function(isRegExp, isDate) {
var primitive = [ "string", "number", "boolean" ];
function isMatch(item, filterObj) {
var itemType;
if (item === filterObj) {
return true;
} else if (typeof filterObj === "object") {
itemType = typeof item;
if (primitive.indexOf(itemType) !== -1) {
if (isRegExp(filterObj) && !filterObj.test(item + "")) {
return false;
} else if (isDate(filterObj)) {
if (isDate(item) && filterObj.getTime() === item.getTime()) {
return true;
}
return false;
}
}
if (item instanceof Array && filterObj[0] !== undefined) {
for (var i = 0; i < item.length; i += 1) {
if (isMatch(item[i], filterObj[0])) {
return true;
}
}
return false;
} else {
for (var j in filterObj) {
if (filterObj.hasOwnProperty(j)) {
if (item[j] === undefined && !item.hasOwnProperty(j)) {
return false;
}
if (!isMatch(item[j], filterObj[j])) {
return false;
}
}
}
}
return true;
} else if (typeof filterObj === "function") {
return !!filterObj(item);
}
return false;
}
return isMatch;
});
|
! node_modules/hbjs/src/utils/validators/isArguments.js |
define("isArguments", [ "toString" ], function(toString) {
var isArguments = function(value) {
var str = String(value);
var isArguments = str === "[object Arguments]";
if (!isArguments) {
isArguments = str !== "[object Array]" && value !== null && typeof value === "object" && typeof value.length === "number" && value.length >= 0 && (!value.callee || toString.call(value.callee) === "[object Function]");
}
return isArguments;
};
return isArguments;
});
|
! node_modules/hbjs/src/utils/validators/isArray.js |
define("isArray", function() {
Array.prototype.__isArray = true;
Object.defineProperty(Array.prototype, "__isArray", {
enumerable: false,
writable: true
});
var isArray = function(val) {
return val ? !!val.__isArray : false;
};
return isArray;
});
|
! node_modules/hbjs/src/utils/validators/isUndefined.js |
define("isUndefined", function() {
var isUndefined = function(val) {
return typeof val === "undefined";
};
return isUndefined;
});
|
! node_modules/hbjs/src/utils/array/sort.js |
define("sort", function() {
function partition(array, left, right, compareFunction) {
var cmp = array[right - 1], minEnd = left, maxEnd, dir = 0;
for (maxEnd = left; maxEnd < right - 1; maxEnd += 1) {
dir = compareFunction(array[maxEnd], cmp);
if (dir < 0) {
if (maxEnd !== minEnd) {
swap(array, maxEnd, minEnd);
}
minEnd += 1;
}
}
if (compareFunction(array[minEnd], cmp)) {
swap(array, minEnd, right - 1);
}
return minEnd;
}
function swap(array, i, j) {
var temp = array[i];
array[i] = array[j];
array[j] = temp;
return array;
}
function quickSort(array, left, right, fn) {
if (left < right) {
var p = partition(array, left, right, fn);
quickSort(array, left, p, fn);
quickSort(array, p + 1, right, fn);
}
return array;
}
return function(array, compareFunction) {
var result = quickSort(array, 0, array.length, compareFunction);
return result;
};
});
|
! node_modules/hbjs/src/utils/async/dispatcher.js |
define("dispatcher", [ "apply", "isFunction" ], function(apply, isFunction) {
function Event(type) {
this.type = type;
this.defaultPrevented = false;
this.propagationStopped = false;
this.immediatePropagationStopped = false;
}
Event.prototype.preventDefault = function() {
this.defaultPrevented = true;
};
Event.prototype.stopPropagation = function() {
this.propagationStopped = true;
};
Event.prototype.stopImmediatePropagation = function() {
this.immediatePropagationStopped = true;
};
Event.prototype.toString = function() {
return this.type;
};
function validateEvent(e) {
if (!e) {
throw Error("event cannot be undefined");
}
}
var dispatcher = function(target, scope, map) {
if (target && target.on && target.on.dispatcher) {
return target;
}
target = target || {};
var listeners = {};
function off(event, callback) {
validateEvent(event);
var index, list;
list = listeners[event];
if (list) {
if (callback) {
index = list.indexOf(callback);
if (index !== -1) {
list.splice(index, 1);
}
} else {
list.length = 0;
}
}
}
function on(event, callback) {
if (isFunction(callback)) {
validateEvent(event);
listeners[event] = listeners[event] || [];
listeners[event].push(callback);
return function() {
off(event, callback);
};
}
}
on.dispatcher = true;
function once(event, callback) {
if (isFunction(callback)) {
validateEvent(event);
function fn() {
off(event, fn);
apply(callback, scope || target, arguments);
}
return on(event, fn);
}
}
function getListeners(event, strict) {
validateEvent(event);
var list, a = "*";
if (event || strict) {
list = [];
if (listeners[a]) {
list = listeners[a].concat(list);
}
if (listeners[event]) {
list = listeners[event].concat(list);
}
return list;
}
return listeners;
}
function removeAllListeners() {
listeners = {};
}
function fire(callback, args) {
return callback && apply(callback, target, args);
}
function dispatch(event) {
validateEvent(event);
var list = getListeners(event, true), len = list.length, i, event = typeof event === "object" ? event : new Event(event);
if (len) {
arguments[0] = event;
for (i = 0; i < len; i += 1) {
if (!event.immediatePropagationStopped) {
fire(list[i], arguments);
}
}
}
return event;
}
if (scope && map) {
target.on = scope[map.on] && scope[map.on].bind(scope);
target.off = scope[map.off] && scope[map.off].bind(scope);
target.once = scope[map.once] && scope[map.once].bind(scope);
target.dispatch = target.fire = scope[map.dispatch].bind(scope);
} else {
target.on = on;
target.off = off;
target.once = once;
target.dispatch = target.fire = dispatch;
}
target.getListeners = getListeners;
target.removeAllListeners = removeAllListeners;
return target;
};
return dispatcher;
});
|
! node_modules/hbjs/src/utils/iterators/matchAll.js |
define("matchAll", [ "isMatch" ], function(isMatch) {
function matchAll(ary, filterObj) {
var result = [];
for (var i = 0; i < ary.length; i += 1) {
if (isMatch(ary[i], filterObj)) {
result.push(ary[i]);
}
}
return result;
}
return matchAll;
});
|
! ################# YOUR CODE ENDS HERE #################### // |
finalize();
})(this["util"] || {}, function() {
return exports;
}());
exports.errors = {
E1000: "Datagrid cannot have a height of 0",
E1001: "RENDER STATE INVALID. The only valid render states are those on ux.datagrid.states",
E1002: "Unable to render. Invalid activeRange.",
E1101: "Script templates that are used for datagrid rows must have a height greater than 0. This may be because the grid is not yet attached to the dom preventing it from calculating heights.",
E1102: "at least one template is required. There were no row templates found for the datagrid."
};
|
¶ Configsux.datagrid is a highly performant scrolling list for desktop and mobile devices that leverages the browsers ability to gpu cache the dom structure with fast startup times and optimized rendering that allows the gpu to maintain its snapshots as long as possible. Create the default module of ux if it doesn't already exist. |
var module, isIOS = !!navigator.userAgent.match(/(iPad|iPhone|iPod)/g);
try {
module = angular.module("ux", [ "ng" ]);
} catch (e) {
module = angular.module("ux");
}
|
Create the datagrid namespace. add the default options for the datagrid. These can be overridden by passing your own options to each instance of the grid. In your HTML templates you can provide the object that will override these settings on a per grid basis. ...
These options are then available to other addons to configure them. |
exports.datagrid = {
|
version: "1.4.11",
|
|
iOS does not natively support smooth scrolling without a css attribute. |
isIOS: isIOS,
|
states: {
BUILDING: "datagrid:building",
READY: "datagrid:ready"
},
|
|
The events are in three categories based on if they notify of something that happened in the grid then they start with an ON_ or if they are driving the behavior of the datagrid, or they are logging events. ¶ Notifying Events
|
events: {
ON_INIT: "datagrid:onInit",
ON_LISTENERS_READY: "datagrid:onListenersReady",
ON_READY: "datagrid:onReady",
ON_STARTUP_COMPLETE: "datagrid:onStartupComplete",
ON_BEFORE_RENDER: "datagrid:onBeforeRender",
ON_AFTER_RENDER: "datagrid:onAfterRender",
ON_BEFORE_UPDATE_WATCHERS: "datagrid:onBeforeUpdateWatchers",
ON_AFTER_UPDATE_WATCHERS: "datagrid:onAfterUpdateWatchers",
ON_BEFORE_DATA_CHANGE: "datagrid:onBeforeDataChange",
ON_AFTER_DATA_CHANGE: "datagrid:onAfterDataChange",
ON_BEFORE_RENDER_AFTER_DATA_CHANGE: "datagrid:onBeforeRenderAfterDataChange",
ON_RENDER_AFTER_DATA_CHANGE: "datagrid:onRenderAfterDataChange",
ON_ROW_TEMPLATE_CHANGE: "datagrid:onRowTemplateChange",
ON_SCROLL: "datagrid:onScroll",
ON_BEFORE_RESET: "datagrid:onBeforeReset",
ON_AFTER_RESET: "datagrid:onAfterReset",
ON_AFTER_HEIGHTS_UPDATED: "datagrid:onAfterHeightsUpdated",
ON_AFTER_HEIGHTS_UPDATED_RENDER: "datagrid:onAfterHeightsUpdatedRender",
ON_BEFORE_ROW_DEACTIVATE: "datagrid:onBeforeRowDeactivate",
|
handy for knowing when to remove jquery listeners. |
ON_AFTER_ROW_ACTIVATE: "datagrid:onAFterRowActivate",
|
handy for turning jquery listeners back on. |
ON_ROW_COMPILE: "datagrid:onRowCompile",
ON_SCROLL_TO_TOP: "datagrid:onScrollToTop",
ON_SCROLL_TO_BOTTOM: "datagrid:onScrollToBottom",
|
¶ Driving Events
|
RESIZE: "datagrid:resize",
UPDATE: "datagrid:update",
SCROLL_TO_INDEX: "datagrid:scrollToIndex",
SCROLL_TO_ITEM: "datagrid:scrollToItem",
SCROLL_INTO_VIEW: "datagrid:scrollIntoView",
|
¶ Log Events
|
LOG: "datagrid:log",
INFO: "datagrid:info",
WARN: "datagrid:warn",
ERROR: "datagrid:error"
},
getGrid: function(scope) {
var result;
while (scope) {
if (scope.datagrid) {
return scope.datagrid;
} else if (scope.$$childHead) {
result = exports.datagrid.getGrid(scope.$$childHead);
if (result) {
return result;
}
}
scope = scope.$$nextSibling;
}
return null;
},
throwError: function(msg) {
if (window.console && console.warn) {
console.warn(msg);
}
},
|
options: {
|
|
|
async: true,
|
|
updateDelay: 100,
|
|
creepRender: {
enable: true
},
|
|
creepStartDelay: 1e3,
|
|
cushion: -100,
chunks: {
|
|
detachDom: null,
|
|
size: 50,
|
|
chunkClass: "datagrid-chunk",
|
|
chunkDisabledClass: "datagrid-chunk-disabled",
|
|
chunkReadyClass: "datagrid-chunk-ready"
},
scrollModel: {
|
|
speed: 5,
|
|
manual: true,
|
|
simulateClick: false,
|
|
preventTouchMove: false
},
|
|
compiledClass: "compiled",
|
|
uncompiledClass: "uncompiled",
|
|
contentClass: "datagrid-content",
|
|
rowClass: "datagrid-row",
|
|
renderThreshold: 1,
|
|
renderThresholdWait: 50,
|
|
renderWhileScrolling: false,
|
|
creepLimit: 500,
|
|
smartUpdate: true,
|
|
readyToRenderRetryMax: 10,
|
|
minHeight: 100
},
|
the core addons are the ones that are built into the angular-ux-datagrid. This array is used when the grid starts up to add all of these addons before optional addons are added. You can add core addons to the datagrid by adding these directly to this array, however it is not recommended. |
coreAddons: []
};
|
¶ addonsThe addons module is used to pass injected names directly to the directive and then have them applied to the instance. Each of the addons is expected to be a factory that takes at least one argument which is the instance being passed to it. They can ask for additional ones as well and they will be injected in from angular's injector. |
module.factory("gridAddons", [ "$injector", function($injector) {
function applyAddons(addons, instance) {
var i = 0, len = addons.length, result, addon;
while (i < len) {
result = $injector.get(addons[i]);
if (typeof result === "function" || result instanceof Array) {
|
It is expected that each addon be a function or injector array syntax. inst is the instance that is injected. |
addon = $injector.invoke(result, instance, {
inst: instance
});
} else {
|
they must have returned a null? what was the point. Throw an error. |
throw new Error("Addons expect a function ($injector array supported) to pass the grid instance to.");
}
i += 1;
}
}
return function(instance, addons) {
|
addons can be a single item, array, or comma/space separated string. |
addons = addons instanceof Array ? addons : addons && addons.replace(/,/g, " ").replace(/\s+/g, " ").split(" ") || [];
if (instance.addons) {
addons = instance.addons = instance.addons.concat(addons);
}
applyAddons(addons, instance);
};
} ]);
|
charPack given a character it will repeat it until the amount specified. example: charPack('0', 3) => '000' Params
char
String
amount
Number
|
function charPack(char, amount) {
var str = "";
while (str.length < amount) {
str += char;
}
return str;
}
|
¶ ux.CSSCreate custom style sheets. This has a performance increase over modifying the style on multiple dom elements because you can create the sheet or override it and then all with that classname update by the browser instead of the manual insertion into style attributes per dom node. |
exports.css = function CSS() {
var customStyleSheets = {}, cache = {}, cnst = {
head: "head",
screen: "screen",
string: "string",
object: "object"
};
|
createCustomStyleSheet given a name creates a new styleSheet. Params
name
Strng
|
function createCustomStyleSheet(name) {
if (!getCustomSheet(name)) {
customStyleSheets[name] = createStyleSheet(name);
}
return getCustomSheet(name);
}
|
getCustomSheet get one of the custom created sheets. Params
name
|
function getCustomSheet(name) {
return customStyleSheets[name];
}
|
createStyleSheet does the heavy lifting of creating a style sheet. Params
name
String
|
function createStyleSheet(name) {
if (!document.styleSheets) {
return;
}
if (document.getElementsByTagName(cnst.head).length === 0) {
return;
}
var styleSheet, mediaType, i, media;
if (document.styleSheets.length > 0) {
for (i = 0; i < document.styleSheets.length; i++) {
if (document.styleSheets[i].disabled) {
continue;
}
media = document.styleSheets[i].media;
mediaType = typeof media;
if (mediaType === cnst.string) {
if (media === "" || media.indexOf(cnst.screen) !== -1) {
styleSheet = document.styleSheets[i];
}
} else if (mediaType === cnst.object) {
if (media.mediaText === "" || media.mediaText.indexOf(cnst.screen) !== -1) {
styleSheet = document.styleSheets[i];
}
}
if (typeof styleSheet !== "undefined") {
break;
}
}
}
var styleSheetElement = document.createElement("style");
styleSheetElement.type = "text/css";
styleSheetElement.title = name;
document.getElementsByTagName(cnst.head)[0].appendChild(styleSheetElement);
var index = document.styleSheets.length - 1;
styleSheet = document.styleSheets[index];
return {
name: name,
styleSheet: styleSheet
};
}
function removeStyleSheet(name) {
var sheetData = customStyleSheets[name];
var len = document.styleSheets.length;
for (var i = 0; i < len; i += 1) {
if (document.styleSheets[i] === sheetData.styleSheet) {
document.styleSheets.splice(i, 1);
}
}
delete customStyleSheets[name];
sheetData = null;
}
|
createClass creates a class on a custom style sheet. Params
sheetName
String
selector
String
style
String
|
function createClass(sheetName, selector, style) {
var sheet = getCustomSheet(sheetName) || createCustomStyleSheet(sheetName), styleSheet = sheet.styleSheet, i;
if (styleSheet.addRule) {
for (i = 0; i < styleSheet.rules.length; i++) {
if (styleSheet.rules[i].selectorText && styleSheet.rules[i].selectorText.toLowerCase() === selector.toLowerCase()) {
styleSheet.rules[i].style.cssText = style;
return;
}
}
styleSheet.addRule(selector, style);
if (styleSheet.rules[styleSheet.rules.length - 1].cssText === selector + " { }") {
throw new Error("CSS failed to write");
}
} else if (styleSheet.insertRule) {
for (i = 0; i < styleSheet.cssRules.length; i++) {
if (styleSheet.cssRules[i].selectorText && styleSheet.cssRules[i].selectorText.toLowerCase() === selector.toLowerCase()) {
styleSheet.cssRules[i].style.cssText = style;
return;
}
}
styleSheet.insertRule(selector + "{" + style + "}", 0);
}
}
|
getSelector given a selector this will find that selector in the stylesheets. Not just the custom ones. Params
selector
String
|
function getSelector(selector) {
var i, ilen, sheet, classes, result;
if (selector.indexOf("{") !== -1 || selector.indexOf("}") !== -1) {
return null;
}
if (cache[selector]) {
return cache[selector];
}
for (i = 0, ilen = document.styleSheets.length; i < ilen; i += 1) {
sheet = document.styleSheets[i];
classes = sheet.rules || sheet.cssRules;
result = getRules(classes, selector);
if (result) {
return result;
}
}
return null;
}
|
getRules given a set of classes and a selector it will get the rules for a style sheet. Params
classes
CSSRules
selector
String
|
function getRules(classes, selector) {
var j, jlen, cls, result;
if (classes) {
for (j = 0, jlen = classes.length; j < jlen; j += 1) {
cls = classes[j];
if (cls.cssRules) {
result = getRules(cls.cssRules, selector);
if (result) {
return result;
}
}
if (cls.selectorText) {
var expression = "(\b)*" + selector.replace(".", "\\.") + "([^-a-zA-Z0-9]|,|$)", matches = cls.selectorText.match(expression);
if (matches && matches.indexOf(selector) !== -1) {
cache[selector] = cls.style;
|
cache the value |
return cls.style;
}
}
}
}
return null;
}
|
getCSSValue return the css value of a property given the selector and the property. Params
selector
String
property
String
|
function getCSSValue(selector, property) {
var cls = getSelector(selector);
return cls && cls[property] !== undefined ? cls[property] : null;
}
|
setCSSValue overwrite a css value given a selector, property, and new value. Params
selector
String
property
String
value
String
|
function setCSSValue(selector, property, value) {
var cls = getSelector(selector);
cls[property] = value;
}
|
ux.CSS API |
return {
createdStyleSheets: [],
createStyleSheet: createStyleSheet,
createClass: createClass,
getCSSValue: getCSSValue,
setCSSValue: setCSSValue,
getSelector: getSelector,
removeStyleSheet: removeStyleSheet
};
}();
|
¶ ux.eachLike angular.forEach except that you can pass additional arguments to it that will be available in the iteration function. It is optimized to use while loops where possible instead of for loops for speed. Like Lo-Dash. Params
list
Array\Object
method
Function
data
*=
additional arguments passes are available in the iteration function
example:
|
function each(list, method, data) {
var i = 0, len, result, extraArgs, apl = exports.util.apply;
if (arguments.length > 2) {
extraArgs = exports.util.array.toArray(arguments);
extraArgs.splice(0, 2);
}
if (list && list.length) {
len = list.length;
while (i < len) {
result = apl(method, null, [ list[i], i, list ].concat(extraArgs));
if (result !== undefined) {
return result;
}
i += 1;
}
} else if (list && apl(Object.prototype.hasOwnProperty, list, [ "0" ])) {
while (apl(Object.prototype.hasOwnProperty, list, [ i ])) {
result = apl(method, null, [ list[i], i, list ].concat(extraArgs));
if (result !== undefined) {
return result;
}
i += 1;
}
} else if (!(list instanceof Array)) {
for (i in list) {
if (apl(Object.prototype.hasOwnProperty, list, [ i ])) {
result = apl(method, null, [ list[i], i, list ].concat(extraArgs));
if (result !== undefined) {
return result;
}
}
}
}
return list;
}
exports.each = each;
|
filter built on the same concepts as each. So that you can pass additional arguments. Params
list
method
data
|
function filter(list, method, data) {
var i = 0, len, result = [], extraArgs, response, apl = exports.util.apply;
if (arguments.length > 2) {
extraArgs = exports.util.array.toArray(arguments);
extraArgs.splice(0, 2);
}
if (list && list.length) {
len = list.length;
while (i < len) {
response = apl(method, null, [ list[i], i, list ].concat(extraArgs));
if (response) {
result.push(list[i]);
}
i += 1;
}
} else {
for (i in list) {
if (apl(Object.prototype.hasOwnProperty, list, [ i ])) {
response = apl(method, null, [ list[i], i, list ].concat(extraArgs));
if (response) {
result.push(list[i]);
}
}
}
}
return result;
}
exports.filter = filter;
|
function extend(destination, source) {
var args = exports.util.array.toArray(arguments), i = 1, len = args.length, item, j;
while (i < len) {
item = args[i];
for (j in item) {
if (destination[j] && typeof destination[j] === "object") {
destination[j] = extend(destination[j], item[j]);
} else if (item[j] instanceof Array) {
destination[j] = extend([], item[j]);
} else if (item[j] && typeof item[j] === "object") {
destination[j] = extend({}, item[j]);
} else {
destination[j] = item[j];
}
}
i += 1;
}
return destination;
}
exports.extend = extend;
(function() {
"use strict";
var c = 0;
exports.uid = function UID() {
c += 1;
var str = c.toString(36).toUpperCase();
while (str.length < 6) {
str = "0" + str;
}
return str;
};
})();
exports.logWrapper = function LogWrapper(name, instance, theme, inst) {
var apl = exports.util.apply;
theme = theme || "black";
instance.$logName = name;
instance.log = instance.info = instance.warn = instance.error = function() {};
function dispatchFn(dispatch, args) {
if (typeof dispatch === "function") {
apl(dispatch, instance, args);
}
}
instance.log = function log() {
var args = [ exports.datagrid.events.LOG, name, theme ].concat(exports.util.array.toArray(arguments));
if (inst.logger) {
apl(inst.logger.log, inst.logger, args);
} else {
dispatchFn(inst, args);
}
};
instance.info = function info() {
var args = [ exports.datagrid.events.INFO, name, theme ].concat(exports.util.array.toArray(arguments));
if (inst.logger) {
apl(inst.logger.info, inst.logger, args);
} else {
dispatchFn(inst, args);
}
};
instance.warn = function warn() {
var args = [ exports.datagrid.events.WARN, name, theme ].concat(exports.util.array.toArray(arguments));
if (inst.logger) {
apl(inst.logger.warn, inst.logger, args);
} else {
dispatchFn(inst, args);
}
};
instance.error = function error() {
var args = [ exports.datagrid.events.ERROR, name, theme ].concat(exports.util.array.toArray(arguments));
if (inst.logger) {
apl(inst.logger.error, inst.logger, args);
} else {
dispatchFn(inst, args);
}
};
instance.destroyLogger = function() {
if (inst.logger) {
inst.log("destroy");
inst.logger.destroy();
inst.logger = null;
}
};
return instance;
};
function Flow(inst, dispatch, pauseFn, $timeout) {
var running = false, current = null, list = [], history = [], historyLimit = 10, uniqueMethods = {}, execStartTime, execEndTime, timeouts = {}, nextPromise, consoleMethodStyle = "color:#666666;";
function getMethodName(method) {
|
|
TODO: there might be a faster way to get the function name. |
return method.toString().split(/\b/)[2];
}
function createItem(method, args, delay) {
return {
label: getMethodName(method),
method: method,
args: args || [],
delay: delay
};
}
function unique(method) {
var name = getMethodName(method);
uniqueMethods[name] = method;
}
function clearSimilarItemsFromList(item) {
var i = 0, len = list.length;
while (i < len) {
if (list[i].label === item.label) {
if (list[i] === current && nextPromise) {
$timeout.cancel(nextPromise);
nextPromise = null;
current = null;
inst.warn("REMOVE ACTIVE FLOW ITEM %c%s", consoleMethodStyle, item.label);
} else {
inst.info("remove Flow item %c%s", consoleMethodStyle, item.label);
}
list.splice(i, 1);
i -= 1;
len -= 1;
}
i += 1;
}
if (!current) {
|
it was cleared. So we now call next. |
next();
}
}
function add(method, args, delay) {
var item = createItem(method, args, delay), index = -1;
if (uniqueMethods[item.label]) {
clearSimilarItemsFromList(item);
}
list.push(item);
if (running) {
next();
}
}
|
this puts it right after the one currently running. |
function insert(method, args, delay) {
list.splice(1, 0, createItem(method, args, delay));
}
function remove(method) {
clearSimilarItemsFromList({
label: getMethodName(method)
});
}
|
timeouts that do not block the flow. |
function timeout(method, time) {
var intv, item = createItem(method, []), startTime = Date.now(), timeoutCall = function() {
inst.log("exec timeout method %c%s %sms (len:%s)", consoleMethodStyle, item.label, Date.now() - startTime, list.length);
list.push(item);
|
add after timeout time. |
if (running) {
next();
}
};
inst.log("wait for timeout method %c%s (len:%s)", consoleMethodStyle, item.label, list.length);
intv = setTimeout(timeoutCall, time);
|
use regular timeout because we are just waiting to put it in the queue. |
timeouts[intv] = function() {
clearTimeout(intv);
delete timeouts[intv];
};
return intv;
}
function stopTimeout(intv) {
if (timeouts[intv]) timeouts[intv]();
}
function getArguments(fn) {
var str = fn.toString(), match = str.match(/\(.*\)/);
return match[0].match(/([\$\w])+/gm);
}
function hasDoneArg(fn) {
var args = getArguments(fn);
return !!(args && args.indexOf("done") !== -1);
}
function done() {
execEndTime = Date.now();
inst.log("finish %c%s took %dms (len:%s)", consoleMethodStyle, current.label, execEndTime - execStartTime, list.length);
current = null;
addToHistory(list.shift());
next();
return execEndTime - execStartTime;
}
|
Keep a history of what methods were executed for debugging. Keep up to the limit. |
function addToHistory(item) {
history.unshift(item);
while (history.length > historyLimit) {
history.pop();
}
}
function next() {
inst.log("next %s", list.length);
if (!current && list.length) {
current = list[0];
if (inst.async && current.delay !== undefined) {
inst.log(" delay for %c%s %sms (len:%s)", consoleMethodStyle, current.label, current.delay, list.length);
nextPromise = $timeout(exec, current.delay, false);
} else {
exec();
}
}
}
function exec() {
if (!inst) {
return;
}
if (nextPromise) {
$timeout.cancel(nextPromise);
}
if (pauseFn && pauseFn()) {
inst.warn(" wait for pauseFn");
nextPromise = $timeout(exec, 0, false);
return;
}
var methodHasDoneArg = hasDoneArg(current.method);
inst.log("start method %c%s (len:%s)" + (methodHasDoneArg && " - (has done arg)" || ""), consoleMethodStyle, current.label, list.length);
if (methodHasDoneArg) {
current.args.push(done);
}
try {
execStartTime = Date.now();
exports.util.apply(current.method, null, current.args);
} catch (e) {
inst.warn(e.message + "\n" + (e.stack || e.stacktrace || e.backtrace));
} finally {
if (!methodHasDoneArg) {
done();
}
}
}
function run() {
running = true;
next();
}
function clear() {
var len = current ? 1 : 0, item;
inst.info("clear");
while (list.length > len) {
item = list.splice(len, 1)[0];
inst.log(" remove %s from flow", item.label);
}
}
function length() {
return list.length;
}
function count(name) {
var c = 0;
for (var i = 0; i < list.length; i += 1) {
if (list[i].label === name) {
c += 1;
}
}
return c;
}
function destroy() {
list.length = 0;
inst = null;
}
exports.logWrapper("Flow", inst, "grey", inst);
|
inst.async = exports.util.apply(Object.prototype.hasOwnProperty, inst, ['async']) ? inst.async : true; |
inst.debug = exports.util.apply(Object.prototype.hasOwnProperty, inst, [ "debug" ]) ? inst.debug : 0;
inst.insert = insert;
inst.add = add;
inst.unique = unique;
inst.remove = remove;
inst.timeout = timeout;
inst.stopTimeout = stopTimeout;
inst.run = run;
inst.clear = clear;
inst.length = length;
inst.count = count;
inst.destroy = destroy;
return inst;
}
exports.datagrid.Flow = Flow;
/*global each, charPack, Flow, exports, module */
|
¶ Datagrid DirectiveThe datagrid manages the Params
scope
Scope
element
HTMLElement
attr
Object
$compile
Function
|
function Datagrid(scope, element, attr, $compile, $timeout) {
|
flow flow management for methods of the datagrid. Keeping functions firing in the correct order especially if async methods are executed. |
var flow;
|
waitCount waiting to render. If it fails too many times it will die. |
var waitCount = 0;
|
changeWatcherSet flag for change watchers. |
var changeWatcherSet = false;
|
unwatchers list of scope listeners that we want to clear on destroy |
var unwatchers = [];
|
content the DOM element with all of the chunks. |
var content;
|
oldContent the temporary content when the grid is being reset. |
var oldContent;
|
scopes the array of all scopes that have been compiled. |
var scopes = [];
|
active the scopes that are currently active. |
var active = [];
|
lastVisibleScrollStart cached index to improve render loop by starting where it left off. |
var lastVisibleScrollStart = 0;
|
rowOffsets cache for the heights of the rows for faster height calculations. |
var rowOffsets = {};
|
viewHeight the visual area height. |
var viewHeight = 0;
|
options configs that are shared through the datagrid and addons. |
var options;
|
states local reference to the states constants |
var states = exports.datagrid.states;
|
events local reference to the events constants |
var events = exports.datagrid.events;
|
state |
var state = states.BUILDING;
|
values |
var values = {
|
dirty: false,
|
|
scroll: 0,
|
|
speed: 0,
|
|
absSpeed: 0,
|
|
scrollPercent: 0,
|
|
touchDown: false,
|
|
scrollingStopIntv: null,
|
|
activeRange: {
min: 0,
max: 0
}
};
|
|
var logEvents = [ exports.datagrid.events.LOG, exports.datagrid.events.INFO, exports.datagrid.events.WARN, exports.datagrid.events.ERROR ];
|
|
the instance of the datagrid that will be referenced by all addons. |
var inst = this, eventLogger = {}, startupComplete = false, gcIntv, $compileCache = {};
|
wrap the instance for logging. |
exports.logWrapper("datagrid event", inst, "grey", inst);
|
for debugging and watching the angular phase start and end. cannot use for flowPauseFn it causes lots of errors because datagrid will not flow at all during a phase with this setting a flag to use. function beforePhase() { inst.info("NG-$digest START"); $timeout(afterPhase, 0, false); } function afterPhase() { if (inst) {// it may be destroyed after a phase. so only log if it is there. inst.info("NG-$digest END"); } } scope.$watch(beforePhase); |
function init() {
flow.unique(reset);
flow.unique(render);
flow.unique(updateRowWatchers);
}
|
Build out the public API variables for the datagrid. |
function setupExports() {
inst.uid = exports.uid();
inst.name = scope.$eval(attr.gridName) || "datagrid";
inst.scope = scope;
inst.element = element;
inst.attr = attr;
inst.rowsLength = 0;
inst.scopes = scopes;
inst.data = inst.data || [];
inst.unwatchers = unwatchers;
inst.values = values;
inst.start = start;
inst.update = update;
inst.reset = reset;
inst.isReady = isReady;
inst.isStartupComplete = isStartupComplete;
inst.forceRenderScope = forceRenderScope;
inst.dispatch = dispatch;
inst.activateScope = activateScope;
inst.deactivateScope = deactivateScope;
inst.updateLinks = updateLinks;
inst.render = function() {
flow.add(render);
};
inst.updateHeights = updateHeights;
inst.getOffsetIndex = getOffsetIndex;
inst.isActive = isActive;
inst.isCompiled = isCompiled;
inst.swapItem = swapItem;
inst.moveItem = moveItem;
inst.getScope = getScope;
inst.getRowItem = getRowItem;
inst.getRowElm = getRowElm;
inst.getExistingRow = getExistingRow;
inst.getRowIndex = inst.getIndexOf = getRowIndex;
inst.getRowOffset = getRowOffset;
inst.getRowHeight = getRowHeight;
inst.getViewportHeight = getViewportHeight;
inst.getContentHeight = getContentHeight;
inst.getContent = getContent;
inst.isDigesting = isDigesting;
inst.safeDigest = safeDigest;
inst.getRowIndexFromElement = getRowIndexFromElement;
inst.updateViewportHeight = updateViewportHeight;
inst.calculateViewportHeight = calculateViewportHeight;
inst.options = options = exports.extend({}, exports.datagrid.options, scope.$eval(attr.options) || {});
inst.flow = flow = new Flow({
async: exports.util.apply(Object.prototype.hasOwnProperty, options, [ "async" ]) ? !!options.async : true,
debug: exports.util.apply(Object.prototype.hasOwnProperty, options, [ "debug" ]) ? options.debug : 0
}, inst.dispatch, isDigesting, $timeout);
|
this needs to be set immediately so that it will be available to other views. |
inst.grouped = scope.$eval(attr.grouped);
inst.gc = forceGarbageCollection;
inst.throwError = exports.datagrid.throwError;
flow.add(init);
|
initialize core. |
flow.run();
}
|
The content DOM element is the only direct child created by the datagrid.
It is used to append all of the |
function createContent() {
var contents = element[0].getElementsByClassName(options.contentClass), cnt, classes = options.contentClass;
contents = exports.filter(contents, filterOldContent);
cnt = contents[0];
if (cnt) {
|
if there is an old one. Pull the classes from it. |
classes = cnt.className || options.contentClass;
}
if (!cnt) {
classes = getClassesFromOldContent() || classes;
cnt = angular.element('<div class="' + classes + '"></div>');
if (inst.options.chunks.detachDom) {
cnt[0].style.position = "relative";
}
element.prepend(cnt);
}
if (!cnt[0]) {
cnt = angular.element(cnt);
}
return cnt;
}
|
If the old content exists it may have been an original DOM element passed to the datagrid. If so we want to keep that DOM element's classes in tact. |
function getClassesFromOldContent() {
var classes, index;
if (oldContent) {
|
let's get classes from it. |
classes = exports.util.array.toArray(oldContent[0].classList);
index = classes.indexOf("old-" + options.contentClass);
if (index !== -1) {
classes.splice(index, 1);
}
return classes.join(" ");
}
}
|
filter the list of content DOM to remove any references to the [oldContent][#oldContent]. Params
cnt
index
list
|
function filterOldContent(cnt, index, list) {
return angular.element(cnt).hasClass("old-" + options.contentClass) ? false : true;
}
|
return the reference to the content div. |
function getContent() {
return content;
}
|
function start() {
inst.dispatch(exports.datagrid.events.ON_INIT, inst);
content = createContent();
waitForElementReady(0);
}
|
|
this waits for the body element because if the grid has been constructed, but no heights are showing it is usually because the grid has not been attached to the document yet. So wait for the heights to be available, but only wait a little then exit. Params
count
|
function waitForElementReady(count) {
if (!inst.element[0].offsetHeight) {
if (count < 1) {
|
if they are doing custom compiling. They may compile before adding it to the DOM. allow a pass to happen just in case. |
flow.add(waitForElementReady, [ count + 1 ], 0);
|
retry. |
return;
} else {
flow.warn("Datagrid: DOM Element does not have a height.");
}
}
if (options.templateModel && options.templateModel.templates) {
flow.add(inst.templateModel.createTemplatesFromData, [ options.templateModel.templates ], 0);
}
flow.add(inst.templateModel.createTemplates, null, 0);
|
allow element to be added to DOM. if the templates have different heights then they are dynamic. |
flow.add(function updateDynamicRowHeights() {
options.dynamicRowHeights = inst.templateModel.dynamicHeights();
});
flow.add(addListeners);
}
|
Adds listeners. Notice that all listeners are added to the unwatchers array so that they can be cleared before references are removed to avoid memory leaks with circular references and to prevent events from being listened to while the destroy is happening. |
function addListeners() {
var unwatchFirstRender = scope.$on(exports.datagrid.events.ON_BEFORE_RENDER_AFTER_DATA_CHANGE, function() {
unwatchFirstRender();
flow.add(onStartupComplete);
});
window.addEventListener("resize", onResize);
unwatchers.push(scope.$on(exports.datagrid.events.UPDATE, update));
unwatchers.push(scope.$on(exports.datagrid.events.ON_ROW_TEMPLATE_CHANGE, onRowTemplateChange));
unwatchers.push(scope.$on("$destroy", destroy));
flow.add(setupChangeWatcher, [], 0);
inst.dispatch(exports.datagrid.events.ON_LISTENERS_READY);
}
function isStartupComplete() {
return startupComplete;
}
function onStartupComplete() {
startupComplete = true;
dispatch(exports.datagrid.events.ON_STARTUP_COMPLETE, inst);
}
|
When a change happens update the DOM. |
function setupChangeWatcher() {
if (!changeWatcherSet) {
inst.log("setupChangeWatcher");
changeWatcherSet = true;
unwatchers.push(scope.$watchCollection(attr.uxDatagrid, onDataChangeFromWatcher));
|
force initial watcher. |
var d = scope.$eval(attr.uxDatagrid);
if (d && d.length) {
flow.add(render);
}
}
}
|
function onDataChangeFromWatcher(newValue, oldValue, scope) {
flow.add(onDataChanged, [ newValue, oldValue ]);
}
|
|
This function can be used to force update the viewHeight. |
function updateViewportHeight() {
viewHeight = inst.calculateViewportHeight();
if (!viewHeight) {
viewHeight = options.minHeight;
}
}
|
return if grid state is states.READY. |
function isReady() {
return state === states.READY;
}
|
Calculate Viewport Height can be expensive. Depending on the number of DOM elements. so if you need to use this method, use it sparingly because you may experience performance issues if overused. |
function calculateViewportHeight() {
return element[0].offsetHeight;
}
|
function onResize(event) {
forceRedraw();
}
|
|
swap out an old item with a new item without causing a data change. Quick swap of items. this will only work if the item already exists in the datagrid. You cannot add or remove items this way. Only change them to a different reference. Adding or Removing requires a re-chunking. Params
oldItem
Object
newItem
Object
keepTemplate
Boolean=
|
function swapItem(oldItem, newItem, keepTemplate) {
|
TODO: needs unit test. |
var index = getRowIndex(oldItem), oldTpl, newTpl;
if (exports.util.apply(Object.prototype.hasOwnProperty, inst.data, [ index ])) {
oldTpl = inst.templateModel.getTemplate(oldItem);
if (keepTemplate) {
newTpl = oldTpl;
} else {
newTpl = inst.templateModel.getTemplate(newItem);
}
inst.normalizeModel.replace(newItem, index);
if (oldTpl !== newTpl) {
inst.templateModel.setTemplate(index, newTpl);
} else {
|
nothing changed except the reference. So just update the scope and digest. |
scopes[index][newTpl.item] = newItem;
safeDigest(scopes[index]);
}
}
}
function moveItem(fromIndex, toIndex) {
inst.normalizeModel.move(fromIndex, toIndex);
changeData(inst.getOriginalData(), inst.getOriginalData());
}
|
function getScope(index) {
return scopes[index];
}
|
|
function getRowItem(index) {
return this.getData()[index];
}
|
|
function getRowElm(index) {
return angular.element(inst.chunkModel.getRow(index));
}
|
|
Return the DOM element at that row index. This will not build it if it doesn't exist. Params
index
Number
|
function getExistingRow(index) {
return angular.element(inst.chunkModel.getExistingRow(index));
}
|
function isCompiled(index) {
return !!scopes[index];
}
|
|
function getRowIndex(item) {
return inst.getNormalizedIndex(item, 0);
}
|
|
Get the index of a row from a reference to a DOM element that is contained within a row. Params
el
JQLite
DOMElement
|
function getRowIndexFromElement(el) {
if (el && element[0].contains(el[0] || el)) {
el = el.scope ? el : angular.element(el);
var s = el.scope();
if (s === inst.scope) {
inst.throwError("Unable to get row scope... something went wrong.");
}
|
make sure we get the right scope to grab the index from. We need to get it from a row. |
while (s && s.$parent && s.$parent !== inst.scope) {
s = s.$parent;
}
return s.$index;
}
return -1;
}
|
Return the scroll offset of a row by its index. All offsets are cached. They get updated if a row template changes, because it may change the height as well. Params
index
Number
|
function getRowOffset(index) {
if (rowOffsets[index] === undefined) {
if (options.dynamicRowHeights) {
|
dynamicRowHeights should be set by the templates. |
updateHeightValues();
} else {
rowOffsets[index] = index * options.rowHeight;
}
}
return rowOffsets[index];
}
|
function getRowHeight(index) {
return inst.templateModel.getRowHeight(index);
}
|
|
Return the height of the viewable area of the datagrid. |
function getViewportHeight() {
return viewHeight;
}
|
Return the total height of the content of the datagrid. |
function getContentHeight() {
var list = inst.chunkModel.getChunkList();
return list && list.height || 0;
}
|
function createDom(list) {
|
|
TODO: if there is any dom. It needs destroyed first. |
inst.log("OVERWRITE DOM!!!");
var len = list.length;
|
this async is important because it allows the updateRowWatchers on first digest to escape the current digest. |
inst.chunkModel.chunkDom(list, options.chunks.size, '<div class="' + options.chunks.chunkClass + '">', "</div>", content);
inst.rowsLength = len;
inst.log("created %s dom elements", len);
}
function link(index, s) {
s = s || getScope(index);
var prev = getScope(index - 1), next = getScope(index + 1);
if (prev) {
prev.$$nextSibling = s;
}
s.$$prevSibling = prev;
s.$$nextSibling = next;
if (s.$$nextSibling) {
s.$$nextSibling.$$prevSibling = s;
}
scopes[index] = s;
}
|
Compile a row at that index. This creates the scope for that row when compiled. It does not perform a digest. Params
index
Number
el
Object=
|
function compileRow(index, el) {
var s = scopes[index], tplName, tpl, $c;
if (s && !s.$parent) {
s.$parent = scope;
}
if (!s) {
|
fixes a bug expanding the last row and trying to scroll to it. |
if (!scope.$$childTail && scope.$$childHead && scopes[index - 1]) {
scope.$$childTail = scopes[index - 1];
}
s = scope.$new();
tplName = inst.templateModel.getTemplateName(inst.data[index]);
tpl = inst.templateModel.getTemplate(inst.data[index]);
link(index, s);
s.$status = options.compiledClass;
s[tpl.item] = inst.data[index];
|
set the data to the scope. |
s.$index = index;
scopes[index] = s;
el = el || getRowElm(index);
el.removeClass(options.uncompiledClass);
|
by keeping the $compile(el) cached this seems to be faster than $compile(el)(s) every time. |
$c = $compileCache[tplName] || ($compileCache[tplName] = $compile(el));
|
since compile is cached we now use the clone method to replace our dom element with the cloned one. |
$c(s, function(clone) {
var indexes = inst.chunkModel.getRowIndexes(index);
|
gets the nested indexes for the row |
indexes.pop();
|
pop off the index for the row, we want it's parent. |
var parent = inst.chunkModel.getItemByIndexes(indexes).dom;
|
get the parent by indexes. |
var attrs = el[0].attributes, len = attrs.length;
|
we need to copy over the row-id and any other custom properties on this row. use for loop instead of each to avoid closure function overhead. Needs to be as fast as possible. |
for (var i = 0; i < len; i += 1) {
var attr = attrs[i];
|
copy the attr from el to clone |
if (clone.attr(attr.name) !== attr.value) {
clone.attr(attr.name, attr.value);
}
}
parent.replaceChild(clone[0], el[0]);
});
if (inst.templateModel.hasVariableRowHeights()) {
inst.chunkModel.updateAllChunkHeights(index);
}
inst.dispatch(exports.datagrid.events.ON_ROW_COMPILE, s, el);
deactivateScope(s, index);
}
return s;
}
|
function buildRows(list, forceRender) {
inst.log(" buildRows %s", list.length);
state = states.BUILDING;
createDom(list);
flow.add(updateHeightValues, 0);
if (!isReady()) {
flow.add(ready);
}
if (forceRender) {
flow.add(render);
}
}
|
|
Set the state to states.READY and start the first render. |
function ready() {
inst.log(" ready");
state = states.READY;
flow.add(fireReadyEvent);
flow.add(safeDigest, [ scope ]);
}
|
Fire the events.ON_READY |
function fireReadyEvent() {
scope.$emit(exports.datagrid.events.ON_READY);
}
function isDigesting(s) {
|
return !!(s && (s.$$phase || s.$root.$$phase)); this must be checked this way. Otherwise isolated scopes can cause the value to be missleading. |
var ds = s || scope;
while (ds) {
if (ds.$$phase) {
return true;
}
ds = ds.$parent;
}
return false;
}
|
SafeDigest by checking the render phase of the scope before rendering. while this is not recommended by angular it is effective. Params
s
Scope
|
function safeDigest(s) {
|
|
if (!isDigesting(s)) {
s.$digest();
return true;
}
return false;
}
|
Take all of the counts that we have and move up the parent chain subtracting them from the totals so that event listeners do not get stuck on broadcast. Params
s
Scope
listenerCounts
Object
fn
Function
|
function applyEventCounts(s, listenerCounts, fn) {
|
TODO: angular 1.3+ is doing counts differently. Some counts are getting removed. |
while (s) {
for (var eventName in listenerCounts) {
if (exports.util.apply(Object.prototype.hasOwnProperty, listenerCounts, [ eventName ])) {
fn(s, listenerCounts, eventName);
}
}
s = s.$parent;
}
}
|
function addEvents(s, listenerCounts) {
applyEventCounts(s, listenerCounts, addEvent);
}
|
|
function addEvent(s, listenerCounts, eventName) {
|
|
console.log("%c%s.$$listenerCount[%s] %s + %s = %s", "color:#009900", s.$id, eventName, s.$$listenerCount[eventName], listenerCounts[eventName], s.$$listenerCount[eventName] + listenerCounts[eventName]); |
s.$$listenerCount[eventName] += listenerCounts[eventName];
}
|
Take all of the counts that we have and move up the parent chain subtracting them from the totals so that event listeners do not get stuck on broadcast. Params
s
Scope
listenerCounts
Object
|
function subtractEvents(s, listenerCounts) {
applyEventCounts(s, listenerCounts, subtractEvent);
}
|
Take the count of events away from the $$listenerCount Params
s
Scope
listenerCounts
Object
eventName
String
|
function subtractEvent(s, listenerCounts, eventName) {
s.$$listenerCount[eventName] -= listenerCounts[eventName];
}
|
One of the core features to the datagrid's performance is the ability to make only the scopes that are in view to render. This deactivates a scope by removing its $$watchers that angular uses to know that it needs to digest. Thus inactivating the row. We also remove all watchers from child scopes recursively storing them on each child in a separate variable to activate later. They need to be reactivated before being destroyed for proper cleanup. $$childHead and $$nextSibling variables are also updated for angular so that it will not even iterate over a scope that is deactivated. It becomes completely hidden from the digest. Params
s
Scope
index
number
|
function deactivateScope(s, index) {
|
if the scope is not created yet. just skip. |
if (s && !isActive(s)) {
|
do not deactivate one that is already deactivated. |
s.$emit(exports.datagrid.events.ON_BEFORE_ROW_DEACTIVATE);
s.$$$watchers = s.$$watchers;
s.$$watchers = [];
s.$$$listenerCount = angular.extend({}, s.$$listenerCount);
subtractEvents(s, s.$$listenerCount);
if (index >= 0) {
s.$parent = null;
s.$$nextSibling = null;
s.$$prevSibling = null;
}
return true;
}
return false;
}
|
Taking a scope that is deactivated the watchers that it did have are now stored on $$$watchers and can be put back to $$watchers so angular will pick up this scope on a digest. This is done recursively though child scopes as well to activate them. It also updates the linking $$childHead and $$nextSiblings to fully make sure the scope is as if it was before it was deactivated. Params
s
Scope
index
number
|
function activateScope(s, index) {
if (s && s.$$$watchers) {
|
do not activate one that is already active. |
s.$parent = s.$$parent;
s.$$watchers = s.$$$watchers;
delete s.$$$watchers;
addEvents(s, s.$$$listenerCount);
delete s.$$$listenerCount;
if (index >= 0) {
s.$$nextSibling = scopes[index + 1];
s.$$prevSibling = scopes[index - 1];
}
s.$parent = scope;
s.$emit(exports.datagrid.events.ON_AFTER_ROW_ACTIVATE);
return true;
}
return !!(s && !s.$$$watchers);
}
|
function isActive(index) {
var s = scopes[index];
return !!(s && !s.$$$watchers);
}
|
|
Given a scroll offset, get the index that is closest to that scroll offset value. Params
offset
Number
|
function getOffsetIndex(offset) {
|
updateHeightValues must be called before this. |
var est = Math.floor(offset / inst.templateModel.averageTemplateHeight()), i = 0, len = inst.rowsLength;
if (!offset || inst.rowsLength < 2) {
return i;
}
if (rowOffsets[est] && rowOffsets[est] <= offset) {
i = est;
}
while (i < len) {
if (rowOffsets[i] <= offset && rowOffsets[i + 1] > offset) {
return i;
}
i += 1;
}
return i;
}
|
Because the datagrid can render as many as 50k rows it becomes necessary to optimize loops by determining the index to start checking for deactivated and activated scopes at instead of iterating all of the items. This greatly improves a render because it only iterates from where the last render was. It does this by taking the last first active element and then counting from there till we get to the top of the start area. So we never have to loop the whole thing. |
function getStartingIndex() {
if (values.dirty && inst.chunkModel.getChunkList() && inst.chunkModel.getChunkList().height - inst.getViewportHeight() < values.scroll) {
|
We are trying to start the scroll off at a height that is taller than we have in the list. reset scroll to 0. |
inst.info("Scroll reset because either there is no data or the scroll is taller than there is scroll area");
values.scroll = 0;
}
var height = viewHeight, scroll = values.scroll >= 0 ? values.scroll : 0, result = {
startIndex: 0,
i: 0,
inc: 1,
end: inst.rowsLength,
visibleScrollStart: scroll + options.cushion,
visibleScrollEnd: scroll + height - options.cushion
};
result.startIndex = result.i = inst.getOffsetIndex(scroll);
if (inst.rowsLength && result.startIndex === result.end) {
result.startIndex = result.i = result.end - 1;
|
always select at least one row. |
inst.log(exports.errors.E1002);
}
return result;
}
|
invalidate and update all height values of the chunks and rows. |
function updateHeightValues() {
|
TODO: this is going to be updated to use ChunkArray data to be faster. |
var height = 0, i = 0, contentHeight;
while (i < inst.rowsLength) {
rowOffsets[i] = height;
height += inst.getRowHeight(i);
i += 1;
}
options.rowHeight = inst.rowsLength ? inst.templateModel.getTemplateHeight("default") : 0;
contentHeight = getContentHeight();
inst.getContent()[0].style.height = contentHeight + "px";
inst.log("heights: viewport %s content %s", inst.getViewportHeight(), contentHeight);
}
|
This is the core of the datagird rendering. It determines the range of scopes to be activated and deactivates any scopes that were active before that are not still active. |
function updateRowWatchers() {
var loop = getStartingIndex(), offset, lastActive = [].concat(active), lastActiveIndex, s, prevS, digestLater = false;
if (loop.i < 0) {
|
then scroll is negative. ignore it. |
return;
}
inst.dispatch(events.ON_BEFORE_UPDATE_WATCHERS, loop);
|
we only want to update stuff if we are scrolling slow. |
resetMinMax();
|
this needs to always be set after the dispatch of before update watchers in case they need the before activeRange. |
active.length = 0;
|
make sure not to reset until after getStartingIndex. |
inst.log(" scroll %s visibleScrollStart %s visibleScrollEnd %s", values.scroll, loop.visibleScrollStart, loop.visibleScrollEnd);
while (loop.i < inst.rowsLength) {
prevS = scope.$$childHead ? scopes[loop.i - 1] : null;
offset = inst.getRowOffset(loop.i);
|
this is where the chunks and rows get created is when they are requested if they don't exist. we only want to render what is visible. however, we always want to render at least one row if possible. So if we can tell that there is one row available then render that otherwise it will not enter here if there are no rows to render and will throw an error because of an invalid range. |
if (offset >= loop.visibleScrollStart && offset <= loop.visibleScrollEnd || loop.i === loop.startIndex && loop.i < loop.end) {
s = compileRow(loop.i);
|
only compiles if it is not already compiled. Still returns the scope. |
if (loop.started === undefined) {
loop.started = loop.i;
}
updateMinMax(loop.i);
if (activateScope(s, loop.i)) {
inst.getRowElm(loop.i).attr("status", "active");
lastActiveIndex = lastActive.indexOf(loop.i);
if (lastActiveIndex !== -1) {
lastActive.splice(lastActiveIndex, 1);
}
|
make sure to put them into active in the right order. |
active.push(loop.i);
if (!safeDigest(s, true)) {
digestLater = true;
}
s.$digested = true;
}
}
loop.i += loop.inc;
|
optimize the loop |
if (loop.inc > 0 && offset > loop.visibleScrollEnd || loop.inc < 0 && offset < loop.visibleScrollStart) {
break;
}
}
loop.ended = loop.i - 1;
if (inst.rowsLength && values.activeRange.min < 0 && values.activeRange.max < 0) {
inst.throwError(exports.errors.E1002);
}
inst.log(" startIndex %s endIndex %s", loop.startIndex, loop.i);
deactivateList(lastActive);
lastVisibleScrollStart = loop.visibleScrollStart;
inst.log(" activated %s", active.join(", "));
updateLinks();
|
update the $$childHead and $$nextSibling values to keep digest loops at a minimum count. this dispatch needs to be after the digest so that it doesn't cause {} to show up in the render. the creep render cannot be synchronous. It needs to wait till done to render. |
if (inst.templateModel.hasVariableRowHeights()) {
updateHeightValues();
}
flow.add(onAfterUpdateWatchers, [ loop ], 0);
if (digestLater) {
flow.add(function() {
safeDigest(scope);
});
}
}
function onAfterUpdateWatchers(loop) {
inst.dispatch(events.ON_AFTER_UPDATE_WATCHERS, loop);
}
|
Deactivate a list of scopes. |
function deactivateList(lastActive) {
var lastActiveIndex, deactivated = [];
while (lastActive.length) {
lastActiveIndex = lastActive.pop();
deactivated.push(lastActiveIndex);
deactivateScope(scopes[lastActiveIndex], lastActiveIndex);
inst.getRowElm(lastActiveIndex).attr("status", "inactive");
}
inst.log(" deactivated %s", deactivated.join(", "));
}
|
Updates the $$childHead, $$childTail, $$nextSibling, and $$prevSibling values from the parent scope to completely hide scopes that are deactivated from angular's knowledge so digest loops are as small as possible. |
function updateLinks() {
if (active.length) {
var lastIndex = active[active.length - 1], i = 0, len = active.length, s;
scope.$$childHead = scopes[active[0]];
scope.$$childTail = scopes[lastIndex];
while (i < len) {
s = scopes[active[i]];
s.$$prevSibling = scopes[active[i - 1]];
s.$$nextSibling = scopes[active[i + 1]];
s.$parent = scope;
i += 1;
}
}
}
|
resets the min and max of the activeRange for what is activated. |
function resetMinMax() {
values.activeRange.min = values.activeRange.max = -1;
}
|
takes an index that has just been activated and updates the min and max |
function updateMinMax(activeIndex) {
|
values for later calculations to know the range. |
values.activeRange.min = values.activeRange.min < activeIndex && values.activeRange.min >= 0 ? values.activeRange.min : activeIndex;
values.activeRange.max = values.activeRange.max > activeIndex && values.activeRange.max >= 0 ? values.activeRange.max : activeIndex;
}
|
fired just before the update watchers are applied if the data has changed. |
function beforeRenderAfterDataChange() {
if (values.dirty) {
dispatch(exports.datagrid.events.ON_BEFORE_RENDER_AFTER_DATA_CHANGE);
}
}
|
after the data has changed this is fired after the following render. |
function afterRenderAfterDataChange() {
var tplHeight, oldHeight;
if (values.dirty && values.activeRange.max >= 0) {
values.dirty = false;
tplHeight = inst.templateModel.calculateRowHeight(getRowElm(values.activeRange.min)[0]);
if (flow.async && inst.getData().length && tplHeight !== (oldHeight = inst.templateModel.getRowHeight(values.activeRange.min))) {
if (window.console && console.warn) {
console.warn("Template height change from " + oldHeight + " to " + tplHeight + ". This can cause gaps in the datagrid.");
}
inst.templateModel.updateTemplateHeights();
}
dispatch(exports.datagrid.events.ON_RENDER_AFTER_DATA_CHANGE);
}
}
function whenReadyToRender() {
flow.add(inst.updateViewportHeight, null, waitCount);
|
have it wait a moment for the height to change. |
flow.add(render);
}
|
the datagrid requires a height to be able to render. If the datagrid is compiled and not added to the DOM it will not have a height until added to the DOM. If this fails it will wait until the next frame to check the height. If that fails it exits. |
function readyToRender() {
updateViewportHeight();
if (!viewHeight) {
waitCount += 1;
if (waitCount < inst.options.readyToRenderRetryMax) {
inst.info("datagrid is waiting for element to have a height.");
whenReadyToRender();
} else {
flow.warn("Datagrid: Unable to determine a height for the datagrid. Cannot render. Exiting.");
}
return false;
}
if (waitCount) {
inst.info("datagrid has height of %s.", viewHeight);
}
waitCount = 0;
return true;
}
|
depending on the state of the datagrid this will create necessary DOM, compile rows, or digest activeRange of rows. |
function render() {
inst.info("render");
if (readyToRender()) {
waitCount = 0;
inst.log(" render %s", state);
|
Where states.BUILDING is used |
if (state === states.BUILDING) {
buildRows(inst.data);
} else if (state === states.READY) {
inst.dispatch(exports.datagrid.events.ON_BEFORE_RENDER);
flow.add(beforeRenderAfterDataChange);
flow.add(updateRowWatchers);
|
if we do not wait here row heights show too tall because the rows are evaluated at their height before being digetsted. |
flow.add(afterRenderAfterDataChange, [], 0);
|
|
flow.add(inst.dispatch, [ exports.datagrid.events.ON_AFTER_RENDER ]);
} else {
inst.throwError(exports.errors.E1001);
}
} else {
inst.log(" not ready to render.");
}
}
|
function update() {
if (inst) {
inst.warn("force update");
}
if (!onDataChanged(scope.$eval(attr.uxDatagrid), inst.data)) {
forceRedraw();
}
}
|
|
force the datagrid to fire a data change update. |
function forceRedraw() {
|
we need to wait a moment for the browser to finish the resize, then adjust and fire the event. |
flow.add(updateHeights, [], 100);
}
|
Compare the new and the old value. If the item number is the same, and no templates have changed then just update the scopes and run the watchers instead of doing a reset. Params
newVal
Array
oldVal
Array
|
function dirtyCheckData(newVal, oldVal) {
|
TODO: this needs unit tested. |
if (newVal && oldVal && newVal.length === oldVal.length) {
var i = 0, len = newVal.length;
while (i < len) {
if (dirtyCheckItemTemplate(newVal[i], oldVal[i])) {
return true;
}
i += 1;
}
if (inst.data.length !== inst.normalize(newVal, inst.grouped).length) {
inst.log(" dirtyCheckData length is different");
return true;
}
return false;
}
return true;
}
|
check to see if the template for the item has changed Params
newItem
*
oldItem
*
|
function dirtyCheckItemTemplate(newItem, oldItem) {
if (inst.templateModel.getTemplate(newItem) !== inst.templateModel.getTemplate(oldItem)) {
inst.log(" dirtyCheckData row template changed");
return true;
}
return false;
}
|
function mapData(newVal, oldVal) {
|
|
TODO: there is some error here that is causing the rows now not to compile. |
inst.log(" mapData()");
var oldTemplates = [];
|
get temp cache for templates |
exports.each(inst.getData(), cacheOldTemplates, oldTemplates);
inst.data = inst.setData(newVal, inst.grouped) || [];
inst.chunkModel.updateList(inst.data);
exports.each(inst.getData(), updateScope, oldTemplates);
oldTemplates = null;
|
clear temp cache for templates. |
dispatch(exports.datagrid.events.ON_AFTER_DATA_CHANGE, inst.data, oldVal);
}
|
Params
item
index
list
cache
|
function cacheOldTemplates(item, index, list, cache) {
cache[index] = inst.templateModel.getTemplate(item);
}
|
update the scope at that index with the new item. |
function updateScope(item, index, list, oldTemplates) {
var tpl, oldTemplate;
if (scopes[index]) {
|
|
oldTemplate = oldTemplates[index];
delete scopes[index][oldTemplate.item];
tpl = inst.templateModel.getTemplate(item);
scopes[index][tpl.item] = item;
if (tpl !== oldTemplates[index]) {
|
|
onRowTemplateChange({}, item, oldTemplates[index].name, tpl.name, [], true);
}
}
}
|
when the data changes. It is compared by reference, not value for speed (this is the default angular setting). |
function onDataChanged(newVal, oldVal) {
inst.log("onDataChanged");
inst.grouped = scope.$eval(attr.grouped);
if (oldVal !== inst.getOriginalData()) {
oldVal = inst.getOriginalData();
}
if (!inst.options.smartUpdate || !inst.data.length || dirtyCheckData(newVal, oldVal)) {
var evt = dispatch(exports.datagrid.events.ON_BEFORE_DATA_CHANGE, newVal, oldVal);
if (evt.defaultPrevented && evt.newValue) {
newVal = evt.newValue;
}
values.dirty = true;
flow.add(changeData, [ newVal, oldVal ]);
return true;
} else if (isDataReallyChanged(newVal)) {
|
we just want to update the data values and scope values, because no templates changed. |
values.dirty = true;
mapData(newVal, oldVal);
flow.add(updateHeights, [], 0);
return true;
}
return false;
}
function isDataReallyChanged(newVal) {
|
loop through the data and make sure it hasn't already been updated by swap. |
var i = 0, norm = inst.normalize(newVal, inst.grouped), len = newVal.length;
while (i < len) {
if (inst.data[i] !== norm[i]) {
return true;
}
i += 1;
}
return false;
}
function changeData(newVal, oldVal) {
if (inst.flow.count("changeData") > 1) {
|
the first one is this call. |
return;
}
inst.log(" changeData");
inst.templateModel.clearAllRowHeights();
dispatch(exports.datagrid.events.ON_BEFORE_RESET, inst);
inst.data = inst.setData(newVal, inst.grouped) || [];
dispatch(exports.datagrid.events.ON_AFTER_DATA_CHANGE, inst.data, oldVal);
reset();
}
|
function reset() {
inst.info("reset start");
flow.clear();
|
|
we are going to clear all in the flow before doing a reset. state = states.BUILDING; |
destroyScopes();
|
now destroy all of the dom. |
rowOffsets = {};
active.length = 0;
scopes.length = 0;
|
keep reference to the old content and add a class to it so we can tell it is old. We will remove it after the render. |
content.children().unbind();
content.children().remove();
|
make sure scopes are destroyed before this level and listeners as well or this will create a memory leak. |
if (inst.chunkModel.getChunkList()) {
inst.chunkModel.reset(inst.data, content, scopes);
inst.rowsLength = inst.data.length;
updateHeights();
} else {
buildRows(inst.data, true);
}
flow.add(inst.info, [ "reset complete" ]);
flow.add(dispatch, [ exports.datagrid.events.ON_AFTER_RESET, inst ]);
}
|
function forceRenderScope(index) {
var s = scopes[index];
|
|
|
if (!s && index >= 0 && index < inst.rowsLength) {
s = compileRow(index);
}
if (s && !scope.$$phase) {
activateScope(s);
s.$digest();
deactivateScope(s);
s.$digested = true;
if (inst.templateModel.hasVariableRowHeights()) {
inst.chunkModel.updateAllChunkHeights(index);
updateHeightValues();
}
}
}
|
when changing the template for an individual row. Params
evt
Event
item
*
oldTemplate
Object
newTemplate
Object
classes
Array
skipUpdateHeights
Boolean=
|
function onRowTemplateChange(evt, item, oldTemplate, newTemplate, classes, skipUpdateHeights) {
var index = inst.getNormalizedIndex(item), el = getExistingRow(index), s = getScope(index), replaceEl;
if (s && s !== scope) {
|
no scope if that row was removed. |
replaceEl = angular.element(inst.templateModel.getTemplateByName(newTemplate).template);
replaceEl.addClass(options.uncompiledClass);
while (classes && classes.length) {
replaceEl.addClass(classes.shift());
}
el.parent()[0].replaceChild(replaceEl[0], el[0]);
activateScope(s);
link(index, s);
el.remove();
s.$destroy();
scopes[index] = null;
if (!skipUpdateHeights) {
inst.chunkModel.updateRow(index, item);
updateHeights(index);
}
}
}
|
force invalidation of heights and recalculate them then render. If a rowIndex is specified, only update ones affected by that row, otherwise update all. Params
rowIndex
Number=
|
function updateHeights(rowIndex) {
flow.add(updateViewportHeight);
flow.add(inst.chunkModel.updateAllChunkHeights, [ rowIndex ]);
flow.add(updateHeightValues);
flow.add(updateViewportHeight);
flow.add(function() {
var maxScrollHeight = inst.getContentHeight() - inst.getViewportHeight();
if (values.scroll > maxScrollHeight) {
values.scroll = maxScrollHeight;
}
});
flow.add(inst.dispatch, [ exports.datagrid.events.ON_AFTER_HEIGHTS_UPDATED ]);
flow.add(render);
flow.add(inst.dispatch, [ exports.datagrid.events.ON_AFTER_HEIGHTS_UPDATED_RENDER ]);
}
|
function dispatch(event) {
if (options.debug) eventLogger.log("$emit %s", event);
|
|
THIS SHOULD ONLY EMIT. Broadcast could perform very poorly especially if there are a lot of rows. |
return exports.util.apply(scope.$emit, scope, arguments);
}
function forceGarbageCollection() {
|
concept is to create a large object that will cause the browser to garbage collect before creating it. then since it has no reference it gets removed. |
clearInterval(gcIntv);
if (!inst.shuttingDown) {
gcIntv = setTimeout(function() {
if (inst) {
inst.info("GC");
var a, i, total = 1024 * 1024 * .5;
for (i = 0; i < total; i += 1) {
a = .5;
}
}
}, 5e3);
}
}
|
used to destroy the scopes of all rows in the datagrid that are compiled. |
function destroyScopes() {
|
because child scopes may not be in order because of rendering techniques we must loop through all scopes and destroy them manually. |
var lastScope, nextScope, i = 0;
each(scopes, function(s, index) {
|
listeners should be destroyed with the angular destroy. |
if (s) {
s.$$prevSibling = lastScope || undefined;
i = index;
while (!nextScope && i < inst.rowsLength) {
i += 1;
nextScope = scopes[i] || undefined;
}
activateScope(s);
lastScope = s;
s.$destroy();
}
});
scope.$$childHead = undefined;
scope.$$childTail = undefined;
scopes.length = 0;
}
|
function destroy() {
inst.shuttingDown = true;
getContent()[0].style.display = "none";
scope.datagrid = null;
|
|
we have a circular reference. break it on destroy. |
inst.log("destroying grid");
window.removeEventListener("resize", onResize);
clearTimeout(values.scrollingStopIntv);
|
destroy flow. |
flow.destroy();
inst.flow = undefined;
flow = null;
|
destroy watchers. |
while (unwatchers.length) {
unwatchers.pop()();
}
inst.destroyLogger();
|
now remove every property on exports. |
for (var i in inst) {
if (inst[i] && inst[i].hasOwnProperty("destroy")) {
inst[i].destroy();
inst[i] = null;
}
}
|
activate scopes so they can be destroyed by angular. |
destroyScopes();
element.remove();
|
this seems to be the most memory efficient way to remove elements. |
delete scope.$parent[inst.name];
rowOffsets = null;
inst = null;
scope = null;
element = null;
attr = null;
unwatchers = null;
content = null;
active.length = 0;
active = null;
scopes.length = 0;
scopes = null;
values = null;
states = null;
events = null;
options = null;
logEvents = null;
$compile = null;
}
exports.logWrapper("datagrid", inst, "green", inst);
exports.logWrapper("events", eventLogger, "light", inst);
scope.datagrid = inst;
setupExports();
return inst;
}
|
define the directive, setup addons, apply core addons then optional addons. |
module.directive("uxDatagrid", [ "$compile", "gridAddons", "$timeout", function($compile, gridAddons, $timeout) {
return {
restrict: "AE",
scope: true,
link: {
pre: function(scope, element, attr) {
var inst = new Datagrid(scope, element, attr, $compile, $timeout);
each(exports.datagrid.coreAddons, function(method) {
exports.util.apply(method, inst, [ inst ]);
});
gridAddons(inst, attr.addons);
},
post: function(scope, element, attr) {
scope.datagrid.start();
}
}
};
} ]);
|
¶ chunkModelBecause the browser has low performance on dom elements that exist in high numbers and are all siblings chunking is used to break them up into limits of their number and their parents and so on. So think of it as every chunk not having more than X number of children weather those children be chunks or they be rows. This speeds up the browser significantly because a resize event from a dom element will not affect all of them, but just those direct siblings and then it's parents siblings and so on up the chain. Params
inst
Datagrid
|
exports.datagrid.coreAddons.chunkModel = function chunkModel(inst) {
var _list, _rows, _chunkSize, _el, result = exports.logWrapper("chunkModel", {}, "purple", inst), _templateStartCache, _templateEndCache, _cachedDomRows = [];
|
getChunkList Return the list that was created. |
function getChunkList() {
return _list;
}
|
Create a ChunkArray from the array of data that is passed. The array that is passed should not be multi-dimensional. This will only work with a single dimensional array. Params
list
Array
size
Number
templateStart
String
templateEnd
String
|
function chunkList(list, size, templateStart, templateEnd) {
var i = 0, len = list.length, result = new ChunkArray(inst.options.chunks.detachDom), childAry, item;
while (i < len) {
item = list[i];
if (i % size === 0) {
if (childAry) {
childAry.updateHeight(inst.templateModel, _rows);
}
childAry = new ChunkArray(inst.options.chunks.detachDom);
childAry.min = item.min || i;
childAry.templateModel = inst.templateModel;
childAry.templateStart = templateStart;
childAry.templateEnd = templateEnd;
childAry.parent = result;
childAry.index = result.length;
result.push(childAry);
}
if (item instanceof ChunkArray) {
item.parent = childAry;
item.index = childAry.length;
}
childAry.push(item);
childAry.max = item.max || i;
i += 1;
}
if (childAry) {
childAry.updateHeight(inst.templateModel, _rows);
}
if (!result.min) {
result.min = result[0] ? result[0].min : 0;
result.max = result[result.length - 1] ? result[result.length - 1].max : 0;
result.templateStart = templateStart;
result.templateEnd = templateEnd;
result.updateHeight(inst.templateModel, _rows);
result.dirtyHeight = false;
}
return result.length > size ? chunkList(result, size, templateStart, templateEnd) : result;
}
|
Update rows affected by this particular index change. if rowIndex is undefined, update all. Params
rowIndex
Number=
|
function updateAllChunkHeights(rowIndex) {
var indexes, ary;
if (rowIndex === undefined || inst.options.chunks.detachDom) {
|
TODO: unit test needed. detach dom must enter here, because it is absolute positioned so it will not push down the other chunks automatically like relative positioning will. |
if (_list) {
_list.forceHeightReCalc(inst.templateModel, _rows);
_list.updateHeight(inst.templateModel, _rows, 1, true);
if (_list.detachDom) {
_list.updateDomHeight(1);
}
}
} else {
indexes = getRowIndexes(rowIndex, _list);
ary = getArrayFromIndexes(indexes, _list);
ary.updateHeight(inst.templateModel, _rows, -1, true);
}
}
|
Look up the chunkArray given an indexes array. Params
indexes
Array
ary
ChunkArray
|
function getArrayFromIndexes(indexes, ary) {
var index;
while (indexes.length) {
index = indexes.shift();
if (ary[index] instanceof ChunkArray) {
ary = ary[index];
}
}
return ary;
}
|
Create the chunkList so that it is ready for dom. Set properties needed to create the dom. The dom gets created when the rows are accessed. Params
list
Array
// single dimensional array only.
size
Number
templateStart
String
templateEnd
String
el
DomElement
|
function chunkDom(list, size, templateStart, templateEnd, el) {
result.log("chunkDom");
_el = el;
_chunkSize = size;
_rows = list;
_templateStartCache = templateStart;
_templateEndCache = templateEnd;
_list = chunkList(list, size, templateStart, templateEnd);
updateDom(_list);
_list.updateDomHeight(1);
return el;
}
|
For quick updates that do not require rechunking. Params
list
|
function updateList(list) {
if (_rows.length !== list.length) {
return chunkDom(list, _chunkSize, _templateStartCache, _templateEndCache, _el);
} else {
var i = 0, len = list.length;
while (i < len) {
updateRow(i, list[i]);
i += 1;
}
}
}
|
Update the item using the normalized index to map to the chunkArray. Params
rowIndex
Number
rowData
Object
|
function updateRow(rowIndex, rowData) {
var indexes = getRowIndexes(rowIndex, _list), lastIndex = indexes.pop(), ca = getItemByIndexes(indexes);
if (ca && ca[lastIndex]) {
ca[lastIndex] = rowData;
ca.dirtyHeight = true;
}
_rows[rowIndex] = rowData;
}
|
Generate an array of indexes that point to that row. Params
rowIndex
chunkList
indexes
|
function getRowIndexes(rowIndex, chunkList, indexes) {
if (!chunkList) {
return [];
}
var i = 0, len = chunkList.length, chunk;
indexes = indexes || [];
while (i < len) {
chunk = chunkList[i];
if (chunk instanceof ChunkArray) {
if (rowIndex >= chunk.min && rowIndex <= chunk.max) {
indexes.push(i);
getRowIndexes(rowIndex, chunk, indexes);
break;
}
} else {
|
we are at the end. So we just need the last index. |
indexes.push(rowIndex % _chunkSize);
break;
}
i += 1;
}
return indexes;
}
|
Get the dom row element. Params
rowIndex
{Number}
|
function getRow(rowIndex) {
var indexes = getRowIndexes(rowIndex, _list), el = buildDomByIndexes(indexes);
if (el && el.length && el.attr("row-id") === undefined) {
el.attr("row-id", rowIndex);
}
return el;
}
|
Get the dom row element. Params
rowIndex
{Number}
|
function getExistingRow(rowIndex) {
if (!_list) {
return undefined;
}
var indexes = getRowIndexes(rowIndex, _list);
return getDomRowByIndexes(indexes);
}
|
function getItemByIndexes(indexes) {
var indxs = indexes.slice(0), ca = _list;
while (indxs.length) {
ca = ca[indxs.shift()];
}
return ca;
}
|
|
Get the dom element given the indexes array. This cannot be exposed because public api should use the buildDomByIndexes that is called from getRow. Params
indexes
Array
unrendered
Function
|
function getDomRowByIndexes(indexes, unrendered) {
var i = 0, index, indxs = indexes.slice(0), ca = _list, el = _el;
while (i < indxs.length) {
index = indxs.shift();
if (unrendered && (!ca.rendered || shouldRecompileDecompiledRows(ca))) {
unrendered(el, ca);
updateDom(ca);
}
if (!indxs.length) {
checkAllCompiled(ca);
}
ca = ca[index];
el = ca.rendered || angular.element(el.children()[index]);
}
return el;
}
function shouldRecompileDecompiledRows(ca) {
var recompile = !ca.hasChildChunks() && ca.length && ca.rendered && ca.rendered.children().length !== ca.length;
if (recompile) {
result.info("recompile chunk %s", ca.getId());
}
return recompile;
}
|
How to handle array chunks that have not been rendered yet. They may copy from cache or even create new dom from html strings. Params
el
JQLite
ca
ChunkArray
|
function unrendered(el, ca) {
var children, i = 0, iLen;
el.html(ca.getChildrenStr(false, _chunkSize));
children = el.children();
ca.rendered = el;
if (ca.hasChildChunks()) {
|
assign the dom element. |
iLen = children.length;
while (i < iLen) {
ca[i].dom = children[i];
i += 1;
}
}
if (ca.detachDom && ca.dirtyHeight) {
ca.updateDomHeight();
}
exports.each(children, computeStyles);
if (ca.hasChildChunks()) {
if (children[0].className.indexOf(inst.options.chunks.chunkClass) !== -1) {
|
need to calculate css styles before adding this class to make transitions work. |
children.addClass(inst.options.chunks.chunkReadyClass);
}
} else if (!ca.rendered.hasClass(inst.options.chunks.chunkReadyClass)) {
ca.rendered.addClass(inst.options.chunks.chunkReadyClass);
}
}
|
Get the domElement by indexes, create the dom if it doesn't exist. Params
indexes
Array
|
function buildDomByIndexes(indexes) {
return getDomRowByIndexes(indexes, unrendered);
}
|
calculate the computed styles of each element |
function computeStyles(elm) {
if (elm) {
var style = window.getComputedStyle(elm);
if (style) {
return style.getPropertyValue("top");
}
}
}
|
Each ChunkArray keeps track of weather or not it's dom has been compiled. Since each ChunkArray generates the values and updates properties of the dom. The dom chunks are a reflection of the ChunkArrays. Params
ca
ChunkArray
|
function checkAllCompiled(ca) {
if (!ca.compiled) {
ca.compiled = isCompiled(ca);
if (ca.compiled) {
if (ca.parent) {
|
a parent cannot be compiled till it's last child is done. So don't check until a chunk child compiles. |
checkAllCompiled(ca.parent);
}
inst.flow.add(disableNonVisibleChunks);
}
}
return ca.compiled;
}
|
function isCompiled(ca) {
var min, max;
if (ca[0] instanceof ChunkArray) {
min = 0;
max = ca.length;
while (min < max) {
if (!ca[min].compiled) {
return false;
}
min += 1;
}
return true;
}
min = ca.min;
max = ca.max;
while (min < max) {
if (!inst.isCompiled(min)) {
return false;
}
min += 1;
}
return true;
}
function updateDom(ca) {
ca.updateDom(inst.options.chunks.chunkDisabledClass);
}
|
|
function reset(newList, content, scopes) {
result.log("reset");
_cachedDomRows.length = 0;
newList = newList || [];
|
|
TODO: this needs to make sure it destroys things properly |
if (_list) {
_list.destroy();
_list = null;
_el = null;
_rows = null;
}
chunkDom(newList, _chunkSize, _templateStartCache, _templateEndCache, content);
}
|
disable all chunks that are outside of the values.activeRange.min/max. |
function disableNonVisibleChunks() {
var r = inst.values.activeRange, o = inst.options.chunks;
_list.enableRange(r.min, r.max, o.chunkDisabledClass);
if (o.detachDom) {
|
we need to update which chunks are compiled. |
updateDom(_list);
}
}
|
function destroy() {
reset();
_list = null;
_rows = null;
_chunkSize = null;
_el = null;
_templateStartCache = null;
_templateEndCache = null;
_cachedDomRows.length = 0;
_cachedDomRows = null;
inst.chunkModel = null;
result.destroyLogger();
result = null;
inst = null;
}
inst.flow.unique(updateDom);
result.chunkDom = chunkDom;
result.getChunkList = getChunkList;
result.getRowIndexes = function(rowIndex) {
return getRowIndexes(rowIndex, _list);
};
result.getItemByIndexes = getItemByIndexes;
result.getRow = getRow;
result.getExistingRow = getExistingRow;
result.reset = reset;
result.updateRow = updateRow;
result.updateList = updateList;
result.updateAllChunkHeights = updateAllChunkHeights;
result.getRowIndexFromIndexes = getRowIndexFromIndexes;
result.destroy = destroy;
inst.scope.$on(exports.datagrid.events.ON_AFTER_UPDATE_WATCHERS, disableNonVisibleChunks);
|
|
apply event dispatching. |
exports.util.dispatcher(result);
inst.chunkModel = result;
return result;
};
exports.datagrid.coreAddons.push(exports.datagrid.coreAddons.chunkModel);
|
Params
indexes
Array.
chunkSize
Number
|
function getRowIndexFromIndexes(indexes, chunkSize) {
var rowIndex = 0;
if (typeof indexes === "string") {
indexes = indexes.split(".");
}
|
don't multiply the last one, because it is a row and not a chunk |
for (var i = 0; i < indexes.length; i += 1) {
indexes[i] = parseInt(indexes[i], 10);
if (i < indexes.length - 1) {
rowIndex += indexes[i] * chunkSize;
} else {
rowIndex += indexes[i];
}
}
return rowIndex;
}
|
¶ ChunkArrayis an array with additional properties needed by the chunkModel to generate and access chunks of the dom with high performance. |
function ChunkArray(detachDom) {
this.uid = ChunkArray.uid++;
this.enabled = true;
this.min = 0;
this.max = 0;
this.templateStart = "";
this.templateStartWithPos = "";
this.templateEnd = "";
this.parent = null;
this.mode = detachDom ? ChunkArray.DETACHED : ChunkArray.ATTACHED;
this.detachDom = detachDom;
this.index = 0;
}
ChunkArray.uid = 0;
ChunkArray.DETACHED = "chunkArray:detached";
ChunkArray.ATTACHED = "chunkArray:attached";
ChunkArray.prototype = [];
ChunkArray.prototype.getStub = function getStub(str) {
if (!this.templateStartWithPos) {
this.createDomTemplates();
}
return this.templateStartWithPos + str + this.templateEnd;
};
ChunkArray.prototype.inRange = function(value) {
return value >= this.min && value <= this.max;
};
ChunkArray.prototype.rangeOverlap = function(min, max, cushion) {
var overlap = false;
cushion = cushion > 0 ? cushion : 0;
min -= cushion;
max += cushion;
while (min <= max) {
|
if min < max then a grid with only 1 items shows that row disabled. |
if (this.inRange(min)) {
overlap = true;
break;
}
min += 1;
}
return overlap;
};
ChunkArray.prototype.each = function(method, args) {
var i = 0, len = this.length;
while (i < len) {
exports.util.apply(method, this[i], args);
i += 1;
}
};
|
¶ ChunkArray.prototype.getChildrenStrGet the HTML string representation of the children in this array. If deep then return this and all children down. Params
deep
chunkSize
|
ChunkArray.prototype.getChildrenStr = function(deep, chunkSize) {
var i = 0, len = this.length, str = "", ca = this, rowIndex, tpl, xml, style;
while (i < len) {
if (ca[i] instanceof ChunkArray) {
str += ca[i].getStub(deep ? ca[i].getChildrenStr(deep) : "", chunkSize);
} else {
rowIndex = getRowIndexFromIndexes(ca._id + "." + i, chunkSize);
str += this.templateModel.getTemplate(ca[i]).template;
}
i += 1;
}
return str;
};
|
ChunkArray.prototype.updateHeight = function(templateModel, _rows, recurse, updateDomHeight) {
var i = 0, len, height = 0, lastChild;
if (this[0] instanceof ChunkArray) {
len = this.length;
while (i < len) {
if (recurse === 1) {
this[i].updateHeight(templateModel, _rows, recurse, updateDomHeight);
}
height += this[i].height;
i += 1;
}
} else {
height = templateModel.getHeight(_rows, this.min, this.max);
}
if (this.height !== height) {
this.dirtyHeight = true;
this.height = height;
} else if (this.rendered) {}
if (recurse == -1 && this.dirtyHeight && this.parent) {
this.parent.updateHeight(templateModel, _rows, recurse, updateDomHeight);
}
if (updateDomHeight && this.dirtyHeight) {
this.updateDomHeight();
}
};
ChunkArray.prototype.getPreviousSibling = function() {
var prevSibling, prevIndex;
if (this.parent) {
prevIndex = this.index - 1;
prevSibling = this.parent[prevIndex];
if (!prevSibling || prevSibling.index !== this.index - 1) {
|
|
we must the first in the array. so we have to jump up higher. Or we are the first item in the first chunk. |
if (this.parent.parent) {
prevSibling = this.parent.getPreviousSibling();
if (prevSibling) {
prevSibling = prevSibling.last();
}
}
}
}
return prevSibling;
};
ChunkArray.prototype.getNextSibling = function() {
var nextSibling, nextIndex;
if (this.parent) {
nextIndex = this.index + 1;
nextSibling = this.parent[nextIndex];
if (!nextSibling || nextSibling.index !== this.index + 1) {
|
we must be at the end of the array. So we need to jump up higher. Or we could be at the very end. |
if (this.parent.parent) {
nextSibling = this.parent.getNextSibling();
if (nextSibling) {
nextSibling = nextSibling.first();
}
}
}
}
return nextSibling;
};
ChunkArray.prototype.first = function() {
return this[0];
};
ChunkArray.prototype.last = function() {
return this[this.length - 1];
};
|
ChunkArray.prototype.calculateTop = function() {
var top = 0, prevSibling;
if (this.index && this.parent) {
prevSibling = this.getPreviousSibling();
if (prevSibling) {
top = prevSibling.top + prevSibling.height;
}
}
this.top = top;
return this.top;
};
|
|
Ignore any cached values and update the height of this chunk. Params
templateModel
_rows
|
ChunkArray.prototype.forceHeightReCalc = function(templateModel, _rows) {
var i = 0, len, height = 0;
if (this[0] instanceof ChunkArray) {
len = this.length;
while (i < len) {
height += this[i].forceHeightReCalc(templateModel, _rows);
i += 1;
}
} else {
height = templateModel.getHeight(_rows, this.min, this.max);
}
if (this.height !== height) {
this.height = height;
if (this.detachDom) {
|
we need to update all siblings if we change. |
this.dirtySiblings();
} else {
this.setDirtyHeight();
}
}
return this.height;
};
|
Set this chunk as dirty so heights need calculated. |
ChunkArray.prototype.setDirtyHeight = function() {
var p = this;
while (p) {
p.dirtyHeight = true;
p = p.parent;
}
};
ChunkArray.prototype.dirtySiblings = function() {
this.dirtyHeight = true;
if (this.parent) {
var i = 0, iLen = this.length;
while (i < iLen) {
this[i].dirtyHeight = true;
i += 1;
}
this.parent.dirtySiblings();
}
};
|
ChunkArray.prototype.getId = function() {
if (this._index !== this.index || !this._id) {
var p = this, s = "";
this._index = this.index;
|
|
keep the last index so if it changes. We change the id. |
while (p) {
s = "." + p.index + s;
p = p.parent;
}
this._id = s.substr(1, s.length);
}
return this._id;
};
ChunkArray.prototype.hasChildChunks = function() {
if (!this._hasChildChunks) {
this._hasChildChunks = this.first() instanceof ChunkArray;
}
return this._hasChildChunks;
};
ChunkArray.prototype.enableRange = function(min, max, disabledClass) {
if (this.rangeOverlap(min, max, this.detachDom)) {
this.enable(disabledClass);
} else {
this.disable(disabledClass);
}
if (this.hasChildChunks()) {
this.each(this.enableRange, [ min, max, disabledClass ]);
}
};
ChunkArray.prototype.enable = function(disabledClass) {
if (!this.enabled) {
this.enabled = true;
this.updateDom(disabledClass);
if (this.parent) {
this.parent.enable(disabledClass);
}
}
};
ChunkArray.prototype.disable = function(disabledClass) {
var i = 0, len;
if (this.compiled) {
if (this.hasChildChunks()) {
len = this.length;
while (i < len) {
this[i].disable(disabledClass);
i += 1;
}
}
if (this.enabled) {
this.enabled = false;
this.updateDom(disabledClass);
}
}
};
ChunkArray.prototype.updateDom = function(disabledClass) {
if (this.rendered) {
if (this.compiled && !this.rendered.attr("compiled")) {
this.rendered.attr("compiled", true);
}
if (this.detachDom) {
if (this.enabled) {
if (this.detached) {
this.detached = false;
this.parent.rendered.append(this.rendered);
}
} else if (!this.enabled && !this.detached) {
if (this.parent && this.parent.compiled && this.rendered.parent().length) {
this.detached = true;
|
jquery detach is just 2nd param pass true to keep data around. |
this.rendered.remove(undefined, true);
}
}
} else {
this.rendered.attr("enabled", this.enabled);
if (this.enabled) {
this.rendered.removeAttr("disabled");
this.rendered.removeAttr("read-only");
this.rendered.removeClass(disabledClass);
} else {
this.rendered.attr("disabled", "disabled");
this.rendered.attr("read-only", true);
this.rendered.addClass(disabledClass);
}
}
this.each(this.updateDom, [ disabledClass ]);
}
};
ChunkArray.prototype.updateDomHeight = function(recursiveDirection) {
var dom = this.rendered && this.rendered[0] || this.dom;
if (dom) {
this.dirtyHeight = false;
if (this.mode === ChunkArray.DETACHED) {
this.calculateTop();
dom.style.top = this.top + "px";
}
dom.style.height = this.height + "px";
} else {
this.createDomTemplates();
}
if (recursiveDirection === -1 && this.parent) {
this.parent.updateDomHeight(recursiveDirection);
} else if (recursiveDirection && this.hasChildChunks()) {
this.each(this.updateDomHeight, [ recursiveDirection ]);
}
};
ChunkArray.prototype.createDomTemplates = function() {
if (!this.templateReady && this.templateStart) {
var str = this.templateStart.substr(0, this.templateStart.length - 1) + ' style="';
if (this.mode === ChunkArray.DETACHED) {
this.calculateTop();
str += "position:absolute;top:" + this.top + "px;left:0px;";
}
this.templateStartWithPos = str + "width:100%;height:" + this.height + 'px;" chunk-id="' + this.getId() + '" range="' + this.min + ":" + this.max + '">';
this.templateReady = true;
}
};
|
Return an array of the children created from the rendered properties of the children. |
ChunkArray.prototype.children = function() {
var children = [];
this.each(function() {
children.push(this.rendered);
}, []);
};
ChunkArray.prototype.decompile = function(chunkReadyClass) {
if (this.hasChildChunks()) {
this.each("decompile", [ chunkReadyClass ]);
} else {
|
we are going to remove all dom rows to free up memory. this can only be done if the chunk has no rows for children instead of chunks. |
if (this.rendered) {
this.rendered.children().remove();
this.rendered.removeClass(chunkReadyClass);
}
}
};
|
ChunkArray.prototype.destroy = function() {
if (this.hasChildChunks()) {
this.each(this.destroy);
}
this.templateStart = "";
this.templateEnd = "";
this.templateModel = null;
this.rendered = null;
this.dom = null;
this.parent = null;
while (this.length) {
this.pop();
}
this.length = 0;
};
exports.datagrid.events.ON_RENDER_PROGRESS = "datagrid:onRenderProgress";
exports.datagrid.events.STOP_CREEP = "datagrid:stopCreep";
exports.datagrid.events.ENABLE_CREEP = "datagrid:enableCreep";
exports.datagrid.events.DISABLE_CREEP = "datagrid:disableCreep";
exports.datagrid.coreAddons.creepRenderModel = function creepRenderModel(inst) {
var intv = 0, creepCount = 0, model = exports.logWrapper("creepModel", {}, "blue", inst), upIndex = 0, downIndex = 0, waitHandle, waitingOnReset, time, lastPercent, unwatchers = [], forceScroll = false, scrollIndex = 0, scrollIndexPadding = 0;
function digest(index) {
if (inst.scope.$root.$$phase) {
return false;
}
var s = inst.getScope(index);
if (!s || !s.$digested) {
|
|
just skip if already digested. |
inst.forceRenderScope(index);
}
return true;
}
function calculatePercent() {
var result = {
count: 0
};
each(inst.scopes, calculateScopePercent, result);
if (result.count >= inst.rowsLength) {
model.disable();
}
return {
count: result.count,
len: inst.rowsLength
};
}
function calculateScopePercent(s, index, list, result) {
result.count += s ? 1 : 0;
}
function onInterval(started, ended, force) {
if (!inst.values.touchDown) {
waitingOnReset = false;
time = Date.now() + inst.options.renderThreshold;
upIndex = started;
downIndex = ended;
render(onComplete, force);
}
}
function wait(method, time) {
var args = exports.util.array.toArray(arguments);
args.splice(0, 2);
if (inst.options.async) {
clearTimeout(waitHandle);
waitHandle = setTimeout(function() {
exports.util.apply(method, null, args);
}, time);
} else {
exports.util.apply(method, this, args);
}
return waitHandle;
}
function findUncompiledIndex(index, dir) {
while (index >= 0 && index < inst.rowsLength && inst.isCompiled(index)) {
index += dir;
}
if (index >= 0 && index < inst.rowsLength) {
return index;
}
return dir > 0 ? inst.rowsLength : -1;
}
function render(complete, force) {
var now = Date.now(), dynamicHeights;
if (time > now && hasIndexesLeft()) {
dynamicHeights = inst.templateModel.hasVariableRowHeights();
upIndex = force ? upIndex : findUncompiledIndex(upIndex, -1);
if (upIndex >= 0) {
if (digest(upIndex)) {
if (force) {
upIndex -= 1;
}
}
}
downIndex = force ? downIndex : findUncompiledIndex(downIndex, 1);
if (downIndex !== inst.rowsLength) {
if (digest(downIndex)) {
if (force) {
downIndex += 1;
}
}
}
render(complete, force);
|
making this async was counter effective on performance. |
if (dynamicHeights) {
forceScrollToIndex();
}
} else {
complete();
}
}
function onComplete() {
stop();
if (!hasIndexesLeft()) {
creepCount = 0;
model.disable();
lastPercent = 1;
inst.dispatch(exports.datagrid.events.ON_RENDER_PROGRESS, 1);
} else {
creepCount += 1;
if (!inst.values.touchDown && !inst.values.speed && hasIndexesLeft()) {
resetInterval(upIndex, downIndex);
}
var percent = calculatePercent();
if (percent !== lastPercent) {
inst.dispatch(exports.datagrid.events.ON_RENDER_PROGRESS, percent);
}
}
}
function hasIndexesLeft() {
return !!(upIndex > -1 || downIndex < inst.rowsLength);
}
function stop() {
time = 0;
clearTimeout(intv);
clearTimeout(waitHandle);
intv = 0;
}
function resetInterval(started, ended, waitTime, forceCompileRowRender) {
stop();
if (creepCount < inst.options.creepLimit) {
intv = wait(onInterval, waitTime || inst.options.renderThresholdWait, started, ended, forceCompileRowRender);
}
}
function renderLater(event, forceCompileRowRender) {
resetInterval(upIndex, downIndex, inst.options.creepStartDelay, forceCompileRowRender);
}
function forceScrollToIndex() {
forceScroll = true;
var scroll = inst.getRowOffset(scrollIndex) + scrollIndexPadding;
inst.scrollModel.scrollTo(scroll, true);
forceScroll = false;
}
function onBeforeRender(event) {
if (!forceScroll) {
if (inst.templateModel.hasVariableRowHeights()) {
scrollIndex = inst.getOffsetIndex(inst.values.scroll);
scrollIndexPadding = inst.values.scroll - inst.getRowOffset(scrollIndex);
}
stop();
}
}
function onAfterRender(event, loopData, forceCompileRowRender) {
creepCount = 0;
upIndex = loopData.started || 0;
downIndex = loopData.ended || 0;
renderLater(event, forceCompileRowRender);
}
function onBeforeReset(event) {
onBeforeRender(event);
if (inst.options.creepRender && inst.options.creepRender.enable !== false) {
model.enable();
}
}
model.stop = stop;
|
allow external stop of creep render. |
model.destroy = function destroy() {
model.disable();
stop();
inst = null;
model = null;
};
model.enable = function() {
if (!unwatchers.length) {
unwatchers.push(inst.scope.$on(exports.datagrid.events.BEFORE_VIRTUAL_SCROLL_START, onBeforeRender));
unwatchers.push(inst.scope.$on(exports.datagrid.events.ON_VIRTUAL_SCROLL_UPDATE, onBeforeRender));
unwatchers.push(inst.scope.$on(exports.datagrid.events.ON_TOUCH_DOWN, onBeforeRender));
unwatchers.push(inst.scope.$on(exports.datagrid.events.ON_SCROLL_START, onBeforeRender));
unwatchers.push(inst.scope.$on(exports.datagrid.events.ON_AFTER_UPDATE_WATCHERS, onAfterRender));
}
};
model.disable = function() {
stop();
model.info("creep Disabled");
while (unwatchers.length) {
unwatchers.pop()();
}
};
inst.unwatchers.push(inst.scope.$on(exports.datagrid.events.DISABLE_CREEP, model.disable));
inst.unwatchers.push(inst.scope.$on(exports.datagrid.events.ON_BEFORE_RESET, onBeforeReset));
inst.unwatchers.push(inst.scope.$on(exports.datagrid.events.STOP_CREEP, stop));
inst.creepRenderModel = model;
|
do not add listeners if it is not enabled. |
if (inst.options.creepRender && inst.options.creepRender.enable) {
model.enable();
} else {
model.disable();
}
};
exports.datagrid.coreAddons.push(exports.datagrid.coreAddons.creepRenderModel);
/*global ux */
exports.datagrid.coreAddons.normalizeModel = function normalizeModel(inst) {
|
TODO: this needs to be put on exp.normalizedModel |
var originalData, normalizedData, result = exports.logWrapper("normalizeModel", {}, "grey", inst);
|
inst.normalize = function normalize(data, grouped, normalized) {
data = data || [];
var i = 0, len = data.length;
normalized = normalized || [];
while (i < len) {
normalized.push(data[i]);
if (data[i] && data[i][grouped]) {
inst.normalize(data[i][grouped], grouped, normalized);
}
i += 1;
}
return normalized;
};
|
|
inst.setData = function(data, grouped) {
result.log("setData %s", data);
originalData = data;
if (grouped) {
normalizedData = inst.normalize(data, grouped);
} else {
normalizedData = data && data.slice(0) || [];
}
return normalizedData;
};
|
|
inst.getData = function() {
return normalizedData;
};
|
|
inst.getOriginalData = function() {
return originalData;
};
|
|
get the index or indexes of the item from the original data that the normalized array was created from. |
inst.getOriginalIndexOfItem = function getOriginalIndexOfItem(item) {
var indexes = ux.each(originalData, findItem, item, []);
return indexes && indexes !== originalData ? indexes : [];
};
|
function findItem(item, index, list, targetItem, indexes) {
var found;
indexes = indexes.slice(0);
indexes.push(index);
if (item === targetItem) {
return indexes;
} else if (item[inst.grouped] && item[inst.grouped].length) {
found = ux.each(item[inst.grouped], findItem, targetItem, indexes);
if (found && found !== item[inst.grouped]) {
return found;
}
}
return undefined;
}
|
|
inst.getNormalizedIndex = function getNormalizedIndex(item, startIndex) {
var i = startIndex || 0;
while (i < inst.rowsLength) {
if (inst.data[i] === item) {
return i;
}
i += 1;
}
if (startIndex) {
i = startIndex;
while (i >= 0) {
if (inst.data[i] === item) {
return i;
}
i -= 1;
}
}
return -1;
};
function applyAction(list, index, item, action) {
if (action === "replace") {
list[index] = item;
} else if (action === "insert") {
list.splice(index, 0, item);
} else if (action === "remove") {
list.splice(index, 1);
}
}
function modifyItem(item, index, action) {
|
|
first get the original item index. |
var indexes = inst.getOriginalIndexOfItem(normalizedData[index]), origItem, list = originalData, lastIndex;
while (indexes.length) {
lastIndex = indexes.shift();
origItem = list[lastIndex];
if (!indexes.length) {
if (inst.grouped && list[0] && list[0].hasOwnProperty(inst.grouped)) {
list = list[0][inst.grouped];
indexes.push(list.length);
lastIndex = list.length;
}
applyAction(list, lastIndex, item, action);
|
original data |
break;
}
if (inst.grouped) {
list = origItem[inst.grouped];
}
}
applyAction(normalizedData, index, item, action);
}
|
result.replace = function(item, index) {
modifyItem(item, index, "replace");
};
result.insert = function(item, index) {
modifyItem(item, index, "insert");
};
result.remove = function(index) {
modifyItem(null, index, "remove");
};
result.move = function(fromIndex, toIndex) {
var item = inst.getRowItem(fromIndex);
if (fromIndex > toIndex) {
result.remove(fromIndex);
result.insert(item, toIndex);
} else if (fromIndex < toIndex) {
result.insert(item, toIndex);
result.remove(fromIndex);
}
};
|
|
result.destroy = function destroy() {
result.destroyLogger();
originalData = null;
normalizedData = null;
inst.normalizeModel = null;
inst = null;
result = null;
};
inst.normalizeModel = result;
return inst;
};
exports.datagrid.coreAddons.push(exports.datagrid.coreAddons.normalizeModel);
/*global ux */
exports.datagrid.events.ON_SCROLL_START = "datagrid:scrollStart";
exports.datagrid.events.ON_SCROLL_STOP = "datagrid:scrollStop";
exports.datagrid.events.ON_TOUCH_DOWN = "datagrid:touchDown";
exports.datagrid.events.ON_TOUCH_UP = "datagrid:touchUp";
exports.datagrid.events.ON_TOUCH_MOVE = "datagrid:touchMove";
exports.datagrid.coreAddons.scrollModel = function scrollModel(inst) {
var result = exports.logWrapper("scrollModel", {}, "orange", inst), setup = false, enable = true, unwatchSetup, waitForStopIntv, lastTouchUpdateTime = 0, hasScrollListener = false, lastScroll, bottomOffset = 0, lastRenderTime, // start easing
startOffsetY, startOffsetX, offsetY, offsetX, startScroll, lastDeltaY, lastDeltaX, speed = 0, speedX = 0, startTime, distance, scrollingIntv, // end easing
listenerData = [ {
event: "touchstart",
method: "onTouchStart",
enabled: true
}, {
event: "touchmove",
method: "onTouchMove",
enabled: false
}, {
event: "touchend",
method: "onTouchEnd",
enabled: true
}, {
event: "touchcancel",
method: "onTouchEnd",
enabled: true
} ];
|
|
Listen for scrollingEvents. |
function setupScrolling() {
unwatchSetup();
inst.element.css("willChange", "scroll-position");
if (!inst.element.css("overflow") || inst.element.css("overflow") === "visible") {
inst.element.css({
overflow: "auto"
});
}
result.log("addScrollListener");
addScrollListener();
inst.unwatchers.push(inst.scope.$on(exports.datagrid.events.SCROLL_TO_INDEX, function(event, index) {
inst.scrollModel.scrollToIndex(index, true);
}));
inst.unwatchers.push(inst.scope.$on(exports.datagrid.events.SCROLL_TO_ITEM, function(event, item) {
inst.scrollModel.scrollToItem(item, true);
}));
inst.unwatchers.push(inst.scope.$on(exports.datagrid.events.SCROLL_INTO_VIEW, function(event, itemOrIndex) {
inst.scrollModel.scrollIntoView(itemOrIndex, true);
}));
addTouchEvents();
setup = true;
}
function addScrollListener() {
result.log("addScrollListener");
hasScrollListener = true;
inst.element[0].addEventListener("scroll", onUpdateScrollHandler);
}
function onBeforeReset() {
if (inst.options.scrollModel && inst.options.scrollModel.manual) {
listenerData[1].enabled = true;
}
if (hasScrollListener) {
result.removeScrollListener();
hasScrollListener = false;
}
result.removeTouchEvents();
}
function onAfterReset() {
if (!hasScrollListener) {
addScrollListener();
}
addTouchEvents();
}
function addTouchEvents() {
result.log("addTouchEvents");
var content = inst.getContent();
exports.each(listenerData, function(item) {
if (item.enabled) {
result.log(" add %s", item.event);
content.bind(item.event, result[item.method]);
}
});
}
result.fireOnScroll = function fireOnScroll() {
if (inst.values.scroll !== lastScroll) {
lastScroll = inst.values.scroll;
inst.dispatch(exports.datagrid.events.ON_SCROLL, inst.values);
}
};
result.removeScrollListener = function removeScrollListener() {
result.log("removeScrollListener");
hasScrollListener = false;
inst.element[0].removeEventListener("scroll", onUpdateScrollHandler);
};
result.removeTouchEvents = function removeTouchEvents() {
if (setup) {
result.log("removeTouchEvents");
var content = inst.getContent();
exports.each(listenerData, function(item) {
result.log(" remove %s", item.event);
content.unbind(item.event, result[item.method]);
});
}
};
function getTouches(event) {
return event.touches || event.originalEvent.touches;
}
result.killEvent = function(event) {
event.preventDefault();
if (event.stopPropagation) event.stopPropagation();
if (event.stopImmediatePropagation) event.stopImmediatePropagation();
};
result.enable = function(value) {
enable = !!value;
};
function getScrollTop() {
return inst.values.scroll;
}
function setElementScroll(value) {
inst.element[0].scrollTop = value;
inst.values.scroll = value;
}
result.onTouchStart = function onTouchStart(event) {
if (!enable) {
return;
}
clearTimeout(scrollingIntv);
inst.values.touchDown = true;
offsetY = startOffsetY = getTouches(event)[0].clientY || 0;
offsetX = startOffsetX = getTouches(event)[0].clientX || 0;
if (inst.values.scroll < 0) {
inst.values.scroll = 0;
} else if (inst.values.scroll > bottomOffset) {
inst.values.scroll = bottomOffset;
}
startScroll = inst.values.scroll;
lastDeltaY = 0;
lastDeltaX = 0;
inst.dispatch(exports.datagrid.events.ON_TOUCH_DOWN, event);
};
result.onTouchMove = function(event) {
if (!enable) {
return;
}
if (inst.options.scrollModel && inst.options.scrollModel.preventTouchMove) {
result.killEvent(event);
}
var now = Date.now();
if (now - lastTouchUpdateTime < 20) {
return;
}
lastTouchUpdateTime = now;
var y = getTouches(event)[0].clientY, x = getTouches(event)[0].clientX, deltaY = offsetY - y, deltaX = offsetX - x, scroll;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
return;
}
if (offsetY !== y) {
scroll = result.capScrollValue(getScrollTop() + deltaY);
result.setScroll(scroll);
speed = deltaY;
offsetY = y;
lastDeltaY = deltaY;
}
if (deltaX !== lastDeltaX) {
|
horizontal scrolling is not complete. prevent until completed otherwise it is firing multiple setScroll values. result.setScroll(result.capScrollValue(startScroll + deltaY)); |
speedX = deltaX - lastDeltaX;
lastDeltaX = deltaX;
}
inst.dispatch(exports.datagrid.events.ON_TOUCH_MOVE, speed, deltaY, lastDeltaY, speedX, deltaX, lastDeltaX);
};
result.onTouchEnd = function onTouchEnd(event) {
if (!enable) {
return;
}
if (!inst.values.touchDown) {
return;
}
inst.values.touchDown = false;
inst.dispatch(exports.datagrid.events.ON_TOUCH_UP, event);
if (listenerData[1].enabled) {
if (Math.abs(lastDeltaY) < 2 && Math.abs(lastDeltaX) < 2) {
result.click(event);
} else {
startTime = Date.now();
distance = speed * inst.options.scrollModel.speed;
result.scrollSlowDown(true);
}
} else {
result.onUpdateScroll();
}
var sTop = getScrollTop();
if (sTop < 0 || inst.getContentHeight() < inst.getViewportHeight()) {
setElementScroll(0);
} else if (sTop > inst.getContentHeight() - inst.getViewportHeight()) {
setElementScroll(inst.getContentHeight() - inst.getViewportHeight());
}
};
result.scrollSlowDown = function(wait) {
clearTimeout(scrollingIntv);
var value, duration = Math.abs(speed) * inst.options.scrollModel.speed, t = duration - (Date.now() - startTime), prevDistance = distance, change;
distance = result.easeOut(t, distance, speed || 0, duration);
change = distance - prevDistance;
if (Math.abs(change) < 5) {
t = 0;
}
if (t > 0) {
value = result.capScrollValue(getScrollTop() + change);
if (!wait) {
|
|
setElementScroll(value);
}
scrollingIntv = setTimeout(result.scrollSlowDown, 20);
}
};
result.easeOut = function easeOutQuad(t, b, c, d) {
return -c * (t /= d) * (t - 2) + b;
};
result.click = function(e) {
|
TODO: this needs to deprecate because this has finally been fixed in android. (Feb 5th 2015) simulate click on android. Ignore on IOS. |
if (inst.options.scrollModel.simulateClick) {
if (inst.options.scrollModel.simulateClick && target && !/(SELECT|INPUT|TEXTAREA)/i.test(target.tagName)) {
result.killEvent(e);
}
var target = e.target, ev;
if (!inst.isDigesting(inst.$scope) && target && !/(SELECT|INPUT|TEXTAREA)/i.test(target.tagName)) {
ev = document.createEvent("MouseEvents");
ev.initMouseEvent("click", true, true, e.view, 1, target.screenX, target.screenY, target.clientX, target.clientY, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, 0, null);
ev._constructed = true;
try {
inst.creepRenderModel.stop();
target.dispatchEvent(ev);
} catch (err) {}
}
}
};
result.getScroll = function getScroll(el) {
if (el) {
return el.scrollTop;
}
return getScrollTop();
};
result.setScroll = function setScroll(value) {
var unwatch, chunkList = inst.chunkModel.getChunkList();
if (!chunkList || !chunkList.height) {
|
wait until that height is ready then scroll. |
unwatch = inst.scope.$on(exports.datagrid.events.ON_AFTER_RENDER, function() {
unwatch();
result.setScroll(value);
});
} else if (inst.getContentHeight() - inst.getViewportHeight() >= value) {
setElementScroll(value);
result.onUpdateScroll();
}
};
function onUpdateScrollHandler(event) {
inst.scrollModel.onUpdateScroll(event);
}
|
When a scrollEvent is fired, recalculate the values. Params
event
|
result.onUpdateScroll = function onUpdateScroll(event) {
var val = inst.scrollModel.getScroll(event && (event.target || event.srcElement));
if (inst.values.scroll !== val) {
inst.dispatch(exports.datagrid.events.ON_SCROLL_START, val);
inst.values.speed = val - inst.values.scroll;
inst.values.absSpeed = Math.abs(inst.values.speed);
inst.values.scroll = val;
inst.values.scrollPercent = (inst.values.scroll / inst.getContentHeight() * 100).toFixed(2);
}
inst.scrollModel.waitForStop();
result.fireOnScroll();
};
result.capScrollValue = function(value) {
var newVal;
if (inst.getContentHeight() < inst.getViewportHeight()) {
inst.log(" CAPPED scroll value from %s to 0", value);
value = 0;
} else if (inst.getContentHeight() - value < inst.getViewportHeight()) {
|
don't allow to scroll past the bottom. |
newVal = inst.getContentHeight() - inst.getViewportHeight();
|
this will be the bottom scroll. |
inst.log(" CAPPED scroll value to keep it from scrolling past the bottom. changed %s to %s", value, newVal);
value = newVal;
}
return value;
};
|
Scroll to the numeric value. Params
value
immediately
Boolean=
|
result.scrollTo = function scrollTo(value, immediately) {
value = result.capScrollValue(value);
if (value !== lastScroll) {
inst.scrollModel.setScroll(value);
if (immediately) {
inst.scrollModel.onScrollingStop();
} else {
inst.scrollModel.waitForStop();
}
return true;
}
return false;
};
result.clearOnScrollingStop = function clearOnScrollingStop() {
result.onScrollingStop();
};
function flowWaitForStop() {
lastRenderTime = Date.now();
inst.scrollModel.onScrollingStop();
}
|
Wait for the datagrid to slow down enough to render. |
result.waitForStop = function waitForStop() {
var forceRender = false;
clearTimeout(waitForStopIntv);
result.log("waitForStop scroll = %s", inst.values.scroll);
if (inst.options.renderWhileScrolling) {
if (Date.now() - (inst.options.renderWhileScrolling > 0 || 0) > lastRenderTime) {
forceRender = true;
}
}
if (!forceRender && (inst.flow.async || inst.values.touchDown)) {
waitForStopIntv = setTimeout(flowWaitForStop, inst.options.updateDelay);
} else {
flowWaitForStop();
}
};
|
When it stops render. |
result.onScrollingStop = function onScrollingStop() {
result.log("onScrollingStop %s", inst.values.scroll);
result.checkForEnds();
inst.values.speed = 0;
inst.values.absSpeed = 0;
inst.render();
result.fireOnScroll();
inst.dispatch(exports.datagrid.events.ON_SCROLL_STOP, inst.values);
result.calculateBottomOffset();
};
|
Scroll to the normalized index. Params
index
immediately
Boolean=
|
result.scrollToIndex = function scrollToIndex(index, immediately) {
result.log("scrollToIndex");
var offset = inst.getRowOffset(index);
inst.scrollModel.scrollTo(offset, immediately);
return offset;
};
|
Scroll to an item by finding it's normalized index. Params
item
immediately
Boolean=
|
result.scrollToItem = function scrollToItem(item, immediately) {
result.log("scrollToItem");
var index = inst.getNormalizedIndex(item);
if (index !== -1) {
return inst.scrollModel.scrollToIndex(index, immediately);
}
return inst.values.scroll;
};
|
If the item is above or below the viewable area, scroll till it is in view. Params
itemOrIndex
immediately
|
result.scrollIntoView = function scrollIntoView(itemOrIndex, immediately) {
result.log("scrollIntoView");
var index = typeof itemOrIndex === "number" ? itemOrIndex : inst.getNormalizedIndex(itemOrIndex), offset = inst.getRowOffset(index), rowHeight, viewHeight;
compileRowSiblings(index);
if (offset < inst.values.scroll) {
|
it is above the view. |
return inst.scrollModel.scrollTo(offset, immediately);
}
inst.updateViewportHeight();
|
always update the height before calculating. onResize is not reliable |
viewHeight = inst.getViewportHeight();
rowHeight = inst.templateModel.getTemplateHeight(inst.getData()[index]);
if (offset >= inst.values.scroll + viewHeight - rowHeight) {
|
it is below the view. |
return inst.scrollModel.scrollTo(offset - viewHeight + rowHeight, immediately);
}
|
otherwise it is in view so do nothing. |
return false;
};
function compileRowSiblings(index) {
if (inst.data[index - 1] && !inst.isCompiled(index - 1)) {
inst.forceRenderScope(index - 1);
}
if (inst.data[index + 1] && !inst.isCompiled(index + 1)) {
inst.forceRenderScope(index + 1);
}
}
function onAfterHeightsUpdated() {
if (hasScrollListener) {
result.log("onAfterHeightsUpdated force scroll to %s", inst.values.scroll);
setElementScroll(inst.values.scroll);
}
}
|
Scroll to top. Params
immediately
|
result.scrollToTop = function(immediately) {
result.log("scrollToTop");
inst.scrollModel.scrollTo(0, immediately);
};
|
Scroll to bottom. Params
immediately
|
result.scrollToBottom = function(immediately) {
result.log("scrollToBottom");
var value = inst.getContentHeight() - inst.getViewportHeight();
inst.scrollModel.scrollTo(value >= 0 ? value : 0, immediately);
};
|
calculate the scroll value for when the grid is scrolled to the bottom. |
result.calculateBottomOffset = function() {
if (inst.rowsLength) {
var i = inst.rowsLength - 1;
result.bottomOffset = bottomOffset = inst.getRowOffset(i) - inst.getViewportHeight() + inst.getRowHeight(i);
}
};
|
When the scroll value updates. Determine if we are at the top or the bottom and dispatch if so. |
result.checkForEnds = function() {
if (inst.values.scroll && inst.values.scroll >= bottomOffset) {
inst.dispatch(ux.datagrid.events.ON_SCROLL_TO_BOTTOM, inst.values.speed);
} else if (inst.values.scroll <= 0) {
inst.dispatch(ux.datagrid.events.ON_SCROLL_TO_TOP, inst.values.speed);
}
};
function destroy() {
clearTimeout(waitForStopIntv);
result.destroyLogger();
unwatchSetup();
if (setup) {
result.removeScrollListener();
result.removeTouchEvents();
}
result = null;
inst = null;
}
|
Wait till the grid is ready before we setup our listeners. |
unwatchSetup = inst.scope.$on(exports.datagrid.events.ON_READY, setupScrolling);
inst.unwatchers.push(inst.scope.$on(exports.datagrid.events.ON_AFTER_HEIGHTS_UPDATED, onAfterHeightsUpdated));
inst.unwatchers.push(inst.scope.$on(exports.datagrid.events.ON_BEFORE_RESET, onBeforeReset));
inst.unwatchers.push(inst.scope.$on(exports.datagrid.events.ON_AFTER_RESET, onAfterReset));
inst.unwatchers.push(inst.scope.$on(ux.datagrid.events.ON_RENDER_AFTER_DATA_CHANGE, result.calculateBottomOffset));
result.destroy = destroy;
inst.scrollModel = result;
|
all models should try not to pollute the main model to keep it clean. |
return inst;
};
exports.datagrid.coreAddons.push(exports.datagrid.coreAddons.scrollModel);
/*global angular */
|
exports.datagrid.coreAddons.templateModel = function templateModel(inst) {
"use strict";
var tplNameRx = /\#{3}[\w\d\W]+\#{3}/gi;
var includeTplRx = /\#{3}include:([\w\d\W]+)\#{3}/gi;
var uncompiledRx = /uncompiled\s?/;
function trim(str) {
|
|
remove newline / carriage return |
str = str.replace(/\n/g, "");
|
remove whitespace (space and tabs) before tags |
str = str.replace(/[\t ]+</g, "<");
|
remove whitespace between tags |
str = str.replace(/>[\t ]+</g, "><");
|
remove whitespace after tags |
str = str.replace(/>[\t ]+$/g, ">");
return str;
}
inst.templateModel = function() {
var templates = [], totalHeight, defaultName = "default", result = exports.logWrapper("templateModel", {}, "teal", inst), forcedTemplates = [], templatesKey, rowHeightsDirty = false, overrideRowHeights, options = extend({}, inst.options.templateModel);
function getTemplatesKey() {
if (!templatesKey) {
templatesKey = "$$template_" + inst.uid;
}
return templatesKey;
}
function createTemplates() {
result.log("createTemplates");
var i, scriptTemplates = inst.element[0].getElementsByTagName("script"), len = scriptTemplates.length;
if (!len && !templates.length) {
inst.throwError(exports.errors.E1102);
}
for (i = 0; i < len; i += 1) {
createTemplateFromScriptTemplate(scriptTemplates[i]);
}
|
remove the script templates. |
while (scriptTemplates.length) {
inst.element[0].removeChild(scriptTemplates[0]);
}
}
function createTemplateFromScriptTemplate(scriptTemplate) {
var name = getScriptTemplateAttribute(scriptTemplate, "template-name") || defaultName, base = getScriptTemplateAttribute(scriptTemplate, "template-base") || null, itemName = getScriptTemplateAttribute(scriptTemplate, "template-item");
return createTemplate(trim(angular.element(scriptTemplate).html()), name, itemName, base);
}
function createTemplatesFromData(templateData) {
exports.each(templateData, function(tpl) {
createTemplate(tpl.template, tpl.name, tpl.item, tpl.base);
});
}
function createTemplate(template, name, itemName, base) {
var originalTemplate = template, wrapper = document.createElement("div"), templateData;
wrapper.className = "grid-template-wrapper";
template = result.prepTemplate(name, template, base);
template = angular.element(template)[0];
if (!base) {
template.className += " " + inst.options.rowClass + " " + inst.options.uncompiledClass + " {{$status}}";
}
template.setAttribute("template", name);
inst.getContent()[0].appendChild(wrapper);
wrapper.appendChild(template);
template = trim(wrapper.innerHTML);
templateData = {
name: name,
item: itemName,
template: template,
originalTemplate: originalTemplate,
height: calculateRowHeight(wrapper.children[0])
};
result.log("template: %s %o", name, templateData);
if (!templateData.height) {
if (inst.element.css("display") === "none") {
result.warn("Datagrid was intialized with a display:'none' value. Templates are unable to calculate heights. Grid will not render correctly.");
} else if (!inst.element[0].offsetHeight) {
inst.throwError(exports.errors.E1000);
} else {
inst.throwError(exports.errors.E1101);
}
}
templates[templateData.name] = templateData;
templates.push(templateData);
inst.getContent()[0].removeChild(wrapper);
totalHeight = 0;
|
reset cached value. |
return templateData;
}
function prepTemplate(name, templateStr, base) {
var str = "", baseTemplate;
if (base) {
baseTemplate = result.getTemplateByName(base);
str = baseTemplate.originalTemplate;
str = str.replace(new RegExp("#{3}" + name + "#{3}", "gi"), templateStr);
return str;
} else if (templateStr.indexOf("###include:") !== -1) {
return templateStr.replace(includeTplRx, function(m, tplName) {
var tpl = result.getTemplateByName(tplName);
return tpl && tpl.template.replace(uncompiledRx, "") || "";
});
}
return templateStr.replace(tplNameRx, "");
}
function getScriptTemplateAttribute(scriptTemplate, attrStr) {
var node = scriptTemplate.attributes["data-" + attrStr] || scriptTemplate.attributes[attrStr];
return node && node.value || "";
}
function getTemplates() {
return templates;
}
|
Use the data object from each item in the array to determine the template for that item. Params
data
|
result.getTemplate = function getTemplate(data) {
var tpl = data[getTemplatesKey()] || data._template;
return result.getTemplateByName(tpl);
};
|
TODO: need to make this method so it can be overwritten to look up templates a different way. |
function getTemplateName(el) {
if (el.attr || el.getAttribute) {
return el.attr ? el.attr("template") : el.getAttribute("template");
} else if (!(el instanceof HTMLElement)) {
|
el is a data not an element. |
return el[getTemplatesKey()] || el._template;
}
}
function getTemplateByName(name) {
if (templates[name]) {
return templates[name];
}
return templates[defaultName];
}
function dynamicHeights() {
var i, h;
for (i in templates) {
if (exports.util.apply(Object.prototype.hasOwnProperty, templates, [ i ])) {
h = h || templates[i].height;
if (h !== templates[i].height) {
return true;
}
}
}
return false;
}
function averageTemplateHeight() {
var i = 0, len = templates.length;
if (!totalHeight) {
while (i < len) {
totalHeight += templates[i].height;
i += 1;
}
}
return totalHeight / len;
}
function countTemplates() {
return templates.length;
}
function getTemplateHeight(item) {
var tpl = result.getTemplate(item);
return tpl ? tpl.height : 0;
}
function getHeight(list, startRowIndex, endRowIndex) {
var i = startRowIndex, height = 0;
if (!list.length) {
return 0;
}
while (i <= endRowIndex) {
height += result.getRowHeight(i);
i += 1;
}
return height;
}
function setTemplateName(item, templateName) {
var key = getTemplatesKey();
if (!exports.util.apply(Object.prototype.hasOwnProperty, item, [ key ]) && forcedTemplates.indexOf(item) === -1) {
forcedTemplates.push(item);
}
item[key] = templateName;
}
function setTemplate(itemOrIndex, newTemplateName, classes) {
result.info("setTemplate %s %s", itemOrIndex, newTemplateName);
var item;
if (typeof itemOrIndex === "number") {
item = inst.data[itemOrIndex];
clearRowHeight(itemOrIndex);
} else {
item = itemOrIndex;
}
var oldTemplate = result.getTemplate(item).name;
result.setTemplateName(item, newTemplateName);
|
needs to wait until after the digest. |
inst.flow.add(inst.dispatch, [ exports.datagrid.events.ON_ROW_TEMPLATE_CHANGE, item, oldTemplate, newTemplateName, classes ], 0);
}
|
if no value. calculate it. |
function forceRowHeight(index, value) {
overrideRowHeights[index] = value;
rowHeightsDirty = true;
}
function clearRowHeight(index) {
delete overrideRowHeights[index];
rowHeightsDirty = true;
}
function clearAllRowHeights() {
overrideRowHeights = {};
rowHeightsDirty = true;
}
function hasOverrideHeight(index) {
return !!overrideRowHeights[index];
}
function getRowHeight(index) {
var isOverride = overrideRowHeights.hasOwnProperty(index), el, actualHeight;
var tplHeight = result.getTemplateHeight(inst.data[index]);
if (options.variableRowHeights && !isOverride && inst.isCompiled(index)) {
|
dynamic heights will slow down the datagrid significantly. |
el = inst.getExistingRow(index);
if (el && el.length) {
actualHeight = el[0].offsetHeight;
if (actualHeight !== overrideRowHeights[index] && actualHeight !== tplHeight) {
el[0].style.height = actualHeight + "px";
overrideRowHeights[index] = actualHeight;
isOverride = true;
rowHeightsDirty = true;
}
} else {
return tplHeight;
}
}
|
TODO: need to reset overrideRowHeights on resize event if dynamicHeights. |
return isOverride ? overrideRowHeights[index] : tplHeight;
}
function hasVariableRowHeights() {
return !!options.variableRowHeights;
}
function hasDirtyHeights() {
return rowHeightsDirty;
}
function clearDirtyHeights() {
rowHeightsDirty = false;
}
|
Unify any height calculations for row height. Do not use this function unless you have no choice. Overuse of this function will result in poor datagrid performance. Params
el
|
function calculateRowHeight(el) {
var computedStyle = window.getComputedStyle(el);
return el.offsetHeight + parseInt(computedStyle.marginTop, 10) + parseInt(computedStyle.marginBottom, 10);
}
function updateTemplateHeights() {
|
TODO: needs unit tested. |
var i = inst.values.activeRange.min, len = inst.values.activeRange.max - i, row, tpl, rowHeight, heightCache = {};
while (i < len && !rowHeightsDirty) {
if (!overrideRowHeights.hasOwnProperty(i)) {
|
variable heights calculation is more expensive. |
if (result.hasVariableRowHeights()) {
result.getRowHeight(i);
} else {
|
much faster. exits after it finds the template. |
tpl = result.getTemplate(inst.getData()[i]);
if (!heightCache[tpl.name]) {
row = inst.getRowElm(i);
rowHeight = result.calculateRowHeight(row[0]);
if (rowHeight !== tpl.height) {
tpl.height = rowHeight;
rowHeightsDirty = true;
}
}
}
}
i += 1;
}
if (rowHeightsDirty) {
inst.updateHeights();
clearDirtyHeights();
}
}
function clearTemplate(item) {
delete item[getTemplatesKey()];
}
function clearForcedTemplates() {
exports.each(forcedTemplates, clearTemplate);
forcedTemplates.length = 0;
}
function destroy() {
clearForcedTemplates();
result.destroyLogger();
result = null;
templates.length = 0;
templates = null;
forcedTemplates = null;
}
result.defaultName = defaultName;
result.prepTemplate = prepTemplate;
result.createTemplates = createTemplates;
result.createTemplatesFromData = createTemplatesFromData;
result.getTemplates = getTemplates;
result.getTemplateName = getTemplateName;
result.getTemplateByName = getTemplateByName;
result.calculateRowHeight = calculateRowHeight;
result.templateCount = countTemplates;
result.dynamicHeights = dynamicHeights;
result.averageTemplateHeight = averageTemplateHeight;
result.getHeight = getHeight;
result.getTemplateHeight = getTemplateHeight;
result.getRowHeight = getRowHeight;
result.hasDirtyHeights = hasDirtyHeights;
result.clearDirtyHeights = clearDirtyHeights;
result.hasVariableRowHeights = hasVariableRowHeights;
result.hasOverrideHeight = hasOverrideHeight;
result.forceRowHeight = forceRowHeight;
result.clearRowHeight = clearRowHeight;
result.clearAllRowHeights = clearAllRowHeights;
result.setTemplate = setTemplate;
result.setTemplateName = setTemplateName;
result.updateTemplateHeights = updateTemplateHeights;
result.getTemplatesKey = getTemplatesKey;
result.destroy = destroy;
return result;
}();
return inst.templateModel;
};
exports.datagrid.coreAddons.push(exports.datagrid.coreAddons.templateModel);
}(this.ux = this.ux || {}, function() {return this;}()));
|