diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..37cee087 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +admin/* linguist-vendored +web/* linguist-vendored +*.css linguist-language=Python +*.js linguist-language=Python diff --git a/.gitignore b/.gitignore index 9998bb43..9a3a0b4a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,44 +1,14 @@ -*.py[cod] - -# C extensions -*.c -*.so - -# Jython -*$py.class - -# Packages -*.egg -*.egg-info -dist -build -eggs -parts -var -sdist -develop-eggs -.installed.cfg -lib -lib64 - -# Installer logs -pip-log.txt - -# Unit test / coverage reports -.coverage -.tox -nosetests.xml -htmlcov -*.dat - -# Docs -doc/_build - -# Translations -*.mo - -# Idea +# Logs +logs +*.log +*_build +*_static +*_templates +################# +## JetBrains +################# .idea + .vscode # System @@ -50,10 +20,17 @@ doc/_build # VIM temp files *~ -# Log -*.log -*.log.1 -logs -*_build -*_static -*_templates \ No newline at end of file +################# +## Sublime Text +################# +*.sublime-* + +############ +## Windows +############ + +# Windows image file caches +Thumbs.db + +# Folder config file +Desktop.ini \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..76f32931 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,33 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +## [Unreleased] +### Added +- None. + +### Changed +- None. + +### Fixed +- fixed energy category names and units issue in EnergyItem reports. + +### Removed +- None. + +## [v1.0.5] - 2021-02-23 +### Added +- None. + +### Changed +- None. + +### Fixed +- None. + +### Removed +- None. + +[Unreleased]: https://github.com/MyEMS/myems/compare/v1.0.5...HEAD +[v1.0.5]: https://github.com/MyEMS/myems/releases/tag/v1.0.5 diff --git a/README.md b/README.md index f8ece503..c437decd 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ MyEMS项目由下列组件构成: ### MyEMS Web APP -参考[myems-web](https://github.com/myems/myems-web.git) 获取更多信息。 +参考[web](./web/README.md) 获取更多信息。 ### MyEMS 管理面板 @@ -42,7 +42,7 @@ MyEMS项目由下列组件构成: ### MyEMS 数据接口API -参考仓库[myems-api](https://github.com/myems/myems-api.git) 获取更多信息。 +参考仓库[myems-api](https://github.com/MyEMS/myems-api.git) 获取更多信息。 ### MyEMS MQTT 数据发布服务 (从MyEMS转发到第三方) @@ -171,3 +171,11 @@ MyEMS项目由下列组件构成: | 现场技术支持服务 | ❌ | ✔️ | | 投标技术支持服务 | ❌ | ✔️ | | 二次开发技术支持服务 | ❌ | ✔️ | + + +## MyEMS镜像 + +[1]. http://github.com/MyEMS/myems + +[2]. http://gitee.com/MyEMS/myems + diff --git a/README_DE.md b/README_DE.md index 466a5c53..21595302 100644 --- a/README_DE.md +++ b/README_DE.md @@ -17,7 +17,7 @@ Dieses Projekt besteht aus folgenden Komponenten: ### MyEMS Web APP -Weitere Informationen finden Sie im Repository [myems-web](https://github.com/myems/myems-web.git). +Weitere Informationen finden Sie [web](./web/README.md). ### MyEMS Admin Panel @@ -41,7 +41,7 @@ Weitere Informationen finden Sie im [myems-aggregation](./myems-aggregation/READ ### MyEMS API -Weitere Informationen finden Sie im Repository [myems-api](https://github.com/myems/myems-api.git). +Weitere Informationen finden Sie im Repository [myems-api](https://github.com/MyEMS/myems-api.git). ### MyEMS MQTT Data Publisher Service (transmit data from MyEMS to 3rd Party) @@ -173,3 +173,10 @@ Weitere Informationen finden Sie im [myems-modbus-tcp](./myems-modbus-tcp/README | Onsite Customer Support | ❌ | ✔️ | | Bidding Support Service | ❌ | ✔️ | | Customize Support Service | ❌ | ✔️ | + +## MyEMS Spiegel + +[1]. http://github.com/MyEMS/myems + +[2]. http://gitee.com/MyEMS/myems + diff --git a/README_EN.md b/README_EN.md index e0dea772..c3db6bb5 100644 --- a/README_EN.md +++ b/README_EN.md @@ -17,7 +17,7 @@ This project is compose of following components: ### MyEMS Web APP -See the repository [myems-web](https://github.com/myems/myems-web.git) for more information. +See the repository [web](./web/README.md) for more information. ### MyEMS Admin Panel @@ -41,7 +41,7 @@ See [myems-aggregation](./myems-aggregation/README.md) for more information. ### MyEMS API -See the repository [myems-api](https://github.com/myems/myems-api.git) for more information. +See the repository [myems-api](https://github.com/MyEMS/myems-api.git) for more information. ### MyEMS MQTT Data Publisher Service (transmit data from MyEMS to 3rd Party) @@ -173,3 +173,10 @@ See [myems-modbus-tcp](./myems-modbus-tcp/README.md) for more information. | Onsite Customer Support | ❌ | ✔️ | | Bidding Support Service | ❌ | ✔️ | | Customize Support Service | ❌ | ✔️ | + +## MyEMS Mirrors + +[1]. http://github.com/MyEMS/myems + +[2]. http://gitee.com/MyEMS/myems + diff --git a/admin/.gitignore b/admin/.gitignore new file mode 100644 index 00000000..1a2a2212 --- /dev/null +++ b/admin/.gitignore @@ -0,0 +1,43 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/angular,vscode +# Edit at https://www.toptal.com/developers/gitignore?templates=angular,vscode + +### Angular ### +## Angular ## +# compiled output +tmp/ +app/**/*.js +app/**/*.js.map + +# dependencies +node_modules/ +bower_components/ + +# IDEs and editors +.idea/ + +# misc +.sass-cache/ +connect.lock/ +coverage/ +libpeerconnection.log/ +npm-debug.log +testem.log +typings/ + +# e2e +e2e/*.js +e2e/*.map + +# System Files +.DS_Store/ + +### vscode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# End of https://www.toptal.com/developers/gitignore/api/angular,vscode \ No newline at end of file diff --git a/admin/README.md b/admin/README.md index 2c9dfa13..d11a10ed 100644 --- a/admin/README.md +++ b/admin/README.md @@ -21,43 +21,58 @@ refer to http://nginx.org/en/docs/install.html * Configure NGINX ``` - $ sudo nano /etc/nginx/nginx.conf +$ sudo nano /etc/nginx/nginx.conf ``` -Add another new 'server' section, add values as below: +In the 'http' section, add some directives: ``` - server { - listen 8001; - server_name myems-admin; - location / { - root /var/www/html/admin; - index index.html index.htm; - } - -- To avoid CORS issue, use Nginx to proxy myems-api to path /api - -- Add another location /api in 'server ', replace demo address http://127.0.0.1:8000/ with actual url - location /api { - proxy_pass http://127.0.0.1:8000/; - proxy_connect_timeout 75; - proxy_read_timeout 600; - send_timeout 600; - } - } +http{ + client_header_timeout 600; + client_max_body_size 512M; + gzip on; + gzip_min_length 512; + gzip_proxied any; + gzip_types *; + gzip_vary on; + ... + +} +``` + +Add a new 'server' section with direstives as below: +``` + server { + listen 8001; + server_name myems-admin; + location / { + root /var/www/html/admin; + index index.html index.htm; + } + -- To avoid CORS issue, use Nginx to proxy myems-api to path /api + -- Add another location /api in 'server ', replace demo address http://127.0.0.1:8000/ with actual url + location /api { + proxy_pass http://127.0.0.1:8000/; + proxy_connect_timeout 75; + proxy_read_timeout 600; + send_timeout 600; + } + } ``` * Download myems-admin ``` $ cd ~ -$ git clone https://github.com/myems/myems.git +$ git clone https://github.com/MyEMS/myems.git ``` * Install myems-admin : If the server can not connect to the internet, please compress the myems/admin folder and upload it to the server and extract it to ~/myems/admin ``` - $ sudo cp -r ~/myems/admin /var/www/html/admin - $ sudo chmod 0755 -R /var/www/html/admin +$ sudo cp -r ~/myems/admin /var/www/html/admin +$ sudo chmod 0755 -R /var/www/html/admin ``` Check the config file and change it if necessary: ``` - $ sudo nano /var/www/html/admin/app/api.js +$ sudo nano /var/www/html/admin/app/api.js ``` ## NOTE: diff --git a/admin/js/plugins/nggrid/build/ng-grid.debug.js b/admin/js/plugins/nggrid/build/ng-grid.debug.js new file mode 100644 index 00000000..b2e373cd --- /dev/null +++ b/admin/js/plugins/nggrid/build/ng-grid.debug.js @@ -0,0 +1,3298 @@ +/*********************************************** +* ng-grid JavaScript Library +* Authors: https://github.com/angular-ui/ng-grid/blob/master/README.md +* License: MIT (http://www.opensource.org/licenses/mit-license.php) +* Compiled At: 03/29/2013 17:16 +***********************************************/ +(function(window, $) { +'use strict'; +// the # of rows we want to add to the top and bottom of the rendered grid rows +var EXCESS_ROWS = 6; +var SCROLL_THRESHOLD = 4; +var ASC = "asc"; +// constant for sorting direction +var DESC = "desc"; +// constant for sorting direction +var NG_FIELD = '_ng_field_'; +var NG_DEPTH = '_ng_depth_'; +var NG_HIDDEN = '_ng_hidden_'; +var NG_COLUMN = '_ng_column_'; +var CUSTOM_FILTERS = /CUSTOM_FILTERS/g; +var COL_FIELD = /COL_FIELD/g; +var DISPLAY_CELL_TEMPLATE = /DISPLAY_CELL_TEMPLATE/g; +var EDITABLE_CELL_TEMPLATE = /EDITABLE_CELL_TEMPLATE/g; +var TEMPLATE_REGEXP = /<.+>/; +window.ngGrid = {}; +window.ngGrid.i18n = {}; + +// Declare app level module which depends on filters, and services +var ngGridServices = angular.module('ngGrid.services', []); +var ngGridDirectives = angular.module('ngGrid.directives', []); +var ngGridFilters = angular.module('ngGrid.filters', []); +// initialization of services into the main module +angular.module('ngGrid', ['ngGrid.services', 'ngGrid.directives', 'ngGrid.filters']); +//set event binding on the grid so we can select using the up/down keys +var ngMoveSelectionHandler = function($scope, elm, evt, grid) { + if ($scope.selectionProvider.selectedItems === undefined) { + return true; + } + var charCode = evt.which || evt.keyCode, + newColumnIndex, + lastInRow = false, + firstInRow = false, + rowIndex = $scope.selectionProvider.lastClickedRow.rowIndex, + visibleCols = $scope.columns.filter(function(c) { return c.visible; }), + pinnedCols = $scope.columns.filter(function(c) { return c.pinned; }); + + if ($scope.col) { + newColumnIndex = visibleCols.indexOf($scope.col); + } + if(charCode != 37 && charCode != 38 && charCode != 39 && charCode != 40 && charCode != 9 && charCode != 13){ + return true; + } + + if($scope.enableCellSelection){ + if(charCode == 9){ //tab key + evt.preventDefault(); + } + var focusedOnFirstColumn = $scope.showSelectionCheckbox ? $scope.col.index == 1 : $scope.col.index == 0; + var focusedOnFirstVisibleColumns = $scope.$index == 1 || $scope.$index == 0; + var focusedOnLastVisibleColumns = $scope.$index == ($scope.renderedColumns.length - 1) || $scope.$index == ($scope.renderedColumns.length - 2); + var focusedOnLastColumn = visibleCols.indexOf($scope.col) == (visibleCols.length - 1); + var focusedOnLastPinnedColumn = pinnedCols.indexOf($scope.col) == (pinnedCols.length - 1); + + if (charCode == 37 || charCode == 9 && evt.shiftKey) { + var scrollTo = 0; + if (!focusedOnFirstColumn) { + newColumnIndex -= 1; + } + if (focusedOnFirstVisibleColumns) { + if(focusedOnFirstColumn && charCode == 9 && evt.shiftKey){ + scrollTo = grid.$canvas.width(); + newColumnIndex = visibleCols.length - 1; + firstInRow = true; + } else { + scrollTo = grid.$viewport.scrollLeft() - $scope.col.width; + } + } else if (pinnedCols.length > 0) { + scrollTo = grid.$viewport.scrollLeft() - visibleCols[newColumnIndex].width; + } + grid.$viewport.scrollLeft(scrollTo); + + } else if(charCode == 39 || charCode == 9 && !evt.shiftKey){ + if (focusedOnLastVisibleColumns) { + if(focusedOnLastColumn && charCode == 9 && !evt.shiftKey){ + grid.$viewport.scrollLeft(0); + newColumnIndex = $scope.showSelectionCheckbox ? 1 : 0; + lastInRow = true; + } else { + + grid.$viewport.scrollLeft(grid.$viewport.scrollLeft() + $scope.col.width); + } + } else if (focusedOnLastPinnedColumn) { + grid.$viewport.scrollLeft(0); + } + if(!focusedOnLastColumn){ + newColumnIndex += 1; + } + } + } + + var items; + if ($scope.configGroups.length > 0) { + items = grid.rowFactory.parsedData.filter(function (row) { + return !row.isAggRow; + }); + } else { + items = grid.filteredRows; + } + + var offset = 0; + if(rowIndex != 0 && (charCode == 38 || charCode == 13 && evt.shiftKey || charCode == 9 && evt.shiftKey && firstInRow)){ //arrow key up or shift enter or tab key and first item in row + offset = -1; + } else if(rowIndex != items.length - 1 && (charCode == 40 || charCode == 13 && !evt.shiftKey || charCode == 9 && lastInRow)){//arrow key down, enter, or tab key and last item in row? + offset = 1; + } + + if (offset) { + var r = items[rowIndex + offset]; + if (r.beforeSelectionChange(r, evt)) { + r.continueSelection(evt); + $scope.$emit('ngGridEventDigestGridParent'); + + if ($scope.selectionProvider.lastClickedRow.renderedRowIndex >= $scope.renderedRows.length - EXCESS_ROWS - 2) { + grid.$viewport.scrollTop(grid.$viewport.scrollTop() + $scope.rowHeight); + } else if ($scope.selectionProvider.lastClickedRow.renderedRowIndex <= EXCESS_ROWS + 2) { + grid.$viewport.scrollTop(grid.$viewport.scrollTop() - $scope.rowHeight); + } + } + } + + if($scope.enableCellSelection){ + setTimeout(function(){ + $scope.domAccessProvider.focusCellElement($scope, $scope.renderedColumns.indexOf(visibleCols[newColumnIndex])); + },3); + } + return false; +}; + +if (!String.prototype.trim) { + String.prototype.trim = function() { + return this.replace(/^\s+|\s+$/g, ''); + }; +} +if (!Array.prototype.indexOf) { + Array.prototype.indexOf = function(elt /*, from*/) { + var len = this.length >>> 0; + var from = Number(arguments[1]) || 0; + from = (from < 0) ? Math.ceil(from) : Math.floor(from); + if (from < 0) { + from += len; + } + for (; from < len; from++) { + if (from in this && this[from] === elt) { + return from; + } + } + return -1; + }; +} +if (!Array.prototype.filter) { + Array.prototype.filter = function(fun /*, thisp */) { + "use strict"; + var t = Object(this); + var len = t.length >>> 0; + if (typeof fun !== "function") { + throw new TypeError(); + } + var res = []; + var thisp = arguments[1]; + for (var i = 0; i < len; i++) { + if (i in t) { + var val = t[i]; // in case fun mutates this + if (fun.call(thisp, val, i, t)) { + res.push(val); + } + } + } + return res; + }; +} +ngGridFilters.filter('checkmark', function() { + return function(input) { + return input ? '\u2714' : '\u2718'; + }; +}); +ngGridFilters.filter('ngColumns', function() { + return function(input) { + return input.filter(function(col) { + return !col.isAggCol; + }); + }; +}); +ngGridServices.factory('$domUtilityService',['$utilityService', function($utils) { + var domUtilityService = {}; + var regexCache = {}; + var getWidths = function() { + var $testContainer = $('
'); + $testContainer.appendTo('body'); + // 1. Run all the following measurements on startup! + //measure Scroll Bars + $testContainer.height(100).width(100).css("position", "absolute").css("overflow", "scroll"); + $testContainer.append(''); + domUtilityService.ScrollH = ($testContainer.height() - $testContainer[0].clientHeight); + domUtilityService.ScrollW = ($testContainer.width() - $testContainer[0].clientWidth); + $testContainer.empty(); + //clear styles + $testContainer.attr('style', ''); + //measure letter sizes using a pretty typical font size and fat font-family + $testContainer.append('M'); + domUtilityService.LetterW = $testContainer.children().first().width(); + $testContainer.remove(); + }; + domUtilityService.eventStorage = {}; + domUtilityService.AssignGridContainers = function($scope, rootEl, grid) { + grid.$root = $(rootEl); + //Headers + grid.$topPanel = grid.$root.find(".ngTopPanel"); + grid.$groupPanel = grid.$root.find(".ngGroupPanel"); + grid.$headerContainer = grid.$topPanel.find(".ngHeaderContainer"); + $scope.$headerContainer = grid.$headerContainer; + + grid.$headerScroller = grid.$topPanel.find(".ngHeaderScroller"); + grid.$headers = grid.$headerScroller.children(); + //Viewport + grid.$viewport = grid.$root.find(".ngViewport"); + //Canvas + grid.$canvas = grid.$viewport.find(".ngCanvas"); + //Footers + grid.$footerPanel = grid.$root.find(".ngFooterPanel"); + + $scope.$watch(function () { + return grid.$viewport.scrollLeft(); + }, function (newLeft) { + return grid.$headerContainer.scrollLeft(newLeft); + }); + domUtilityService.UpdateGridLayout($scope, grid); + }; + domUtilityService.getRealWidth = function (obj) { + var width = 0; + var props = { visibility: "hidden", display: "block" }; + var hiddenParents = obj.parents().andSelf().not(':visible'); + $.swap(hiddenParents[0], props, function () { + width = obj.outerWidth(); + }); + return width; + }; + domUtilityService.UpdateGridLayout = function($scope, grid) { + //catch this so we can return the viewer to their original scroll after the resize! + var scrollTop = grid.$viewport.scrollTop(); + grid.elementDims.rootMaxW = grid.$root.width(); + if (grid.$root.is(':hidden')) { + grid.elementDims.rootMaxW = domUtilityService.getRealWidth(grid.$root); + } + grid.elementDims.rootMaxH = grid.$root.height(); + //check to see if anything has changed + grid.refreshDomSizes(); + $scope.adjustScrollTop(scrollTop, true); //ensure that the user stays scrolled where they were + }; + domUtilityService.numberOfGrids = 0; + domUtilityService.BuildStyles = function($scope, grid, digest) { + var rowHeight = grid.config.rowHeight, + $style = grid.$styleSheet, + gridId = grid.gridId, + css, + cols = $scope.columns, + sumWidth = 0; + + if (!$style) { + $style = $('#' + gridId); + if (!$style[0]) { + $style = $("").appendTo(grid.$root); + } + } + $style.empty(); + var trw = $scope.totalRowWidth(); + css = "." + gridId + " .ngCanvas { width: " + trw + "px; }" + + "." + gridId + " .ngRow { width: " + trw + "px; }" + + "." + gridId + " .ngCanvas { width: " + trw + "px; }" + + "." + gridId + " .ngHeaderScroller { width: " + (trw + domUtilityService.ScrollH + 2) + "px}"; + for (var i = 0; i < cols.length; i++) { + var col = cols[i]; + if (col.visible !== false) { + var colLeft = col.pinned ? grid.$viewport.scrollLeft() + sumWidth : sumWidth; + css += "." + gridId + " .col" + i + " { width: " + col.width + "px; left: " + colLeft + "px; height: " + rowHeight + "px }" + + "." + gridId + " .colt" + i + " { width: " + col.width + "px; }"; + sumWidth += col.width; + } + }; + if ($utils.isIe) { // IE + $style[0].styleSheet.cssText = css; + } else { + $style[0].appendChild(document.createTextNode(css)); + } + grid.$styleSheet = $style; + if (digest) { + $scope.adjustScrollLeft(grid.$viewport.scrollLeft()); + domUtilityService.digest($scope); + } + }; + domUtilityService.setColLeft = function(col, colLeft, grid) { + if (grid.$styleSheet) { + var regex = regexCache[col.index]; + if (!regex) { + regex = regexCache[col.index] = new RegExp("\.col" + col.index + " \{ width: [0-9]+px; left: [0-9]+px"); + } + var str = grid.$styleSheet.html(); + var newStr = str.replace(regex, "\.col" + col.index + " \{ width: " + col.width + "px; left: " + colLeft + "px"); + if ($utils.isIe) { // IE + setTimeout(function() { + grid.$styleSheet.html(newStr); + }); + } else { + grid.$styleSheet.html(newStr); + } + } + }; + domUtilityService.setColLeft.immediate = 1; + domUtilityService.RebuildGrid = function($scope, grid){ + domUtilityService.UpdateGridLayout($scope, grid); + if (grid.config.maintainColumnRatios) { + grid.configureColumnWidths(); + } + $scope.adjustScrollLeft(grid.$viewport.scrollLeft()); + domUtilityService.BuildStyles($scope, grid, true); + }; + + domUtilityService.digest = function($scope) { + if (!$scope.$root.$$phase) { + $scope.$digest(); + } + }; + domUtilityService.ScrollH = 17; // default in IE, Chrome, & most browsers + domUtilityService.ScrollW = 17; // default in IE, Chrome, & most browsers + domUtilityService.LetterW = 10; + getWidths(); + return domUtilityService; +}]); +ngGridServices.factory('$sortService', ['$parse', function($parse) { + var sortService = {}; + sortService.colSortFnCache = {}; // cache of sorting functions. Once we create them, we don't want to keep re-doing it + // this takes an piece of data from the cell and tries to determine its type and what sorting + // function to use for it + // @item - the cell data + sortService.guessSortFn = function(item) { + var itemType = typeof(item); + //check for numbers and booleans + switch (itemType) { + case "number": + return sortService.sortNumber; + case "boolean": + return sortService.sortBool; + case "string": + // if number string return number string sort fn. else return the str + return item.match(/^-?[£$¤]?[\d,.]+%?$/) ? sortService.sortNumberStr : sortService.sortAlpha; + default: + //check if the item is a valid Date + if (Object.prototype.toString.call(item) === '[object Date]') { + return sortService.sortDate; + } else { + //finally just sort the basic sort... + return sortService.basicSort; + } + } + }; + //#region Sorting Functions + sortService.basicSort = function(a, b) { + if (a == b) { + return 0; + } + if (a < b) { + return -1; + } + return 1; + }; + sortService.sortNumber = function(a, b) { + return a - b; + }; + sortService.sortNumberStr = function(a, b) { + var numA, numB, badA = false, badB = false; + numA = parseFloat(a.replace(/[^0-9.-]/g, '')); + if (isNaN(numA)) { + badA = true; + } + numB = parseFloat(b.replace(/[^0-9.-]/g, '')); + if (isNaN(numB)) { + badB = true; + } + // we want bad ones to get pushed to the bottom... which effectively is "greater than" + if (badA && badB) { + return 0; + } + if (badA) { + return 1; + } + if (badB) { + return -1; + } + return numA - numB; + }; + sortService.sortAlpha = function(a, b) { + var strA = a.toLowerCase(), + strB = b.toLowerCase(); + return strA == strB ? 0 : (strA < strB ? -1 : 1); + }; + sortService.sortDate = function(a, b) { + var timeA = a.getTime(), + timeB = b.getTime(); + return timeA == timeB ? 0 : (timeA < timeB ? -1 : 1); + }; + sortService.sortBool = function(a, b) { + if (a && b) { + return 0; + } + if (!a && !b) { + return 0; + } else { + return a ? 1 : -1; + } + }; + //#endregion + // the core sorting logic trigger + sortService.sortData = function(sortInfo, data /*datasource*/) { + // first make sure we are even supposed to do work + if (!data || !sortInfo) { + return; + } + var l = sortInfo.fields.length, + order = sortInfo.fields, + col, + direction, + // IE9 HACK.... omg, I can't reference data array within the sort fn below. has to be a separate reference....!!!! + d = data.slice(0); + //now actually sort the data + data.sort(function (itemA, itemB) { + var tem = 0, + indx = 0, + sortFn; + while (tem == 0 && indx < l) { + // grab the metadata for the rest of the logic + col = sortInfo.columns[indx]; + direction = sortInfo.directions[indx], + sortFn = sortService.getSortFn(col, d); + + var propA = $parse(order[indx])(itemA); + var propB = $parse(order[indx])(itemB); + // we want to allow zero values to be evaluated in the sort function + if ((!propA && propA != 0) || (!propB && propB != 0)) { + // we want to force nulls and such to the bottom when we sort... which effectively is "greater than" + if (!propB && !propA) { + tem = 0; + } else if (!propA) { + tem = 1; + } else if (!propB) { + tem = -1; + } + } else { + tem = sortFn(propA, propB); + } + indx++; + } + //made it this far, we don't have to worry about null & undefined + if (direction === ASC) { + return tem; + } else { + return 0 - tem; + } + }); + }; + sortService.Sort = function(sortInfo, data) { + if (sortService.isSorting) { + return; + } + sortService.isSorting = true; + sortService.sortData(sortInfo, data); + sortService.isSorting = false; + }; + sortService.getSortFn = function(col, data) { + var sortFn = undefined, item; + //see if we already figured out what to use to sort the column + if (sortService.colSortFnCache[col.field]) { + sortFn = sortService.colSortFnCache[col.field]; + } else if (col.sortingAlgorithm != undefined) { + sortFn = col.sortingAlgorithm; + sortService.colSortFnCache[col.field] = col.sortingAlgorithm; + } else { // try and guess what sort function to use + item = data[0]; + if (!item) { + return sortFn; + } + sortFn = sortService.guessSortFn($parse(col.field)(item)); + //cache it + if (sortFn) { + sortService.colSortFnCache[col.field] = sortFn; + } else { + // we assign the alpha sort because anything that is null/undefined will never get passed to + // the actual sorting function. It will get caught in our null check and returned to be sorted + // down to the bottom + sortFn = sortService.sortAlpha; + } + } + return sortFn; + }; + return sortService; +}]); + +ngGridServices.factory('$utilityService', ['$parse', function ($parse) { + var funcNameRegex = /function (.{1,})\(/; + var utils = { + visualLength: function(node) { + var elem = document.getElementById('testDataLength'); + if (!elem) { + elem = document.createElement('SPAN'); + elem.id = "testDataLength"; + elem.style.visibility = "hidden"; + document.body.appendChild(elem); + } + $(elem).css('font', $(node).css('font')); + elem.innerHTML = $(node).text(); + return elem.offsetWidth; + }, + forIn: function(obj, action) { + for (var prop in obj) { + if (obj.hasOwnProperty(prop)) { + action(obj[prop], prop); + } + } + }, + evalProperty: function (entity, path) { + return $parse(path)(entity); + }, + endsWith: function(str, suffix) { + if (!str || !suffix || typeof str != "string") { + return false; + } + return str.indexOf(suffix, str.length - suffix.length) !== -1; + }, + isNullOrUndefined: function(obj) { + if (obj === undefined || obj === null) { + return true; + } + return false; + }, + getElementsByClassName: function(cl) { + var retnode = []; + var myclass = new RegExp('\\b' + cl + '\\b'); + var elem = document.getElementsByTagName('*'); + for (var i = 0; i < elem.length; i++) { + var classes = elem[i].className; + if (myclass.test(classes)) { + retnode.push(elem[i]); + } + } + return retnode; + }, + newId: (function() { + var seedId = new Date().getTime(); + return function() { + return seedId += 1; + }; + })(), + seti18n: function($scope, language) { + var $langPack = window.ngGrid.i18n[language]; + for (var label in $langPack) { + $scope.i18n[label] = $langPack[label]; + } + }, + getInstanceType: function (o) { + var results = (funcNameRegex).exec(o.constructor.toString()); + return (results && results.length > 1) ? results[1] : ""; + }, + // we copy KO's ie detection here bc it isn't exported in the min versions of KO + // Detect IE versions for workarounds (uses IE conditionals, not UA string, for robustness) + ieVersion: (function() { + var version = 3, div = document.createElement('div'), iElems = div.getElementsByTagName('i'); + // Keep constructing conditional HTML blocks until we hit one that resolves to an empty fragment + while (div.innerHTML = '', + iElems[0]) ; + return version > 4 ? version : undefined; + })() + }; + + $.extend(utils, { + isIe: (function() { + return utils.ieVersion !== undefined; + })() + }); + return utils; +}]); +var ngAggregate = function (aggEntity, rowFactory, rowHeight) { + var self = this; + self.rowIndex = 0; + self.offsetTop = self.rowIndex * rowHeight; + self.entity = aggEntity; + self.label = aggEntity.gLabel; + self.field = aggEntity.gField; + self.depth = aggEntity.gDepth; + self.parent = aggEntity.parent; + self.children = aggEntity.children; + self.aggChildren = aggEntity.aggChildren; + self.aggIndex = aggEntity.aggIndex; + self.collapsed = true; + self.isAggRow = true; + self.offsetleft = aggEntity.gDepth * 25; + self.aggLabelFilter = aggEntity.aggLabelFilter; + self.toggleExpand = function() { + self.collapsed = self.collapsed ? false : true; + if (self.orig) { + self.orig.collapsed = self.collapsed; + } + self.notifyChildren(); + }; + self.setExpand = function(state) { + self.collapsed = state; + self.notifyChildren(); + }; + self.notifyChildren = function () { + var longest = Math.max(rowFactory.aggCache.length, self.children.length); + for (var i = 0; i < longest; i++) { + if (self.aggChildren[i]) { + self.aggChildren[i].entity[NG_HIDDEN] = self.collapsed; + if (self.collapsed) { + self.aggChildren[i].setExpand(self.collapsed); + } + } + if (self.children[i]) { + self.children[i][NG_HIDDEN] = self.collapsed; + } + if (i > self.aggIndex && rowFactory.aggCache[i]) { + var agg = rowFactory.aggCache[i]; + var offset = (30 * self.children.length); + agg.offsetTop = self.collapsed ? agg.offsetTop - offset : agg.offsetTop + offset; + } + }; + rowFactory.renderedChange(); + }; + self.aggClass = function() { + return self.collapsed ? "ngAggArrowCollapsed" : "ngAggArrowExpanded"; + }; + self.totalChildren = function() { + if (self.aggChildren.length > 0) { + var i = 0; + var recurse = function(cur) { + if (cur.aggChildren.length > 0) { + angular.forEach(cur.aggChildren, function(a) { + recurse(a); + }); + } else { + i += cur.children.length; + } + }; + recurse(self); + return i; + } else { + return self.children.length; + } + }; + self.copy = function () { + var ret = new ngAggregate(self.entity, rowFactory, rowHeight); + ret.orig = self; + return ret; + }; +}; +var ngColumn = function (config, $scope, grid, domUtilityService, $templateCache, $utils) { + var self = this, + colDef = config.colDef, + delay = 500, + clicks = 0, + timer = null; + self.width = colDef.width; + self.groupIndex = 0; + self.isGroupedBy = false; + self.minWidth = !colDef.minWidth ? 50 : colDef.minWidth; + self.maxWidth = !colDef.maxWidth ? 9000 : colDef.maxWidth; + self.enableCellEdit = config.enableCellEdit || colDef.enableCellEdit; + self.headerRowHeight = config.headerRowHeight; + self.displayName = colDef.displayName || colDef.field; + self.index = config.index; + self.isAggCol = config.isAggCol; + self.cellClass = colDef.cellClass; + self.sortPriority = undefined; + self.cellFilter = colDef.cellFilter ? colDef.cellFilter : ""; + self.field = colDef.field; + self.aggLabelFilter = colDef.cellFilter || colDef.aggLabelFilter; + self.visible = $utils.isNullOrUndefined(colDef.visible) || colDef.visible; + self.sortable = false; + self.resizable = false; + self.pinnable = false; + self.pinned = (config.enablePinning && colDef.pinned); + self.originalIndex = self.index; + self.groupable = $utils.isNullOrUndefined(colDef.groupable) || colDef.groupable; + if (config.enableSort) { + self.sortable = $utils.isNullOrUndefined(colDef.sortable) || colDef.sortable; + } + if (config.enableResize) { + self.resizable = $utils.isNullOrUndefined(colDef.resizable) || colDef.resizable; + } + if (config.enablePinning) { + self.pinnable = $utils.isNullOrUndefined(colDef.pinnable) || colDef.pinnable; + } + self.sortDirection = undefined; + self.sortingAlgorithm = colDef.sortFn; + self.headerClass = colDef.headerClass; + self.cursor = self.sortable ? 'pointer' : 'default'; + self.headerCellTemplate = colDef.headerCellTemplate || $templateCache.get('headerCellTemplate.html'); + self.cellTemplate = colDef.cellTemplate || $templateCache.get('cellTemplate.html').replace(CUSTOM_FILTERS, self.cellFilter ? "|" + self.cellFilter : ""); + if(self.enableCellEdit) { + self.cellEditTemplate = $templateCache.get('cellEditTemplate.html'); + self.editableCellTemplate = colDef.editableCellTemplate || $templateCache.get('editableCellTemplate.html'); + } + if (colDef.cellTemplate && !TEMPLATE_REGEXP.test(colDef.cellTemplate)) { + self.cellTemplate = $.ajax({ + type: "GET", + url: colDef.cellTemplate, + async: false + }).responseText; + } + if (self.enableCellEdit && colDef.editableCellTemplate && !TEMPLATE_REGEXP.test(colDef.editableCellTemplate)) { + self.editableCellTemplate = $.ajax({ + type: "GET", + url: colDef.editableCellTemplate, + async: false + }).responseText; + } + if (colDef.headerCellTemplate && !TEMPLATE_REGEXP.test(colDef.headerCellTemplate)) { + self.headerCellTemplate = $.ajax({ + type: "GET", + url: colDef.headerCellTemplate, + async: false + }).responseText; + } + self.colIndex = function () { + var classes = self.pinned ? "pinned " : ""; + classes += "col" + self.index + " colt" + self.index; + return classes; + }; + self.groupedByClass = function() { + return self.isGroupedBy ? "ngGroupedByIcon" : "ngGroupIcon"; + }; + self.toggleVisible = function() { + self.visible = !self.visible; + }; + self.showSortButtonUp = function() { + return self.sortable ? self.sortDirection === DESC : self.sortable; + }; + self.showSortButtonDown = function() { + return self.sortable ? self.sortDirection === ASC : self.sortable; + }; + self.noSortVisible = function() { + return !self.sortDirection; + }; + self.sort = function(evt) { + if (!self.sortable) { + return true; // column sorting is disabled, do nothing + } + var dir = self.sortDirection === ASC ? DESC : ASC; + self.sortDirection = dir; + config.sortCallback(self, evt); + return false; + }; + self.gripClick = function() { + clicks++; //count clicks + if (clicks === 1) { + timer = setTimeout(function() { + //Here you can add a single click action. + clicks = 0; //after action performed, reset counter + }, delay); + } else { + clearTimeout(timer); //prevent single-click action + config.resizeOnDataCallback(self); //perform double-click action + clicks = 0; //after action performed, reset counter + } + }; + self.gripOnMouseDown = function(event) { + if (event.ctrlKey && !self.pinned) { + self.toggleVisible(); + domUtilityService.BuildStyles($scope, grid); + return true; + } + event.target.parentElement.style.cursor = 'col-resize'; + self.startMousePosition = event.clientX; + self.origWidth = self.width; + $(document).mousemove(self.onMouseMove); + $(document).mouseup(self.gripOnMouseUp); + return false; + }; + self.onMouseMove = function(event) { + var diff = event.clientX - self.startMousePosition; + var newWidth = diff + self.origWidth; + self.width = (newWidth < self.minWidth ? self.minWidth : (newWidth > self.maxWidth ? self.maxWidth : newWidth)); + domUtilityService.BuildStyles($scope, grid); + return false; + }; + self.gripOnMouseUp = function (event) { + $(document).off('mousemove', self.onMouseMove); + $(document).off('mouseup', self.gripOnMouseUp); + event.target.parentElement.style.cursor = 'default'; + $scope.adjustScrollLeft(0); + domUtilityService.digest($scope); + return false; + }; + self.copy = function() { + var ret = new ngColumn(config, $scope, grid, domUtilityService, $templateCache); + ret.isClone = true; + ret.orig = self; + return ret; + }; + self.setVars = function (fromCol) { + self.orig = fromCol; + self.width = fromCol.width; + self.groupIndex = fromCol.groupIndex; + self.isGroupedBy = fromCol.isGroupedBy; + self.displayName = fromCol.displayName; + self.index = fromCol.index; + self.isAggCol = fromCol.isAggCol; + self.cellClass = fromCol.cellClass; + self.cellFilter = fromCol.cellFilter; + self.field = fromCol.field; + self.aggLabelFilter = fromCol.aggLabelFilter; + self.visible = fromCol.visible; + self.sortable = fromCol.sortable; + self.resizable = fromCol.resizable; + self.pinnable = fromCol.pinnable; + self.pinned = fromCol.pinned; + self.originalIndex = fromCol.originalIndex; + self.sortDirection = fromCol.sortDirection; + self.sortingAlgorithm = fromCol.sortingAlgorithm; + self.headerClass = fromCol.headerClass; + self.headerCellTemplate = fromCol.headerCellTemplate; + self.cellTemplate = fromCol.cellTemplate; + self.cellEditTemplate = fromCol.cellEditTemplate; + }; +}; + +var ngDimension = function (options) { + this.outerHeight = null; + this.outerWidth = null; + $.extend(this, options); +}; +var ngDomAccessProvider = function (grid) { + var self = this, previousColumn; + self.selectInputElement = function(elm){ + var node = elm.nodeName.toLowerCase(); + if(node == 'input' || node == 'textarea'){ + elm.select(); + } + }; + + self.focusCellElement = function($scope, index){ + if($scope.selectionProvider.lastClickedRow){ + var columnIndex = index != undefined ? index : previousColumn; + var elm = $scope.selectionProvider.lastClickedRow.clone ? $scope.selectionProvider.lastClickedRow.clone.elm : $scope.selectionProvider.lastClickedRow.elm; + if (columnIndex != undefined && elm) { + var columns = angular.element(elm[0].children).filter(function () { return this.nodeType != 8;}); //Remove html comments for IE8 + var i = Math.max(Math.min($scope.renderedColumns.length - 1, columnIndex), 0); + if(grid.config.showSelectionCheckbox && angular.element(columns[i]).scope() && angular.element(columns[i]).scope().col.index == 0){ + i = 1; //don't want to focus on checkbox + } + if (columns[i]) { + columns[i].children[0].focus(); + } + previousColumn = columnIndex; + } + } + }; + + var changeUserSelect = function(elm, value) { + elm.css({ + '-webkit-touch-callout': value, + '-webkit-user-select': value, + '-khtml-user-select': value, + '-moz-user-select': value == 'none' + ? '-moz-none' + : value, + '-ms-user-select': value, + 'user-select': value + }); + }; + + self.selectionHandlers = function($scope, elm){ + var doingKeyDown = false; + elm.bind('keydown', function(evt) { + if (evt.keyCode == 16) { //shift key + changeUserSelect(elm, 'none', evt); + return true; + } else if (!doingKeyDown) { + doingKeyDown = true; + var ret = ngMoveSelectionHandler($scope, elm, evt, grid); + doingKeyDown = false; + return ret; + } + return true; + }); + elm.bind('keyup', function(evt) { + if (evt.keyCode == 16) { //shift key + changeUserSelect(elm, 'text', evt); + } + return true; + }); + }; +}; +var ngEventProvider = function (grid, $scope, domUtilityService, $timeout) { + var self = this; + // The init method gets called during the ng-grid directive execution. + self.colToMove = undefined; + self.groupToMove = undefined; + self.assignEvents = function() { + // Here we set the onmousedown event handler to the header container. + if (grid.config.jqueryUIDraggable && !grid.config.enablePinning) { + grid.$groupPanel.droppable({ + addClasses: false, + drop: function(event) { + self.onGroupDrop(event); + } + }); + } else { + grid.$groupPanel.on('mousedown', self.onGroupMouseDown).on('dragover', self.dragOver).on('drop', self.onGroupDrop); + grid.$headerScroller.on('mousedown', self.onHeaderMouseDown).on('dragover', self.dragOver); + if (grid.config.enableColumnReordering && !grid.config.enablePinning) { + grid.$headerScroller.on('drop', self.onHeaderDrop); + } + if (grid.config.enableRowReordering) { + grid.$viewport.on('mousedown', self.onRowMouseDown).on('dragover', self.dragOver).on('drop', self.onRowDrop); + } + } + $scope.$watch('renderedColumns', function() { + $timeout(self.setDraggables); + }); + }; + self.dragStart = function(evt){ + //FireFox requires there to be dataTransfer if you want to drag and drop. + evt.dataTransfer.setData('text', ''); //cannot be empty string + }; + self.dragOver = function(evt) { + evt.preventDefault(); + }; + //For JQueryUI + self.setDraggables = function() { + if (!grid.config.jqueryUIDraggable) { + //Fix for FireFox. Instead of using jQuery on('dragstart', function) on find, we have to use addEventListeners for each column. + var columns = grid.$root.find('.ngHeaderSortColumn'); //have to iterate if using addEventListener + angular.forEach(columns, function(col){ + col.setAttribute('draggable', 'true'); + //jQuery 'on' function doesn't have dataTransfer as part of event in handler unless added to event props, which is not recommended + //See more here: http://api.jquery.com/category/events/event-object/ + if (col.addEventListener) { //IE8 doesn't have drag drop or event listeners + col.addEventListener('dragstart', self.dragStart); + } + }); + if (navigator.userAgent.indexOf("MSIE") != -1){ + //call native IE dragDrop() to start dragging + grid.$root.find('.ngHeaderSortColumn').bind('selectstart', function () { + this.dragDrop(); + return false; + }); + } + } else { + grid.$root.find('.ngHeaderSortColumn').draggable({ + helper: 'clone', + appendTo: 'body', + stack: 'div', + addClasses: false, + start: function(event) { + self.onHeaderMouseDown(event); + } + }).droppable({ + drop: function(event) { + self.onHeaderDrop(event); + } + }); + } + }; + self.onGroupMouseDown = function(event) { + var groupItem = $(event.target); + // Get the scope from the header container + if (groupItem[0].className != 'ngRemoveGroup') { + var groupItemScope = angular.element(groupItem).scope(); + if (groupItemScope) { + // set draggable events + if (!grid.config.jqueryUIDraggable) { + groupItem.attr('draggable', 'true'); + if(this.addEventListener){//IE8 doesn't have drag drop or event listeners + this.addEventListener('dragstart', self.dragStart); + } + if (navigator.userAgent.indexOf("MSIE") != -1){ + //call native IE dragDrop() to start dragging + groupItem.bind('selectstart', function () { + this.dragDrop(); + return false; + }); + } + } + // Save the column for later. + self.groupToMove = { header: groupItem, groupName: groupItemScope.group, index: groupItemScope.$index }; + } + } else { + self.groupToMove = undefined; + } + }; + self.onGroupDrop = function(event) { + event.stopPropagation(); + // clear out the colToMove object + var groupContainer; + var groupScope; + if (self.groupToMove) { + // Get the closest header to where we dropped + groupContainer = $(event.target).closest('.ngGroupElement'); // Get the scope from the header. + if (groupContainer.context.className == 'ngGroupPanel') { + $scope.configGroups.splice(self.groupToMove.index, 1); + $scope.configGroups.push(self.groupToMove.groupName); + } else { + groupScope = angular.element(groupContainer).scope(); + if (groupScope) { + // If we have the same column, do nothing. + if (self.groupToMove.index != groupScope.$index) { + // Splice the columns + $scope.configGroups.splice(self.groupToMove.index, 1); + $scope.configGroups.splice(groupScope.$index, 0, self.groupToMove.groupName); + } + } + } + self.groupToMove = undefined; + grid.fixGroupIndexes(); + } else if (self.colToMove) { + if ($scope.configGroups.indexOf(self.colToMove.col) == -1) { + groupContainer = $(event.target).closest('.ngGroupElement'); // Get the scope from the header. + if (groupContainer.context.className == 'ngGroupPanel' || groupContainer.context.className == 'ngGroupPanelDescription ng-binding') { + $scope.groupBy(self.colToMove.col); + } else { + groupScope = angular.element(groupContainer).scope(); + if (groupScope) { + // Splice the columns + $scope.removeGroup(groupScope.$index); + } + } + } + self.colToMove = undefined; + } + if (!$scope.$$phase) { + $scope.$apply(); + } + }; + //Header functions + self.onHeaderMouseDown = function(event) { + // Get the closest header container from where we clicked. + var headerContainer = $(event.target).closest('.ngHeaderSortColumn'); + // Get the scope from the header container + var headerScope = angular.element(headerContainer).scope(); + if (headerScope) { + // Save the column for later. + self.colToMove = { header: headerContainer, col: headerScope.col }; + } + }; + self.onHeaderDrop = function(event) { + if (!self.colToMove || self.colToMove.col.pinned) { + return; + } + // Get the closest header to where we dropped + var headerContainer = $(event.target).closest('.ngHeaderSortColumn'); + // Get the scope from the header. + var headerScope = angular.element(headerContainer).scope(); + if (headerScope) { + // If we have the same column, do nothing. + if (self.colToMove.col == headerScope.col) { + return; + } + // Splice the columns + $scope.columns.splice(self.colToMove.col.index, 1); + $scope.columns.splice(headerScope.col.index, 0, self.colToMove.col); + grid.fixColumnIndexes(); + // Finally, rebuild the CSS styles. + domUtilityService.BuildStyles($scope, grid, true); + // clear out the colToMove object + self.colToMove = undefined; + } + }; + // Row functions + self.onRowMouseDown = function(event) { + // Get the closest row element from where we clicked. + var targetRow = $(event.target).closest('.ngRow'); + // Get the scope from the row element + var rowScope = angular.element(targetRow).scope(); + if (rowScope) { + // set draggable events + targetRow.attr('draggable', 'true'); + // Save the row for later. + domUtilityService.eventStorage.rowToMove = { targetRow: targetRow, scope: rowScope }; + } + }; + self.onRowDrop = function(event) { + // Get the closest row to where we dropped + var targetRow = $(event.target).closest('.ngRow'); + // Get the scope from the row element. + var rowScope = angular.element(targetRow).scope(); + if (rowScope) { + // If we have the same Row, do nothing. + var prevRow = domUtilityService.eventStorage.rowToMove; + if (prevRow.scope.row == rowScope.row) { + return; + } + grid.changeRowOrder(prevRow.scope.row, rowScope.row); + grid.searchProvider.evalFilter(); + // clear out the rowToMove object + domUtilityService.eventStorage.rowToMove = undefined; + // if there isn't an apply already in progress lets start one + domUtilityService.digest(rowScope.$root); + } + }; + + self.assignGridEventHandlers = function() { + //Chrome and firefox both need a tab index so the grid can recieve focus. + //need to give the grid a tabindex if it doesn't already have one so + //we'll just give it a tab index of the corresponding gridcache index + //that way we'll get the same result every time it is run. + //configurable within the options. + if (grid.config.tabIndex === -1) { + grid.$viewport.attr('tabIndex', domUtilityService.numberOfGrids); + domUtilityService.numberOfGrids++; + } else { + grid.$viewport.attr('tabIndex', grid.config.tabIndex); + }// resize on window resize + $(window).resize(function() { + domUtilityService.RebuildGrid($scope,grid); + }); + // resize on parent resize as well. + $(grid.$root.parent()).on('resize', function() { + domUtilityService.RebuildGrid($scope, grid); + }); + }; + // In this example we want to assign grid events. + self.assignGridEventHandlers(); + self.assignEvents(); +}; + +var ngFooter = function ($scope, grid) { + $scope.maxRows = function () { + var ret = Math.max($scope.pagingOptions.totalServerItems, grid.data.length); + return ret; + }; + + $scope.multiSelect = (grid.config.enableRowSelection && grid.config.multiSelect); + $scope.selectedItemCount = grid.selectedItemCount; + $scope.maxPages = function () { + return Math.ceil($scope.maxRows() / $scope.pagingOptions.pageSize); + }; + + $scope.pageForward = function() { + var page = $scope.pagingOptions.currentPage; + if ($scope.pagingOptions.totalServerItems > 0) { + $scope.pagingOptions.currentPage = Math.min(page + 1, $scope.maxPages()); + } else { + $scope.pagingOptions.currentPage++; + } + }; + + $scope.pageBackward = function() { + var page = $scope.pagingOptions.currentPage; + $scope.pagingOptions.currentPage = Math.max(page - 1, 1); + }; + + $scope.pageToFirst = function() { + $scope.pagingOptions.currentPage = 1; + }; + + $scope.pageToLast = function() { + var maxPages = $scope.maxPages(); + $scope.pagingOptions.currentPage = maxPages; + }; + + $scope.cantPageForward = function() { + var curPage = $scope.pagingOptions.currentPage; + var maxPages = $scope.maxPages(); + if ($scope.pagingOptions.totalServerItems > 0) { + return !(curPage < maxPages); + } else { + return grid.data.length < 1; + } + + }; + $scope.cantPageToLast = function() { + if ($scope.pagingOptions.totalServerItems > 0) { + return $scope.cantPageForward(); + } else { + return true; + } + }; + + $scope.cantPageBackward = function() { + var curPage = $scope.pagingOptions.currentPage; + return !(curPage > 1); + }; +}; +///+ var values = {name: 'misko', gender: 'male'}; + var log = []; + angular.forEach(values, function(value, key){ + this.push(key + ': ' + value); + }, log); + expect(log).toEqual(['name: misko', 'gender:male']); ++ * + * @param {Object|Array} obj Object to iterate over. + * @param {Function} iterator Iterator function. + * @param {Object=} context Object to become context (`this`) for the iterator function. + * @returns {Object|Array} Reference to `obj`. + */ + + +/** + * @private + * @param {*} obj + * @return {boolean} Returns true if `obj` is an array or array-like object (NodeList, Arguments, ...) + */ +function isArrayLike(obj) { + if (!obj || (typeof obj.length !== 'number')) return false; + + // We have on object which has length property. Should we treat it as array? + if (typeof obj.hasOwnProperty != 'function' && + typeof obj.constructor != 'function') { + // This is here for IE8: it is a bogus object treat it as array; + return true; + } else { + return obj instanceof JQLite || // JQLite + (jQuery && obj instanceof jQuery) || // jQuery + toString.call(obj) !== '[object Object]' || // some browser native object + typeof obj.callee === 'function'; // arguments (on IE8 looks like regular obj) + } +} + + +function forEach(obj, iterator, context) { + var key; + if (obj) { + if (isFunction(obj)){ + for (key in obj) { + if (key != 'prototype' && key != 'length' && key != 'name' && obj.hasOwnProperty(key)) { + iterator.call(context, obj[key], key); + } + } + } else if (obj.forEach && obj.forEach !== forEach) { + obj.forEach(iterator, context); + } else if (isArrayLike(obj)) { + for (key = 0; key < obj.length; key++) + iterator.call(context, obj[key], key); + } else { + for (key in obj) { + if (obj.hasOwnProperty(key)) { + iterator.call(context, obj[key], key); + } + } + } + } + return obj; +} + +function sortedKeys(obj) { + var keys = []; + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + keys.push(key); + } + } + return keys.sort(); +} + +function forEachSorted(obj, iterator, context) { + var keys = sortedKeys(obj); + for ( var i = 0; i < keys.length; i++) { + iterator.call(context, obj[keys[i]], keys[i]); + } + return keys; +} + + +/** + * when using forEach the params are value, key, but it is often useful to have key, value. + * @param {function(string, *)} iteratorFn + * @returns {function(*, string)} + */ +function reverseParams(iteratorFn) { + return function(value, key) { iteratorFn(key, value) }; +} + +/** + * A consistent way of creating unique IDs in angular. The ID is a sequence of alpha numeric + * characters such as '012ABC'. The reason why we are not using simply a number counter is that + * the number string gets longer over time, and it can also overflow, where as the nextId + * will grow much slower, it is a string, and it will never overflow. + * + * @returns an unique alpha-numeric string + */ +function nextUid() { + var index = uid.length; + var digit; + + while(index) { + index--; + digit = uid[index].charCodeAt(0); + if (digit == 57 /*'9'*/) { + uid[index] = 'A'; + return uid.join(''); + } + if (digit == 90 /*'Z'*/) { + uid[index] = '0'; + } else { + uid[index] = String.fromCharCode(digit + 1); + return uid.join(''); + } + } + uid.unshift('0'); + return uid.join(''); +} + +/** + * @ngdoc function + * @name angular.extend + * @function + * + * @description + * Extends the destination object `dst` by copying all of the properties from the `src` object(s) + * to `dst`. You can specify multiple `src` objects. + * + * @param {Object} dst Destination object. + * @param {...Object} src Source object(s). + */ +function extend(dst) { + forEach(arguments, function(obj){ + if (obj !== dst) { + forEach(obj, function(value, key){ + dst[key] = value; + }); + } + }); + return dst; +} + +function int(str) { + return parseInt(str, 10); +} + + +function inherit(parent, extra) { + return extend(new (extend(function() {}, {prototype:parent}))(), extra); +} + + +/** + * @ngdoc function + * @name angular.noop + * @function + * + * @description + * A function that performs no operations. This function can be useful when writing code in the + * functional style. +
+ function foo(callback) { + var result = calculateResult(); + (callback || angular.noop)(result); + } ++ */ +function noop() {} +noop.$inject = []; + + +/** + * @ngdoc function + * @name angular.identity + * @function + * + * @description + * A function that returns its first argument. This function is useful when writing code in the + * functional style. + * +
+ function transformer(transformationFn, value) { + return (transformationFn || identity)(value); + }; ++ */ +function identity($) {return $;} +identity.$inject = []; + + +function valueFn(value) {return function() {return value;};} + +/** + * @ngdoc function + * @name angular.isUndefined + * @function + * + * @description + * Determines if a reference is undefined. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is undefined. + */ +function isUndefined(value){return typeof value == 'undefined';} + + +/** + * @ngdoc function + * @name angular.isDefined + * @function + * + * @description + * Determines if a reference is defined. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is defined. + */ +function isDefined(value){return typeof value != 'undefined';} + + +/** + * @ngdoc function + * @name angular.isObject + * @function + * + * @description + * Determines if a reference is an `Object`. Unlike `typeof` in JavaScript, `null`s are not + * considered to be objects. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is an `Object` but not `null`. + */ +function isObject(value){return value != null && typeof value == 'object';} + + +/** + * @ngdoc function + * @name angular.isString + * @function + * + * @description + * Determines if a reference is a `String`. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is a `String`. + */ +function isString(value){return typeof value == 'string';} + + +/** + * @ngdoc function + * @name angular.isNumber + * @function + * + * @description + * Determines if a reference is a `Number`. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is a `Number`. + */ +function isNumber(value){return typeof value == 'number';} + + +/** + * @ngdoc function + * @name angular.isDate + * @function + * + * @description + * Determines if a value is a date. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is a `Date`. + */ +function isDate(value){ + return toString.apply(value) == '[object Date]'; +} + + +/** + * @ngdoc function + * @name angular.isArray + * @function + * + * @description + * Determines if a reference is an `Array`. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is an `Array`. + */ +function isArray(value) { + return toString.apply(value) == '[object Array]'; +} + + +/** + * @ngdoc function + * @name angular.isFunction + * @function + * + * @description + * Determines if a reference is a `Function`. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is a `Function`. + */ +function isFunction(value){return typeof value == 'function';} + + +/** + * Checks if `obj` is a window object. + * + * @private + * @param {*} obj Object to check + * @returns {boolean} True if `obj` is a window obj. + */ +function isWindow(obj) { + return obj && obj.document && obj.location && obj.alert && obj.setInterval; +} + + +function isScope(obj) { + return obj && obj.$evalAsync && obj.$watch; +} + + +function isFile(obj) { + return toString.apply(obj) === '[object File]'; +} + + +function isBoolean(value) { + return typeof value == 'boolean'; +} + + +function trim(value) { + return isString(value) ? value.replace(/^\s*/, '').replace(/\s*$/, '') : value; +} + +/** + * @ngdoc function + * @name angular.isElement + * @function + * + * @description + * Determines if a reference is a DOM element (or wrapped jQuery element). + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is a DOM element (or wrapped jQuery element). + */ +function isElement(node) { + return node && + (node.nodeName // we are a direct element + || (node.bind && node.find)); // we have a bind and find method part of jQuery API +} + +/** + * @param str 'key1,key2,...' + * @returns {object} in the form of {key1:true, key2:true, ...} + */ +function makeMap(str){ + var obj = {}, items = str.split(","), i; + for ( i = 0; i < items.length; i++ ) + obj[ items[i] ] = true; + return obj; +} + + +if (msie < 9) { + nodeName_ = function(element) { + element = element.nodeName ? element : element[0]; + return (element.scopeName && element.scopeName != 'HTML') + ? uppercase(element.scopeName + ':' + element.nodeName) : element.nodeName; + }; +} else { + nodeName_ = function(element) { + return element.nodeName ? element.nodeName : element[0].nodeName; + }; +} + + +function map(obj, iterator, context) { + var results = []; + forEach(obj, function(value, index, list) { + results.push(iterator.call(context, value, index, list)); + }); + return results; +} + + +/** + * @description + * Determines the number of elements in an array, the number of properties an object has, or + * the length of a string. + * + * Note: This function is used to augment the Object type in Angular expressions. See + * {@link angular.Object} for more information about Angular arrays. + * + * @param {Object|Array|string} obj Object, array, or string to inspect. + * @param {boolean} [ownPropsOnly=false] Count only "own" properties in an object + * @returns {number} The size of `obj` or `0` if `obj` is neither an object nor an array. + */ +function size(obj, ownPropsOnly) { + var size = 0, key; + + if (isArray(obj) || isString(obj)) { + return obj.length; + } else if (isObject(obj)){ + for (key in obj) + if (!ownPropsOnly || obj.hasOwnProperty(key)) + size++; + } + + return size; +} + + +function includes(array, obj) { + return indexOf(array, obj) != -1; +} + +function indexOf(array, obj) { + if (array.indexOf) return array.indexOf(obj); + + for ( var i = 0; i < array.length; i++) { + if (obj === array[i]) return i; + } + return -1; +} + +function arrayRemove(array, value) { + var index = indexOf(array, value); + if (index >=0) + array.splice(index, 1); + return value; +} + +function isLeafNode (node) { + if (node) { + switch (node.nodeName) { + case "OPTION": + case "PRE": + case "TITLE": + return true; + } + } + return false; +} + +/** + * @ngdoc function + * @name angular.copy + * @function + * + * @description + * Creates a deep copy of `source`, which should be an object or an array. + * + * * If no destination is supplied, a copy of the object or array is created. + * * If a destination is provided, all of its elements (for array) or properties (for objects) + * are deleted and then all elements/properties from the source are copied to it. + * * If `source` is not an object or array, `source` is returned. + * + * Note: this function is used to augment the Object type in Angular expressions. See + * {@link ng.$filter} for more information about Angular arrays. + * + * @param {*} source The source that will be used to make a copy. + * Can be any type, including primitives, `null`, and `undefined`. + * @param {(Object|Array)=} destination Destination into which the source is copied. If + * provided, must be of the same type as `source`. + * @returns {*} The copy or updated `destination`, if `destination` was specified. + */ +function copy(source, destination){ + if (isWindow(source) || isScope(source)) throw Error("Can't copy Window or Scope"); + if (!destination) { + destination = source; + if (source) { + if (isArray(source)) { + destination = copy(source, []); + } else if (isDate(source)) { + destination = new Date(source.getTime()); + } else if (isObject(source)) { + destination = copy(source, {}); + } + } + } else { + if (source === destination) throw Error("Can't copy equivalent objects or arrays"); + if (isArray(source)) { + destination.length = 0; + for ( var i = 0; i < source.length; i++) { + destination.push(copy(source[i])); + } + } else { + forEach(destination, function(value, key){ + delete destination[key]; + }); + for ( var key in source) { + destination[key] = copy(source[key]); + } + } + } + return destination; +} + +/** + * Create a shallow copy of an object + */ +function shallowCopy(src, dst) { + dst = dst || {}; + + for(var key in src) { + if (src.hasOwnProperty(key) && key.substr(0, 2) !== '$$') { + dst[key] = src[key]; + } + } + + return dst; +} + + +/** + * @ngdoc function + * @name angular.equals + * @function + * + * @description + * Determines if two objects or two values are equivalent. Supports value types, arrays and + * objects. + * + * Two objects or values are considered equivalent if at least one of the following is true: + * + * * Both objects or values pass `===` comparison. + * * Both objects or values are of the same type and all of their properties pass `===` comparison. + * * Both values are NaN. (In JavasScript, NaN == NaN => false. But we consider two NaN as equal) + * + * During a property comparision, properties of `function` type and properties with names + * that begin with `$` are ignored. + * + * Scope and DOMWindow objects are being compared only be identify (`===`). + * + * @param {*} o1 Object or value to compare. + * @param {*} o2 Object or value to compare. + * @returns {boolean} True if arguments are equal. + */ +function equals(o1, o2) { + if (o1 === o2) return true; + if (o1 === null || o2 === null) return false; + if (o1 !== o1 && o2 !== o2) return true; // NaN === NaN + var t1 = typeof o1, t2 = typeof o2, length, key, keySet; + if (t1 == t2) { + if (t1 == 'object') { + if (isArray(o1)) { + if ((length = o1.length) == o2.length) { + for(key=0; key
+ * // Create a new module + * var myModule = angular.module('myModule', []); + * + * // register a new service + * myModule.value('appName', 'MyCoolApp'); + * + * // configure existing services inside initialization blocks. + * myModule.config(function($locationProvider) { + * // Configure existing providers + * $locationProvider.hashPrefix('!'); + * }); + *+ * + * Then you can create an injector and load your modules like this: + * + *
+ * var injector = angular.injector(['ng', 'MyModule']) + *+ * + * However it's more likely that you'll just use + * {@link ng.directive:ngApp ngApp} or + * {@link angular.bootstrap} to simplify this process for you. + * + * @param {!string} name The name of the module to create or retrieve. + * @param {Array.
+ * // create an injector + * var $injector = angular.injector(['ng']); + * + * // use the injector to kick off your application + * // use the type inference to auto inject arguments, or use implicit injection + * $injector.invoke(function($rootScope, $compile, $document){ + * $compile($document)($rootScope); + * $rootScope.$digest(); + * }); + *+ */ + + +/** + * @ngdoc overview + * @name AUTO + * @description + * + * Implicit module which gets automatically added to each {@link AUTO.$injector $injector}. + */ + +var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; +var FN_ARG_SPLIT = /,/; +var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; +var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; +function annotate(fn) { + var $inject, + fnText, + argDecl, + last; + + if (typeof fn == 'function') { + if (!($inject = fn.$inject)) { + $inject = []; + fnText = fn.toString().replace(STRIP_COMMENTS, ''); + argDecl = fnText.match(FN_ARGS); + forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){ + arg.replace(FN_ARG, function(all, underscore, name){ + $inject.push(name); + }); + }); + fn.$inject = $inject; + } + } else if (isArray(fn)) { + last = fn.length - 1; + assertArgFn(fn[last], 'fn') + $inject = fn.slice(0, last); + } else { + assertArgFn(fn, 'fn', true); + } + return $inject; +} + +/////////////////////////////////////// + +/** + * @ngdoc object + * @name AUTO.$injector + * @function + * + * @description + * + * `$injector` is used to retrieve object instances as defined by + * {@link AUTO.$provide provider}, instantiate types, invoke methods, + * and load modules. + * + * The following always holds true: + * + *
+ * var $injector = angular.injector(); + * expect($injector.get('$injector')).toBe($injector); + * expect($injector.invoke(function($injector){ + * return $injector; + * }).toBe($injector); + *+ * + * # Injection Function Annotation + * + * JavaScript does not have annotations, and annotations are needed for dependency injection. The + * following ways are all valid way of annotating function with injection arguments and are equivalent. + * + *
+ * // inferred (only works if code not minified/obfuscated) + * $inject.invoke(function(serviceA){}); + * + * // annotated + * function explicit(serviceA) {}; + * explicit.$inject = ['serviceA']; + * $inject.invoke(explicit); + * + * // inline + * $inject.invoke(['serviceA', function(serviceA){}]); + *+ * + * ## Inference + * + * In JavaScript calling `toString()` on a function returns the function definition. The definition can then be + * parsed and the function arguments can be extracted. *NOTE:* This does not work with minification, and obfuscation + * tools since these tools change the argument names. + * + * ## `$inject` Annotation + * By adding a `$inject` property onto a function the injection parameters can be specified. + * + * ## Inline + * As an array of injection names, where the last item in the array is the function to call. + */ + +/** + * @ngdoc method + * @name AUTO.$injector#get + * @methodOf AUTO.$injector + * + * @description + * Return an instance of the service. + * + * @param {string} name The name of the instance to retrieve. + * @return {*} The instance. + */ + +/** + * @ngdoc method + * @name AUTO.$injector#invoke + * @methodOf AUTO.$injector + * + * @description + * Invoke the method and supply the method arguments from the `$injector`. + * + * @param {!function} fn The function to invoke. The function arguments come form the function annotation. + * @param {Object=} self The `this` for the invoked method. + * @param {Object=} locals Optional object. If preset then any argument names are read from this object first, before + * the `$injector` is consulted. + * @returns {*} the value returned by the invoked `fn` function. + */ + +/** + * @ngdoc method + * @name AUTO.$injector#instantiate + * @methodOf AUTO.$injector + * @description + * Create a new instance of JS type. The method takes a constructor function invokes the new operator and supplies + * all of the arguments to the constructor function as specified by the constructor annotation. + * + * @param {function} Type Annotated constructor function. + * @param {Object=} locals Optional object. If preset then any argument names are read from this object first, before + * the `$injector` is consulted. + * @returns {Object} new instance of `Type`. + */ + +/** + * @ngdoc method + * @name AUTO.$injector#annotate + * @methodOf AUTO.$injector + * + * @description + * Returns an array of service names which the function is requesting for injection. This API is used by the injector + * to determine which services need to be injected into the function when the function is invoked. There are three + * ways in which the function can be annotated with the needed dependencies. + * + * # Argument names + * + * The simplest form is to extract the dependencies from the arguments of the function. This is done by converting + * the function into a string using `toString()` method and extracting the argument names. + *
+ * // Given + * function MyController($scope, $route) { + * // ... + * } + * + * // Then + * expect(injector.annotate(MyController)).toEqual(['$scope', '$route']); + *+ * + * This method does not work with code minfication / obfuscation. For this reason the following annotation strategies + * are supported. + * + * # The `$inject` property + * + * If a function has an `$inject` property and its value is an array of strings, then the strings represent names of + * services to be injected into the function. + *
+ * // Given + * var MyController = function(obfuscatedScope, obfuscatedRoute) { + * // ... + * } + * // Define function dependencies + * MyController.$inject = ['$scope', '$route']; + * + * // Then + * expect(injector.annotate(MyController)).toEqual(['$scope', '$route']); + *+ * + * # The array notation + * + * It is often desirable to inline Injected functions and that's when setting the `$inject` property is very + * inconvenient. In these situations using the array notation to specify the dependencies in a way that survives + * minification is a better choice: + * + *
+ * // We wish to write this (not minification / obfuscation safe) + * injector.invoke(function($compile, $rootScope) { + * // ... + * }); + * + * // We are forced to write break inlining + * var tmpFn = function(obfuscatedCompile, obfuscatedRootScope) { + * // ... + * }; + * tmpFn.$inject = ['$compile', '$rootScope']; + * injector.invoke(tempFn); + * + * // To better support inline function the inline annotation is supported + * injector.invoke(['$compile', '$rootScope', function(obfCompile, obfRootScope) { + * // ... + * }]); + * + * // Therefore + * expect(injector.annotate( + * ['$compile', '$rootScope', function(obfus_$compile, obfus_$rootScope) {}]) + * ).toEqual(['$compile', '$rootScope']); + *+ * + * @param {function|Array.
+ * function GreetProvider() { + * var salutation = 'Hello'; + * + * this.salutation = function(text) { + * salutation = text; + * }; + * + * this.$get = function() { + * return function (name) { + * return salutation + ' ' + name + '!'; + * }; + * }; + * } + * + * describe('Greeter', function(){ + * + * beforeEach(module(function($provide) { + * $provide.provider('greet', GreetProvider); + * }); + * + * it('should greet', inject(function(greet) { + * expect(greet('angular')).toEqual('Hello angular!'); + * })); + * + * it('should allow configuration of salutation', function() { + * module(function(greetProvider) { + * greetProvider.salutation('Ahoj'); + * }); + * inject(function(greet) { + * expect(greet('angular')).toEqual('Ahoj angular!'); + * }); + * )}; + * + * }); + *+ */ + +/** + * @ngdoc method + * @name AUTO.$provide#provider + * @methodOf AUTO.$provide + * @description + * + * Register a provider for a service. The providers can be retrieved and can have additional configuration methods. + * + * @param {string} name The name of the instance. NOTE: the provider will be available under `name + 'Provider'` key. + * @param {(Object|function())} provider If the provider is: + * + * - `Object`: then it should have a `$get` method. The `$get` method will be invoked using + * {@link AUTO.$injector#invoke $injector.invoke()} when an instance needs to be created. + * - `Constructor`: a new instance of the provider will be created using + * {@link AUTO.$injector#instantiate $injector.instantiate()}, then treated as `object`. + * + * @returns {Object} registered provider instance + */ + +/** + * @ngdoc method + * @name AUTO.$provide#factory + * @methodOf AUTO.$provide + * @description + * + * A short hand for configuring services if only `$get` method is required. + * + * @param {string} name The name of the instance. + * @param {function()} $getFn The $getFn for the instance creation. Internally this is a short hand for + * `$provide.provider(name, {$get: $getFn})`. + * @returns {Object} registered provider instance + */ + + +/** + * @ngdoc method + * @name AUTO.$provide#service + * @methodOf AUTO.$provide + * @description + * + * A short hand for registering service of given class. + * + * @param {string} name The name of the instance. + * @param {Function} constructor A class (constructor function) that will be instantiated. + * @returns {Object} registered provider instance + */ + + +/** + * @ngdoc method + * @name AUTO.$provide#value + * @methodOf AUTO.$provide + * @description + * + * A short hand for configuring services if the `$get` method is a constant. + * + * @param {string} name The name of the instance. + * @param {*} value The value. + * @returns {Object} registered provider instance + */ + + +/** + * @ngdoc method + * @name AUTO.$provide#constant + * @methodOf AUTO.$provide + * @description + * + * A constant value, but unlike {@link AUTO.$provide#value value} it can be injected + * into configuration function (other modules) and it is not interceptable by + * {@link AUTO.$provide#decorator decorator}. + * + * @param {string} name The name of the constant. + * @param {*} value The constant value. + * @returns {Object} registered instance + */ + + +/** + * @ngdoc method + * @name AUTO.$provide#decorator + * @methodOf AUTO.$provide + * @description + * + * Decoration of service, allows the decorator to intercept the service instance creation. The + * returned instance may be the original instance, or a new instance which delegates to the + * original instance. + * + * @param {string} name The name of the service to decorate. + * @param {function()} decorator This function will be invoked when the service needs to be + * instanciated. The function is called using the {@link AUTO.$injector#invoke + * injector.invoke} method and is therefore fully injectable. Local injection arguments: + * + * * `$delegate` - The original service instance, which can be monkey patched, configured, + * decorated or delegated to. + */ + + +function createInjector(modulesToLoad) { + var INSTANTIATING = {}, + providerSuffix = 'Provider', + path = [], + loadedModules = new HashMap(), + providerCache = { + $provide: { + provider: supportObject(provider), + factory: supportObject(factory), + service: supportObject(service), + value: supportObject(value), + constant: supportObject(constant), + decorator: decorator + } + }, + providerInjector = createInternalInjector(providerCache, function() { + throw Error("Unknown provider: " + path.join(' <- ')); + }), + instanceCache = {}, + instanceInjector = (instanceCache.$injector = + createInternalInjector(instanceCache, function(servicename) { + var provider = providerInjector.get(servicename + providerSuffix); + return instanceInjector.invoke(provider.$get, provider); + })); + + + forEach(loadModules(modulesToLoad), function(fn) { instanceInjector.invoke(fn || noop); }); + + return instanceInjector; + + //////////////////////////////////// + // $provider + //////////////////////////////////// + + function supportObject(delegate) { + return function(key, value) { + if (isObject(key)) { + forEach(key, reverseParams(delegate)); + } else { + return delegate(key, value); + } + } + } + + function provider(name, provider_) { + if (isFunction(provider_) || isArray(provider_)) { + provider_ = providerInjector.instantiate(provider_); + } + if (!provider_.$get) { + throw Error('Provider ' + name + ' must define $get factory method.'); + } + return providerCache[name + providerSuffix] = provider_; + } + + function factory(name, factoryFn) { return provider(name, { $get: factoryFn }); } + + function service(name, constructor) { + return factory(name, ['$injector', function($injector) { + return $injector.instantiate(constructor); + }]); + } + + function value(name, value) { return factory(name, valueFn(value)); } + + function constant(name, value) { + providerCache[name] = value; + instanceCache[name] = value; + } + + function decorator(serviceName, decorFn) { + var origProvider = providerInjector.get(serviceName + providerSuffix), + orig$get = origProvider.$get; + + origProvider.$get = function() { + var origInstance = instanceInjector.invoke(orig$get, origProvider); + return instanceInjector.invoke(decorFn, null, {$delegate: origInstance}); + }; + } + + //////////////////////////////////// + // Module Loading + //////////////////////////////////// + function loadModules(modulesToLoad){ + var runBlocks = []; + forEach(modulesToLoad, function(module) { + if (loadedModules.get(module)) return; + loadedModules.put(module, true); + if (isString(module)) { + var moduleFn = angularModule(module); + runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks); + + try { + for(var invokeQueue = moduleFn._invokeQueue, i = 0, ii = invokeQueue.length; i < ii; i++) { + var invokeArgs = invokeQueue[i], + provider = invokeArgs[0] == '$injector' + ? providerInjector + : providerInjector.get(invokeArgs[0]); + + provider[invokeArgs[1]].apply(provider, invokeArgs[2]); + } + } catch (e) { + if (e.message) e.message += ' from ' + module; + throw e; + } + } else if (isFunction(module)) { + try { + runBlocks.push(providerInjector.invoke(module)); + } catch (e) { + if (e.message) e.message += ' from ' + module; + throw e; + } + } else if (isArray(module)) { + try { + runBlocks.push(providerInjector.invoke(module)); + } catch (e) { + if (e.message) e.message += ' from ' + String(module[module.length - 1]); + throw e; + } + } else { + assertArgFn(module, 'module'); + } + }); + return runBlocks; + } + + //////////////////////////////////// + // internal Injector + //////////////////////////////////// + + function createInternalInjector(cache, factory) { + + function getService(serviceName) { + if (typeof serviceName !== 'string') { + throw Error('Service name expected'); + } + if (cache.hasOwnProperty(serviceName)) { + if (cache[serviceName] === INSTANTIATING) { + throw Error('Circular dependency: ' + path.join(' <- ')); + } + return cache[serviceName]; + } else { + try { + path.unshift(serviceName); + cache[serviceName] = INSTANTIATING; + return cache[serviceName] = factory(serviceName); + } finally { + path.shift(); + } + } + } + + function invoke(fn, self, locals){ + var args = [], + $inject = annotate(fn), + length, i, + key; + + for(i = 0, length = $inject.length; i < length; i++) { + key = $inject[i]; + args.push( + locals && locals.hasOwnProperty(key) + ? locals[key] + : getService(key) + ); + } + if (!fn.$inject) { + // this means that we must be an array. + fn = fn[length]; + } + + + // Performance optimization: http://jsperf.com/apply-vs-call-vs-invoke + switch (self ? -1 : args.length) { + case 0: return fn(); + case 1: return fn(args[0]); + case 2: return fn(args[0], args[1]); + case 3: return fn(args[0], args[1], args[2]); + case 4: return fn(args[0], args[1], args[2], args[3]); + case 5: return fn(args[0], args[1], args[2], args[3], args[4]); + case 6: return fn(args[0], args[1], args[2], args[3], args[4], args[5]); + case 7: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6]); + case 8: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]); + case 9: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]); + case 10: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9]); + default: return fn.apply(self, args); + } + } + + function instantiate(Type, locals) { + var Constructor = function() {}, + instance, returnedValue; + + Constructor.prototype = (isArray(Type) ? Type[Type.length - 1] : Type).prototype; + instance = new Constructor(); + returnedValue = invoke(Type, instance, locals); + + return isObject(returnedValue) ? returnedValue : instance; + } + + return { + invoke: invoke, + instantiate: instantiate, + get: getService, + annotate: annotate + }; + } +} +/** + * @ngdoc function + * @name ng.$anchorScroll + * @requires $window + * @requires $location + * @requires $rootScope + * + * @description + * When called, it checks current value of `$location.hash()` and scroll to related element, + * according to rules specified in + * {@link http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document Html5 spec}. + * + * It also watches the `$location.hash()` and scroll whenever it changes to match any anchor. + * This can be disabled by calling `$anchorScrollProvider.disableAutoScrolling()`. + */ +function $AnchorScrollProvider() { + + var autoScrollingEnabled = true; + + this.disableAutoScrolling = function() { + autoScrollingEnabled = false; + }; + + this.$get = ['$window', '$location', '$rootScope', function($window, $location, $rootScope) { + var document = $window.document; + + // helper function to get first anchor from a NodeList + // can't use filter.filter, as it accepts only instances of Array + // and IE can't convert NodeList to an array using [].slice + // TODO(vojta): use filter if we change it to accept lists as well + function getFirstAnchor(list) { + var result = null; + forEach(list, function(element) { + if (!result && lowercase(element.nodeName) === 'a') result = element; + }); + return result; + } + + function scroll() { + var hash = $location.hash(), elm; + + // empty hash, scroll to the top of the page + if (!hash) $window.scrollTo(0, 0); + + // element with given id + else if ((elm = document.getElementById(hash))) elm.scrollIntoView(); + + // first anchor with given name :-D + else if ((elm = getFirstAnchor(document.getElementsByName(hash)))) elm.scrollIntoView(); + + // no element and hash == 'top', scroll to the top of the page + else if (hash === 'top') $window.scrollTo(0, 0); + } + + // does not scroll when user clicks on anchor link that is currently on + // (no url change, no $location.hash() change), browser native does scroll + if (autoScrollingEnabled) { + $rootScope.$watch(function autoScrollWatch() {return $location.hash();}, + function autoScrollWatchAction() { + $rootScope.$evalAsync(scroll); + }); + } + + return scroll; + }]; +} + +/** + * ! This is a private undocumented service ! + * + * @name ng.$browser + * @requires $log + * @description + * This object has two goals: + * + * - hide all the global state in the browser caused by the window object + * - abstract away all the browser specific features and inconsistencies + * + * For tests we provide {@link ngMock.$browser mock implementation} of the `$browser` + * service, which can be used for convenient testing of the application without the interaction with + * the real browser apis. + */ +/** + * @param {object} window The global window object. + * @param {object} document jQuery wrapped document. + * @param {function()} XHR XMLHttpRequest constructor. + * @param {object} $log console.log or an object with the same interface. + * @param {object} $sniffer $sniffer service + */ +function Browser(window, document, $log, $sniffer) { + var self = this, + rawDocument = document[0], + location = window.location, + history = window.history, + setTimeout = window.setTimeout, + clearTimeout = window.clearTimeout, + pendingDeferIds = {}; + + self.isMock = false; + + var outstandingRequestCount = 0; + var outstandingRequestCallbacks = []; + + // TODO(vojta): remove this temporary api + self.$$completeOutstandingRequest = completeOutstandingRequest; + self.$$incOutstandingRequestCount = function() { outstandingRequestCount++; }; + + /** + * Executes the `fn` function(supports currying) and decrements the `outstandingRequestCallbacks` + * counter. If the counter reaches 0, all the `outstandingRequestCallbacks` are executed. + */ + function completeOutstandingRequest(fn) { + try { + fn.apply(null, sliceArgs(arguments, 1)); + } finally { + outstandingRequestCount--; + if (outstandingRequestCount === 0) { + while(outstandingRequestCallbacks.length) { + try { + outstandingRequestCallbacks.pop()(); + } catch (e) { + $log.error(e); + } + } + } + } + } + + /** + * @private + * Note: this method is used only by scenario runner + * TODO(vojta): prefix this method with $$ ? + * @param {function()} callback Function that will be called when no outstanding request + */ + self.notifyWhenNoOutstandingRequests = function(callback) { + // force browser to execute all pollFns - this is needed so that cookies and other pollers fire + // at some deterministic time in respect to the test runner's actions. Leaving things up to the + // regular poller would result in flaky tests. + forEach(pollFns, function(pollFn){ pollFn(); }); + + if (outstandingRequestCount === 0) { + callback(); + } else { + outstandingRequestCallbacks.push(callback); + } + }; + + ////////////////////////////////////////////////////////////// + // Poll Watcher API + ////////////////////////////////////////////////////////////// + var pollFns = [], + pollTimeout; + + /** + * @name ng.$browser#addPollFn + * @methodOf ng.$browser + * + * @param {function()} fn Poll function to add + * + * @description + * Adds a function to the list of functions that poller periodically executes, + * and starts polling if not started yet. + * + * @returns {function()} the added function + */ + self.addPollFn = function(fn) { + if (isUndefined(pollTimeout)) startPoller(100, setTimeout); + pollFns.push(fn); + return fn; + }; + + /** + * @param {number} interval How often should browser call poll functions (ms) + * @param {function()} setTimeout Reference to a real or fake `setTimeout` function. + * + * @description + * Configures the poller to run in the specified intervals, using the specified + * setTimeout fn and kicks it off. + */ + function startPoller(interval, setTimeout) { + (function check() { + forEach(pollFns, function(pollFn){ pollFn(); }); + pollTimeout = setTimeout(check, interval); + })(); + } + + ////////////////////////////////////////////////////////////// + // URL API + ////////////////////////////////////////////////////////////// + + var lastBrowserUrl = location.href, + baseElement = document.find('base'); + + /** + * @name ng.$browser#url + * @methodOf ng.$browser + * + * @description + * GETTER: + * Without any argument, this method just returns current value of location.href. + * + * SETTER: + * With at least one argument, this method sets url to new value. + * If html5 history api supported, pushState/replaceState is used, otherwise + * location.href/location.replace is used. + * Returns its own instance to allow chaining + * + * NOTE: this api is intended for use only by the $location service. Please use the + * {@link ng.$location $location service} to change url. + * + * @param {string} url New url (when used as setter) + * @param {boolean=} replace Should new url replace current history record ? + */ + self.url = function(url, replace) { + // setter + if (url) { + if (lastBrowserUrl == url) return; + lastBrowserUrl = url; + if ($sniffer.history) { + if (replace) history.replaceState(null, '', url); + else { + history.pushState(null, '', url); + // Crazy Opera Bug: http://my.opera.com/community/forums/topic.dml?id=1185462 + baseElement.attr('href', baseElement.attr('href')); + } + } else { + if (replace) location.replace(url); + else location.href = url; + } + return self; + // getter + } else { + // the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172 + return location.href.replace(/%27/g,"'"); + } + }; + + var urlChangeListeners = [], + urlChangeInit = false; + + function fireUrlChange() { + if (lastBrowserUrl == self.url()) return; + + lastBrowserUrl = self.url(); + forEach(urlChangeListeners, function(listener) { + listener(self.url()); + }); + } + + /** + * @name ng.$browser#onUrlChange + * @methodOf ng.$browser + * @TODO(vojta): refactor to use node's syntax for events + * + * @description + * Register callback function that will be called, when url changes. + * + * It's only called when the url is changed by outside of angular: + * - user types different url into address bar + * - user clicks on history (forward/back) button + * - user clicks on a link + * + * It's not called when url is changed by $browser.url() method + * + * The listener gets called with new url as parameter. + * + * NOTE: this api is intended for use only by the $location service. Please use the + * {@link ng.$location $location service} to monitor url changes in angular apps. + * + * @param {function(string)} listener Listener function to be called when url changes. + * @return {function(string)} Returns the registered listener fn - handy if the fn is anonymous. + */ + self.onUrlChange = function(callback) { + if (!urlChangeInit) { + // We listen on both (hashchange/popstate) when available, as some browsers (e.g. Opera) + // don't fire popstate when user change the address bar and don't fire hashchange when url + // changed by push/replaceState + + // html5 history api - popstate event + if ($sniffer.history) jqLite(window).bind('popstate', fireUrlChange); + // hashchange event + if ($sniffer.hashchange) jqLite(window).bind('hashchange', fireUrlChange); + // polling + else self.addPollFn(fireUrlChange); + + urlChangeInit = true; + } + + urlChangeListeners.push(callback); + return callback; + }; + + ////////////////////////////////////////////////////////////// + // Misc API + ////////////////////////////////////////////////////////////// + + /** + * Returns current
+ * var element = $compile('+ * + * - if on the other hand, you need the element to be cloned, the view reference from the original + * example would not point to the clone, but rather to the original template that was cloned. In + * this case, you can access the clone via the cloneAttachFn: + *{{total}}
')(scope); + *
+ * var templateHTML = angular.element('+ * + * + * For information on how the compiler works, see the + * {@link guide/compiler Angular HTML Compiler} section of the Developer Guide. + */ + + +/** + * @ngdoc service + * @name ng.$compileProvider + * @function + * + * @description + */ +$CompileProvider.$inject = ['$provide']; +function $CompileProvider($provide) { + var hasDirectives = {}, + Suffix = 'Directive', + COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/, + CLASS_DIRECTIVE_REGEXP = /(([\d\w\-_]+)(?:\:([^;]+))?;?)/, + MULTI_ROOT_TEMPLATE_ERROR = 'Template must have exactly one root element. was: ', + urlSanitizationWhitelist = /^\s*(https?|ftp|mailto):/; + + + /** + * @ngdoc function + * @name ng.$compileProvider#directive + * @methodOf ng.$compileProvider + * @function + * + * @description + * Register a new directives with the compiler. + * + * @param {string} name Name of the directive in camel-case. (ie{{total}}
'), + * scope = ....; + * + * var clonedElement = $compile(templateHTML)(scope, function(clonedElement, scope) { + * //attach the clone to DOM document at the right place + * }); + * + * //now we have reference to the cloned DOM via `clone` + *
ngBind
which will match as
+ * ng-bind
).
+ * @param {function} directiveFactory An injectable directive factroy function. See {@link guide/directive} for more
+ * info.
+ * @returns {ng.$compileProvider} Self for chaining.
+ */
+ this.directive = function registerDirective(name, directiveFactory) {
+ if (isString(name)) {
+ assertArg(directiveFactory, 'directive');
+ if (!hasDirectives.hasOwnProperty(name)) {
+ hasDirectives[name] = [];
+ $provide.factory(name + Suffix, ['$injector', '$exceptionHandler',
+ function($injector, $exceptionHandler) {
+ var directives = [];
+ forEach(hasDirectives[name], function(directiveFactory) {
+ try {
+ var directive = $injector.invoke(directiveFactory);
+ if (isFunction(directive)) {
+ directive = { compile: valueFn(directive) };
+ } else if (!directive.compile && directive.link) {
+ directive.compile = valueFn(directive.link);
+ }
+ directive.priority = directive.priority || 0;
+ directive.name = directive.name || name;
+ directive.require = directive.require || (directive.controller && directive.name);
+ directive.restrict = directive.restrict || 'A';
+ directives.push(directive);
+ } catch (e) {
+ $exceptionHandler(e);
+ }
+ });
+ return directives;
+ }]);
+ }
+ hasDirectives[name].push(directiveFactory);
+ } else {
+ forEach(name, reverseParams(registerDirective));
+ }
+ return this;
+ };
+
+
+ /**
+ * @ngdoc function
+ * @name ng.$compileProvider#urlSanitizationWhitelist
+ * @methodOf ng.$compileProvider
+ * @function
+ *
+ * @description
+ * Retrieves or overrides the default regular expression that is used for whitelisting of safe
+ * urls during a[href] sanitization.
+ *
+ * The sanitization is a security measure aimed at prevent XSS attacks via html links.
+ *
+ * Any url about to be assigned to a[href] via data-binding is first normalized and turned into an
+ * absolute url. Afterwards the url is matched against the `urlSanitizationWhitelist` regular
+ * expression. If a match is found the original url is written into the dom. Otherwise the
+ * absolute url is prefixed with `'unsafe:'` string and only then it is written into the DOM.
+ *
+ * @param {RegExp=} regexp New regexp to whitelist urls with.
+ * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
+ * chaining otherwise.
+ */
+ this.urlSanitizationWhitelist = function(regexp) {
+ if (isDefined(regexp)) {
+ urlSanitizationWhitelist = regexp;
+ return this;
+ }
+ return urlSanitizationWhitelist;
+ };
+
+
+ this.$get = [
+ '$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse',
+ '$controller', '$rootScope', '$document',
+ function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse,
+ $controller, $rootScope, $document) {
+
+ var Attributes = function(element, attr) {
+ this.$$element = element;
+ this.$attr = attr || {};
+ };
+
+ Attributes.prototype = {
+ $normalize: directiveNormalize,
+
+
+ /**
+ * Set a normalized attribute on the element in a way such that all directives
+ * can share the attribute. This function properly handles boolean attributes.
+ * @param {string} key Normalized key. (ie ngAttribute)
+ * @param {string|boolean} value The value to set. If `null` attribute will be deleted.
+ * @param {boolean=} writeAttr If false, does not write the value to DOM element attribute.
+ * Defaults to true.
+ * @param {string=} attrName Optional none normalized name. Defaults to key.
+ */
+ $set: function(key, value, writeAttr, attrName) {
+ var booleanKey = getBooleanAttrName(this.$$element[0], key),
+ $$observers = this.$$observers,
+ normalizedVal;
+
+ if (booleanKey) {
+ this.$$element.prop(key, value);
+ attrName = booleanKey;
+ }
+
+ this[key] = value;
+
+ // translate normalized key to actual key
+ if (attrName) {
+ this.$attr[key] = attrName;
+ } else {
+ attrName = this.$attr[key];
+ if (!attrName) {
+ this.$attr[key] = attrName = snake_case(key, '-');
+ }
+ }
+
+
+ // sanitize a[href] values
+ if (nodeName_(this.$$element[0]) === 'A' && key === 'href') {
+ urlSanitizationNode.setAttribute('href', value);
+
+ // href property always returns normalized absolute url, so we can match against that
+ normalizedVal = urlSanitizationNode.href;
+ if (!normalizedVal.match(urlSanitizationWhitelist)) {
+ this[key] = value = 'unsafe:' + normalizedVal;
+ }
+ }
+
+
+ if (writeAttr !== false) {
+ if (value === null || value === undefined) {
+ this.$$element.removeAttr(attrName);
+ } else {
+ this.$$element.attr(attrName, value);
+ }
+ }
+
+ // fire observers
+ $$observers && forEach($$observers[key], function(fn) {
+ try {
+ fn(value);
+ } catch (e) {
+ $exceptionHandler(e);
+ }
+ });
+ },
+
+
+ /**
+ * Observe an interpolated attribute.
+ * The observer will never be called, if given attribute is not interpolated.
+ *
+ * @param {string} key Normalized key. (ie ngAttribute) .
+ * @param {function(*)} fn Function that will be called whenever the attribute value changes.
+ * @returns {function(*)} the `fn` Function passed in.
+ */
+ $observe: function(key, fn) {
+ var attrs = this,
+ $$observers = (attrs.$$observers || (attrs.$$observers = {})),
+ listeners = ($$observers[key] || ($$observers[key] = []));
+
+ listeners.push(fn);
+ $rootScope.$evalAsync(function() {
+ if (!listeners.$$inter) {
+ // no one registered attribute interpolation function, so lets call it manually
+ fn(attrs[key]);
+ }
+ });
+ return fn;
+ }
+ };
+
+ var urlSanitizationNode = $document[0].createElement('a'),
+ startSymbol = $interpolate.startSymbol(),
+ endSymbol = $interpolate.endSymbol(),
+ denormalizeTemplate = (startSymbol == '{{' || endSymbol == '}}')
+ ? identity
+ : function denormalizeTemplate(template) {
+ return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol);
+ };
+
+
+ return compile;
+
+ //================================
+
+ function compile($compileNodes, transcludeFn, maxPriority) {
+ if (!($compileNodes instanceof jqLite)) {
+ // jquery always rewraps, where as we need to preserve the original selector so that we can modify it.
+ $compileNodes = jqLite($compileNodes);
+ }
+ // We can not compile top level text elements since text nodes can be merged and we will
+ // not be able to attach scope data to them, so we will wrap them in
+ forEach($compileNodes, function(node, index){
+ if (node.nodeType == 3 /* text node */ && node.nodeValue.match(/\S+/) /* non-empty */ ) {
+ $compileNodes[index] = jqLite(node).wrap('').parent()[0];
+ }
+ });
+ var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority);
+ return function publicLinkFn(scope, cloneConnectFn){
+ assertArg(scope, 'scope');
+ // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart
+ // and sometimes changes the structure of the DOM.
+ var $linkNode = cloneConnectFn
+ ? JQLitePrototype.clone.call($compileNodes) // IMPORTANT!!!
+ : $compileNodes;
+
+ // Attach scope only to non-text nodes.
+ for(var i = 0, ii = $linkNode.length; i+ var $interpolate = ...; // injected + var exp = $interpolate('Hello {{name}}!'); + expect(exp({name:'Angular'}).toEqual('Hello Angular!'); ++ * + * + * @param {string} text The text with markup to interpolate. + * @param {boolean=} mustHaveExpression if set to true then the interpolation string must have + * embedded expression in order to return an interpolation function. Strings with no + * embedded expression will return null for the interpolation function. + * @returns {function(context)} an interpolation function which is used to compute the interpolated + * string. The function has these parameters: + * + * * `context`: an object against which any expressions embedded in the strings are evaluated + * against. + * + */ + function $interpolate(text, mustHaveExpression) { + var startIndex, + endIndex, + index = 0, + parts = [], + length = text.length, + hasInterpolation = false, + fn, + exp, + concat = []; + + while(index < length) { + if ( ((startIndex = text.indexOf(startSymbol, index)) != -1) && + ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1) ) { + (index != startIndex) && parts.push(text.substring(index, startIndex)); + parts.push(fn = $parse(exp = text.substring(startIndex + startSymbolLength, endIndex))); + fn.exp = exp; + index = endIndex + endSymbolLength; + hasInterpolation = true; + } else { + // we did not find anything, so we have to add the remainder to the parts array + (index != length) && parts.push(text.substring(index)); + index = length; + } + } + + if (!(length = parts.length)) { + // we added, nothing, must have been an empty string. + parts.push(''); + length = 1; + } + + if (!mustHaveExpression || hasInterpolation) { + concat.length = length; + fn = function(context) { + for(var i = 0, ii = length, part; i
Reload this page with open console, enter text and hit the log button...
+ Message: + + + + + ++ * var getter = $parse('user.name'); + * var setter = getter.assign; + * var context = {user:{name:'angular'}}; + * var locals = {user:{name:'local'}}; + * + * expect(getter(context)).toEqual('angular'); + * setter(context, 'newValue'); + * expect(context.user.name).toEqual('newValue'); + * expect(getter(context, locals)).toEqual('local'); + *+ * + * + * @param {string} expression String expression to compile. + * @returns {function(context, locals)} a function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the strings + * are evaluated against (tipically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values in + * `context`. + * + * The return function also has an `assign` property, if the expression is assignable, which + * allows one to set values to expressions. + * + */ +function $ParseProvider() { + var cache = {}; + this.$get = ['$filter', '$sniffer', function($filter, $sniffer) { + return function(exp) { + switch(typeof exp) { + case 'string': + return cache.hasOwnProperty(exp) + ? cache[exp] + : cache[exp] = parser(exp, false, $filter, $sniffer.csp); + case 'function': + return exp; + default: + return noop; + } + }; + }]; +} + +/** + * @ngdoc service + * @name ng.$q + * @requires $rootScope + * + * @description + * A promise/deferred implementation inspired by [Kris Kowal's Q](https://github.com/kriskowal/q). + * + * [The CommonJS Promise proposal](http://wiki.commonjs.org/wiki/Promises) describes a promise as an + * interface for interacting with an object that represents the result of an action that is + * performed asynchronously, and may or may not be finished at any given point in time. + * + * From the perspective of dealing with error handling, deferred and promise APIs are to + * asynchronous programming what `try`, `catch` and `throw` keywords are to synchronous programming. + * + *
+ * // for the purpose of this example let's assume that variables `$q` and `scope` are + * // available in the current lexical scope (they could have been injected or passed in). + * + * function asyncGreet(name) { + * var deferred = $q.defer(); + * + * setTimeout(function() { + * // since this fn executes async in a future turn of the event loop, we need to wrap + * // our code into an $apply call so that the model changes are properly observed. + * scope.$apply(function() { + * if (okToGreet(name)) { + * deferred.resolve('Hello, ' + name + '!'); + * } else { + * deferred.reject('Greeting ' + name + ' is not allowed.'); + * } + * }); + * }, 1000); + * + * return deferred.promise; + * } + * + * var promise = asyncGreet('Robin Hood'); + * promise.then(function(greeting) { + * alert('Success: ' + greeting); + * }, function(reason) { + * alert('Failed: ' + reason); + * }); + *+ * + * At first it might not be obvious why this extra complexity is worth the trouble. The payoff + * comes in the way of + * [guarantees that promise and deferred APIs make](https://github.com/kriskowal/uncommonjs/blob/master/promises/specification.md). + * + * Additionally the promise api allows for composition that is very hard to do with the + * traditional callback ([CPS](http://en.wikipedia.org/wiki/Continuation-passing_style)) approach. + * For more on this please see the [Q documentation](https://github.com/kriskowal/q) especially the + * section on serial or parallel joining of promises. + * + * + * # The Deferred API + * + * A new instance of deferred is constructed by calling `$q.defer()`. + * + * The purpose of the deferred object is to expose the associated Promise instance as well as APIs + * that can be used for signaling the successful or unsuccessful completion of the task. + * + * **Methods** + * + * - `resolve(value)` – resolves the derived promise with the `value`. If the value is a rejection + * constructed via `$q.reject`, the promise will be rejected instead. + * - `reject(reason)` – rejects the derived promise with the `reason`. This is equivalent to + * resolving it with a rejection constructed via `$q.reject`. + * + * **Properties** + * + * - promise – `{Promise}` – promise object associated with this deferred. + * + * + * # The Promise API + * + * A new promise instance is created when a deferred instance is created and can be retrieved by + * calling `deferred.promise`. + * + * The purpose of the promise object is to allow for interested parties to get access to the result + * of the deferred task when it completes. + * + * **Methods** + * + * - `then(successCallback, errorCallback)` – regardless of when the promise was or will be resolved + * or rejected calls one of the success or error callbacks asynchronously as soon as the result + * is available. The callbacks are called with a single argument the result or rejection reason. + * + * This method *returns a new promise* which is resolved or rejected via the return value of the + * `successCallback` or `errorCallback`. + * + * + * # Chaining promises + * + * Because calling `then` api of a promise returns a new derived promise, it is easily possible + * to create a chain of promises: + * + *
+ * promiseB = promiseA.then(function(result) { + * return result + 1; + * }); + * + * // promiseB will be resolved immediately after promiseA is resolved and its value will be + * // the result of promiseA incremented by 1 + *+ * + * It is possible to create chains of any length and since a promise can be resolved with another + * promise (which will defer its resolution further), it is possible to pause/defer resolution of + * the promises at any point in the chain. This makes it possible to implement powerful apis like + * $http's response interceptors. + * + * + * # Differences between Kris Kowal's Q and $q + * + * There are three main differences: + * + * - $q is integrated with the {@link ng.$rootScope.Scope} Scope model observation + * mechanism in angular, which means faster propagation of resolution or rejection into your + * models and avoiding unnecessary browser repaints, which would result in flickering UI. + * - $q promises are recognized by the templating engine in angular, which means that in templates + * you can treat promises attached to a scope as if they were the resulting values. + * - Q has many more features that $q, but that comes at a cost of bytes. $q is tiny, but contains + * all the important functionality needed for common async tasks. + * + * # Testing + * + *
+ * it('should simulate promise', inject(function($q, $rootScope) { + * var deferred = $q.defer(); + * var promise = deferred.promise; + * var resolvedValue; + * + * promise.then(function(value) { resolvedValue = value; }); + * expect(resolvedValue).toBeUndefined(); + * + * // Simulate resolving of promise + * deferred.resolve(123); + * // Note that the 'then' function does not get called synchronously. + * // This is because we want the promise API to always be async, whether or not + * // it got called synchronously or asynchronously. + * expect(resolvedValue).toBeUndefined(); + * + * // Propagate promise resolution to 'then' functions using $apply(). + * $rootScope.$apply(); + * expect(resolvedValue).toEqual(123); + * }); + *+ */ +function $QProvider() { + + this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) { + return qFactory(function(callback) { + $rootScope.$evalAsync(callback); + }, $exceptionHandler); + }]; +} + + +/** + * Constructs a promise manager. + * + * @param {function(function)} nextTick Function for executing functions in the next turn. + * @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for + * debugging purposes. + * @returns {object} Promise manager. + */ +function qFactory(nextTick, exceptionHandler) { + + /** + * @ngdoc + * @name ng.$q#defer + * @methodOf ng.$q + * @description + * Creates a `Deferred` object which represents a task which will finish in the future. + * + * @returns {Deferred} Returns a new instance of deferred. + */ + var defer = function() { + var pending = [], + value, deferred; + + deferred = { + + resolve: function(val) { + if (pending) { + var callbacks = pending; + pending = undefined; + value = ref(val); + + if (callbacks.length) { + nextTick(function() { + var callback; + for (var i = 0, ii = callbacks.length; i < ii; i++) { + callback = callbacks[i]; + value.then(callback[0], callback[1]); + } + }); + } + } + }, + + + reject: function(reason) { + deferred.resolve(reject(reason)); + }, + + + promise: { + then: function(callback, errback) { + var result = defer(); + + var wrappedCallback = function(value) { + try { + result.resolve((callback || defaultCallback)(value)); + } catch(e) { + exceptionHandler(e); + result.reject(e); + } + }; + + var wrappedErrback = function(reason) { + try { + result.resolve((errback || defaultErrback)(reason)); + } catch(e) { + exceptionHandler(e); + result.reject(e); + } + }; + + if (pending) { + pending.push([wrappedCallback, wrappedErrback]); + } else { + value.then(wrappedCallback, wrappedErrback); + } + + return result.promise; + } + } + }; + + return deferred; + }; + + + var ref = function(value) { + if (value && value.then) return value; + return { + then: function(callback) { + var result = defer(); + nextTick(function() { + result.resolve(callback(value)); + }); + return result.promise; + } + }; + }; + + + /** + * @ngdoc + * @name ng.$q#reject + * @methodOf ng.$q + * @description + * Creates a promise that is resolved as rejected with the specified `reason`. This api should be + * used to forward rejection in a chain of promises. If you are dealing with the last promise in + * a promise chain, you don't need to worry about it. + * + * When comparing deferreds/promises to the familiar behavior of try/catch/throw, think of + * `reject` as the `throw` keyword in JavaScript. This also means that if you "catch" an error via + * a promise error callback and you want to forward the error to the promise derived from the + * current promise, you have to "rethrow" the error by returning a rejection constructed via + * `reject`. + * + *
+ * promiseB = promiseA.then(function(result) { + * // success: do something and resolve promiseB + * // with the old or a new result + * return result; + * }, function(reason) { + * // error: handle the error if possible and + * // resolve promiseB with newPromiseOrValue, + * // otherwise forward the rejection to promiseB + * if (canHandle(reason)) { + * // handle the error and recover + * return newPromiseOrValue; + * } + * return $q.reject(reason); + * }); + *+ * + * @param {*} reason Constant, message, exception or an object representing the rejection reason. + * @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`. + */ + var reject = function(reason) { + return { + then: function(callback, errback) { + var result = defer(); + nextTick(function() { + result.resolve((errback || defaultErrback)(reason)); + }); + return result.promise; + } + }; + }; + + + /** + * @ngdoc + * @name ng.$q#when + * @methodOf ng.$q + * @description + * Wraps an object that might be a value or a (3rd party) then-able promise into a $q promise. + * This is useful when you are dealing with an object that might or might not be a promise, or if + * the promise comes from a source that can't be trusted. + * + * @param {*} value Value or a promise + * @returns {Promise} Returns a single promise that will be resolved with an array of values, + * each value corresponding to the promise at the same index in the `promises` array. If any of + * the promises is resolved with a rejection, this resulting promise will be resolved with the + * same rejection. + */ + var when = function(value, callback, errback) { + var result = defer(), + done; + + var wrappedCallback = function(value) { + try { + return (callback || defaultCallback)(value); + } catch (e) { + exceptionHandler(e); + return reject(e); + } + }; + + var wrappedErrback = function(reason) { + try { + return (errback || defaultErrback)(reason); + } catch (e) { + exceptionHandler(e); + return reject(e); + } + }; + + nextTick(function() { + ref(value).then(function(value) { + if (done) return; + done = true; + result.resolve(ref(value).then(wrappedCallback, wrappedErrback)); + }, function(reason) { + if (done) return; + done = true; + result.resolve(wrappedErrback(reason)); + }); + }); + + return result.promise; + }; + + + function defaultCallback(value) { + return value; + } + + + function defaultErrback(reason) { + return reject(reason); + } + + + /** + * @ngdoc + * @name ng.$q#all + * @methodOf ng.$q + * @description + * Combines multiple promises into a single promise that is resolved when all of the input + * promises are resolved. + * + * @param {Array.