Browse Source

Initial import

master
Igor Zhukov 10 years ago
parent
commit
0993489baf
  1. 88
      README.md
  2. 1162
      app/css/app.css
  3. BIN
      app/img/Logo_1x.png
  4. BIN
      app/img/Logo_2x.png
  5. BIN
      app/img/bg_full.png
  6. BIN
      app/img/bg_tile.png
  7. BIN
      app/img/icons/Arrow_1x.png
  8. BIN
      app/img/icons/Arrow_2x.png
  9. BIN
      app/img/icons/Attach_1x.png
  10. BIN
      app/img/icons/Attach_2x.png
  11. BIN
      app/img/icons/Attach_pressed_1x.png
  12. BIN
      app/img/icons/Attach_pressed_2x.png
  13. BIN
      app/img/icons/CheckIn_Recent.png
  14. BIN
      app/img/icons/Checks1_1x.png
  15. BIN
      app/img/icons/Checks1_2x.png
  16. BIN
      app/img/icons/Checks2_1x.png
  17. BIN
      app/img/icons/Checks2_2x.png
  18. BIN
      app/img/icons/CloseHover_1x.png
  19. BIN
      app/img/icons/CloseHover_2x.png
  20. BIN
      app/img/icons/Close_1x.png
  21. BIN
      app/img/icons/Close_2x.png
  22. BIN
      app/img/icons/DialogListGroupChatIcon@2x.png
  23. BIN
      app/img/icons/DialogListGroupChatIcon_Highlighted@2x.png
  24. BIN
      app/img/icons/DocBlue_1x.png
  25. BIN
      app/img/icons/DocBlue_2x.png
  26. BIN
      app/img/icons/DocGrey_1x.png
  27. BIN
      app/img/icons/DocGrey_2x.png
  28. BIN
      app/img/icons/Location_Active.png
  29. BIN
      app/img/icons/Logo_1x.png
  30. BIN
      app/img/icons/Logo_2x.png
  31. BIN
      app/img/icons/NoResults.png
  32. BIN
      app/img/icons/Search_1x.png
  33. BIN
      app/img/icons/Search_2x.png
  34. BIN
      app/img/icons/Smile_1x.png
  35. BIN
      app/img/icons/Smile_2x.png
  36. BIN
      app/img/icons/Smile_pressed_1x.png
  37. BIN
      app/img/icons/Smile_pressed_2x.png
  38. BIN
      app/img/icons/VideoIcon.png
  39. BIN
      app/img/icons/icon128.png
  40. BIN
      app/img/icons/icon16.png
  41. BIN
      app/img/placeholders/DialogListAvatarSystem@2x.png
  42. BIN
      app/img/placeholders/GroupAvatar1@2x.png
  43. BIN
      app/img/placeholders/GroupAvatar2@2x.png
  44. BIN
      app/img/placeholders/GroupAvatar3@2x.png
  45. BIN
      app/img/placeholders/GroupAvatar4@2x.png
  46. BIN
      app/img/placeholders/UserAvatar1@2x.png
  47. BIN
      app/img/placeholders/UserAvatar2@2x.png
  48. BIN
      app/img/placeholders/UserAvatar3@2x.png
  49. BIN
      app/img/placeholders/UserAvatar4@2x.png
  50. BIN
      app/img/placeholders/UserAvatar5@2x.png
  51. BIN
      app/img/placeholders/UserAvatar6@2x.png
  52. BIN
      app/img/placeholders/UserAvatar7@2x.png
  53. BIN
      app/img/placeholders/UserAvatar8@2x.png
  54. 45
      app/index.html
  55. 55
      app/js/app.js
  56. 18
      app/js/background.js
  57. 507
      app/js/controllers.js
  58. 512
      app/js/directives.js
  59. 126
      app/js/filters.js
  60. 21
      app/js/lib/aes_worker.js
  61. 14
      app/js/lib/config.js
  62. 2612
      app/js/lib/mtproto.js
  63. 12
      app/js/lib/pq_worker.js
  64. 14
      app/js/lib/sha1_worker.js
  65. 1439
      app/js/services.js
  66. 55
      app/js/util.js
  67. 18
      app/manifest.json
  68. 41
      app/partials/chat_modal.html
  69. 73
      app/partials/dialog.html
  70. 17
      app/partials/head.html
  71. 118
      app/partials/im.html
  72. 28
      app/partials/login.html
  73. 91
      app/partials/message.html
  74. 11
      app/partials/photo_modal.html
  75. 25
      app/partials/user_modal.html
  76. 11
      app/partials/video_modal.html
  77. 5
      app/partials/welcome.html
  78. 70
      app/vendor/README.md
  79. 1294
      app/vendor/angular/angular-animate.js
  80. 22
      app/vendor/angular/angular-animate.min.js
  81. 8
      app/vendor/angular/angular-animate.min.js.map
  82. 202
      app/vendor/angular/angular-cookies.js
  83. 8
      app/vendor/angular/angular-cookies.min.js
  84. 8
      app/vendor/angular/angular-cookies.min.js.map
  85. 24
      app/vendor/angular/angular-csp.css
  86. 410
      app/vendor/angular/angular-loader.js
  87. 9
      app/vendor/angular/angular-loader.min.js
  88. 8
      app/vendor/angular/angular-loader.min.js.map
  89. 546
      app/vendor/angular/angular-resource.js
  90. 12
      app/vendor/angular/angular-resource.min.js
  91. 8
      app/vendor/angular/angular-resource.min.js.map
  92. 891
      app/vendor/angular/angular-route.js
  93. 14
      app/vendor/angular/angular-route.min.js
  94. 8
      app/vendor/angular/angular-route.min.js.map
  95. 615
      app/vendor/angular/angular-sanitize.js
  96. 14
      app/vendor/angular/angular-sanitize.min.js
  97. 8
      app/vendor/angular/angular-sanitize.min.js.map
  98. 563
      app/vendor/angular/angular-touch.js
  99. 13
      app/vendor/angular/angular-touch.min.js
  100. 8
      app/vendor/angular/angular-touch.min.js.map
  101. Some files were not shown because too many files have changed in this diff Show More

88
README.md

@ -1,4 +1,86 @@ @@ -1,4 +1,86 @@
webogram
========
## Webogram - Telegram UNOFFICIAL web application
=================================================
Telegram UNOFFICIAL web application, GPL v3
Telegram offers great [apps for mobile communication](https://www.telegram.org). It is based on the [MTProto protocol](https://core.telegram.org/protocol) and has an [Open API](http://core.telegram.org/api). I personally like Telegram for its speed and cloud-support (that makes a web app possible, unlike in the case of WA and others).
MTProto data can be carried over HTTP, but no official web-version for Telegram exists for the time being. So this project is my take at creating one.
> **Disclaimer**:
> This is an alpha version of the Telegram **UNOFFICIAL** web application. It may have undetected security issues, and there is definitely a load of bugs to fix, features to add and so on. So if you want 200% secure and fully functional communication, don't use this yet! You can always find Telegram official mobile applications here: https://telegram.org
That said, I'm using this app myself and I'd like to share its sources, so anyone can contribute to the development. Any help is welcome!
### Interface
Here are some screenshots of the interface:
[ To be added soon :) ]
### Implemented functionality list
* Sign in by phone number + SMS code (no calls supported yet)
* View list of existings chats (up-posting) with infinite scrolling
* View messages history for each chat/group (bottom-posting) with infinite scrolling
* View brief profile info
* View group info, members list
* View media in messages: photo, video
* Download documents from messages
* Emoji display in all browsers (Chrome, non-OSX ones)
* Emoji keyboard
* Send plain-text messages to user or group
* Send files (photos or documents) via attach icon or drag'n'drop
### Unsupported at the moment
* Secret chats
* Create new group
* Edit group photo/title/participants
* Desktop notifications
* Settings
* Edit profile/userpic
* Contacts
* Edit, delete, forward messsages
* a lot more...
## Technical details
The app is based on AngularJS JavaScript framework, written in pure JavaScript (migration to CoffeeScript is planned for the future). jQuery is used for DOM manipulations, and Bootstrap is the CSS-framework.
### Running locally
#### Running web-server
Project repository is based on angularjs-seed and includes a simple web-server, so it's easy to launch the app locally on your desktop.
Install [node.js](http://nodejs.org/) and run `scripts/web-server.js`. Open page http://localhost:8000/app/index.html in your browser.
#### Running as Chrome Packaged App
It is possible to run this application in Chrome browser as a packaged app. In order to do this, open this URL in Chrome: `chrome://extensions/`, then tick "Developer mode" and press "Load unpacked extension...". Select the downloaded `app` folder and Webogram application should appear in the list.
### Third party libraries
Besides frameworks mentioned above, other libraries are used for protocol and UI needs. Here is the short list:
* [JSBN](http://www-cs-students.stanford.edu/~tjw/jsbn/)
* [CryptoJS](https://code.google.com/p/crypto-js/)
* [zlib.js](https://github.com/imaya/zlib.js)
* [UI Bootstrap](http://angular-ui.github.io/bootstrap/)
* [jQuery Emojiarea](https://github.com/diy/jquery-emojiarea)
* [nanoScrollerJS](https://github.com/jamesflorentino/nanoScrollerJS)
* [gemoji](https://github.com/github/gemoji)
* [emoji-data](https://github.com/iamcal/emoji-data)
Many thanks to all these libraries' authors and contributors. Detailed list with descriptions and licenses is available [here](/app/vendors).
### Licensing
The source code is licensed under GPL v3. License is available [here](/LICENSE).

1162
app/css/app.css

File diff suppressed because it is too large Load Diff

BIN
app/img/Logo_1x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
app/img/Logo_2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
app/img/bg_full.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 KiB

BIN
app/img/bg_tile.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

BIN
app/img/icons/Arrow_1x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

BIN
app/img/icons/Arrow_2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 B

BIN
app/img/icons/Attach_1x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 B

BIN
app/img/icons/Attach_2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 892 B

BIN
app/img/icons/Attach_pressed_1x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 B

BIN
app/img/icons/Attach_pressed_2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 904 B

BIN
app/img/icons/CheckIn_Recent.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 B

BIN
app/img/icons/Checks1_1x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 B

BIN
app/img/icons/Checks1_2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 B

BIN
app/img/icons/Checks2_1x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 B

BIN
app/img/icons/Checks2_2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 713 B

BIN
app/img/icons/CloseHover_1x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 B

BIN
app/img/icons/CloseHover_2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
app/img/icons/Close_1x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 B

BIN
app/img/icons/Close_2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
app/img/icons/DialogListGroupChatIcon@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
app/img/icons/DialogListGroupChatIcon_Highlighted@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 B

BIN
app/img/icons/DocBlue_1x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 B

BIN
app/img/icons/DocBlue_2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 875 B

BIN
app/img/icons/DocGrey_1x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 B

BIN
app/img/icons/DocGrey_2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 898 B

BIN
app/img/icons/Location_Active.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 B

BIN
app/img/icons/Logo_1x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
app/img/icons/Logo_2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
app/img/icons/NoResults.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
app/img/icons/Search_1x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 B

BIN
app/img/icons/Search_2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 706 B

BIN
app/img/icons/Smile_1x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 773 B

BIN
app/img/icons/Smile_2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
app/img/icons/Smile_pressed_1x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 B

BIN
app/img/icons/Smile_pressed_2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
app/img/icons/VideoIcon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 B

BIN
app/img/icons/icon128.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
app/img/icons/icon16.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
app/img/placeholders/DialogListAvatarSystem@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
app/img/placeholders/GroupAvatar1@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
app/img/placeholders/GroupAvatar2@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
app/img/placeholders/GroupAvatar3@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
app/img/placeholders/GroupAvatar4@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
app/img/placeholders/UserAvatar1@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
app/img/placeholders/UserAvatar2@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
app/img/placeholders/UserAvatar3@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
app/img/placeholders/UserAvatar4@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
app/img/placeholders/UserAvatar5@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
app/img/placeholders/UserAvatar6@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
app/img/placeholders/UserAvatar7@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
app/img/placeholders/UserAvatar8@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

45
app/index.html

@ -0,0 +1,45 @@ @@ -0,0 +1,45 @@
<!doctype html>
<html lang="en" ng-app="myApp" ng-csp="">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Webogram</title>
<link rel="stylesheet" href="vendor/angular/angular-csp.css"/>
<link rel="stylesheet" href="vendor/bootstrap/css/bootstrap.css"/>
<link rel="stylesheet" href="vendor/jquery.nanoscroller/nanoscroller.css"/>
<link rel="stylesheet" href="css/app.css"/>
</head>
<body>
<div ng-view></div>
<!-- In production use:
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js"></script>
-->
<script type="text/javascript" src="vendor/jquery/jquery.min.js"></script>
<script type="text/javascript" src="vendor/jquery.nanoscroller/nanoscroller.js"></script>
<script type="text/javascript" src="vendor/jquery.emojiarea/jquery.emojiarea.js"></script>
<script type="text/javascript" src="vendor/angular/angular.js"></script>
<script type="text/javascript" src="vendor/angular/angular-route.js"></script>
<script type="text/javascript" src="vendor/angular/angular-animate.js"></script>
<script type="text/javascript" src="vendor/angular/angular-sanitize.js"></script>
<script type="text/javascript" src="vendor/ui-bootstrap/ui-bootstrap-custom-tpls-0.7.0.js?2"></script>
<script type="text/javascript" src="vendor/jsbn/jsbn_combined.js"></script>
<script type="text/javascript" src="vendor/cryptoJS/crypto.js"></script>
<script type="text/javascript" src="vendor/zlib/gunzip.min.js"></script>
<script type="text/javascript" src="js/lib/config.js"></script>
<script type="text/javascript" src="js/lib/mtproto.js"></script>
<script type="text/javascript" src="js/util.js"></script>
<script type="text/javascript" src="js/app.js"></script>
<script type="text/javascript" src="js/services.js"></script>
<script type="text/javascript" src="js/controllers.js"></script>
<script type="text/javascript" src="js/filters.js"></script>
<script type="text/javascript" src="js/directives.js"></script>
</body>
</html>

55
app/js/app.js

@ -0,0 +1,55 @@ @@ -0,0 +1,55 @@
/*!
* Webogram v0.1 - messaging web application for MTProto
* https://github.com/zhukov/webogram
* Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com>
* https://github.com/zhukov/webogram/blob/master/LICENSE
*/
'use strict';
// window._testMode = 1;
// Declare app level module which depends on filters, and services
angular.module('myApp', [
'ngRoute',
'ngAnimate',
'ngSanitize',
'ui.bootstrap',
'myApp.filters',
'myApp.services',
'mtproto.services',
'myApp.directives',
'myApp.controllers'
]).
config(['$locationProvider', '$routeProvider', '$compileProvider', function($locationProvider, $routeProvider, $compileProvider) {
var icons = {}, reverseIcons = {}, i, j, hex, name, dataItem,
ranges = [[0x1f600, 0x1f637], [0x270a, 0x270c], [0x1f446, 0x1f450]];
for (j in ranges) {
for (i = ranges[j][0]; i <= ranges[j][1]; i++) {
hex = i.toString(16);
if (dataItem = Config.Emoji[hex]) {
name = dataItem[1][0];
icons[':' + name + ':'] = hex + '.png';
reverseIcons[name] = dataItem[0];
}
}
}
$.emojiarea.path = 'vendor/gemoji/images';
$.emojiarea.icons = icons;
$.emojiarea.reverseIcons = reverseIcons;
$compileProvider.imgSrcSanitizationWhitelist(/^\s*(https?|ftp|file|blob|filesystem|chrome-extension):|data:image\//);
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|file|mailto|blob|filesystem|chrome-extension):|data:image\//);
// $locationProvider.html5Mode(true);
$routeProvider.when('/', {templateUrl: 'partials/welcome.html', controller: 'AppWelcomeController'});
$routeProvider.when('/login', {templateUrl: 'partials/login.html', controller: 'AppLoginController'});
$routeProvider.when('/im', {templateUrl: 'partials/im.html', controller: 'AppIMController', reloadOnSearch: false});
$routeProvider.otherwise({redirectTo: '/'});
}]);

18
app/js/background.js

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
/*!
* Webogram v0.1 - messaging web application for MTProto
* https://github.com/zhukov/webogram
* Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com>
* https://github.com/zhukov/webogram/blob/master/LICENSE
*/
chrome.app.runtime.onLaunched.addListener(function(launchData) {
chrome.app.window.create('../index.html', {
bounds: {
width: 1100,
height: 700
},
minWidth: 1100,
minHeight: 700,
frame: 'chrome'
});
});

507
app/js/controllers.js

@ -0,0 +1,507 @@ @@ -0,0 +1,507 @@
/*!
* Webogram v0.1 - messaging web application for MTProto
* https://github.com/zhukov/webogram
* Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com>
* https://github.com/zhukov/webogram/blob/master/LICENSE
*/
'use strict';
/* Controllers */
angular.module('myApp.controllers', [])
.controller('AppWelcomeController', function($scope, $location, MtpApiManager) {
MtpApiManager.getUserID().then(function (id) {
if (id) {
$location.path('/im');
} else {
$location.path('/login');
}
});
})
.controller('AppLoginController', function ($scope, $location, MtpApiManager) {
var dcID = 1;
$scope.credentials = {};
function saveAuth (result) {
MtpApiManager.setUserAuth(dcID, {
expires: result.expires,
id: result.user.id
});
$location.path('/im');
};
$scope.sendCode = function () {
MtpApiManager.invokeApi('auth.sendCode', {
phone_number: $scope.credentials.phone_number,
sms_type: 0,
api_id: 2496,
api_hash: '8da85b0d5bfe62527e5b244c209159c3'
}, {dcID: dcID}).then(function (sentCode) {
$scope.credentials.phone_code_hash = sentCode.phone_code_hash;
$scope.credentials.phone_occupied = sentCode.phone_registered;
$scope.error = {};
}, function (error) {
dLog('sendCode', error);
if (error.code == 303) {
var newDcID = error.type.match(/^(PHONE_MIGRATE_|NETWORK_MIGRATE_)(\d+)/)[2];
if (newDcID != dcID) {
dcID = newDcID;
$scope.sendCode();
return;
}
}
switch (error.type) {
case 'PHONE_NUMBER_INVALID':
$scope.error = {field: 'phone'};
break;
}
});
}
$scope.logIn = function (forceSignUp) {
var method = 'auth.signIn', params = {
phone_number: $scope.credentials.phone_number,
phone_code_hash: $scope.credentials.phone_code_hash,
phone_code: $scope.credentials.phone_code
};
if (forceSignUp) {
method = 'auth.signUp';
angular.extend(params, {
first_name: $scope.credentials.first_name,
last_name: $scope.credentials.last_name
});
}
MtpApiManager.invokeApi(method, params, {dcID: dcID}).then(saveAuth, function (error) {
if (error.code == 400 && error.type == 'PHONE_NUMBER_UNOCCUPIED') {
return $scope.logIn(true);
} else if (error.code == 400 && error.type == 'PHONE_NUMBER_UNOCCUPIED') {
return $scope.logIn(false);
}
switch (error.type) {
case 'FIRSTNAME_INVALID':
$scope.error = {field: 'first_name'};
break;
case 'LASTNAME_INVALID':
$scope.error = {field: 'last_name'};
break;
case 'PHONE_CODE_INVALID':
$scope.error = {field: 'phone_code'};
break;
}
});
};
})
.controller('AppIMController', function ($scope, $location, $routeParams, MtpApiManager) {
$scope.$on('$routeUpdate', updateCurDialog);
$scope.isLoggedIn = true;
$scope.logOut = function () {
MtpApiManager.logOut().then(function () {
$location.path('/login');
});
}
updateCurDialog();
function updateCurDialog() {
$scope.curDialog = {
peer: $routeParams.p || false
};
}
})
.controller('AppImDialogsController', function ($scope, $location, MtpApiManager, AppUsersManager, AppChatsManager, AppMessagesManager, AppPeersManager) {
$scope.dialogs = [];
var offset = 0,
hasMore = false,
limit = 20;
MtpApiManager.invokeApi('account.updateStatus', {offline: false});
$scope.$on('dialogs_need_more', function () {
showMoreDialogs();
});
$scope.$on('dialog_unread', function (e, dialog) {
angular.forEach($scope.dialogs, function(curDialog) {
if (curDialog.peerID == dialog.peerID) {
curDialog.unreadCount = dialog.unread_count;
}
});
});
$scope.$on('dialogs_update', function (e, dialog) {
var pos = false;
angular.forEach($scope.dialogs, function(curDialog, curPos) {
if (curDialog.peerID == dialog.peerID) {
pos = curPos;
}
});
var wrappedDialog = AppMessagesManager.wrapForDialog(dialog.top_message, dialog.unread_count);
if (pos !== false) {
var prev = $scope.dialogs.splice(pos, 1);
wrappedDialog = angular.extend(prev[0], wrappedDialog);
offset++;
}
$scope.dialogs.unshift(wrappedDialog);
});
loadDialogs();
function loadDialogs (startLimit) {
offset = 0;
hasMore = false;
startLimit = startLimit || limit;
AppMessagesManager.getDialogs(offset, startLimit).then(function (dialogsResult) {
offset += startLimit;
hasMore = offset < dialogsResult.count;
$scope.dialogs = [];
angular.forEach(dialogsResult.dialogs, function (dialog) {
$scope.dialogs.push(AppMessagesManager.wrapForDialog(dialog.top_message, dialog.unread_count));
});
$scope.$broadcast('ui_dialogs_change');
}, function (error) {
if (error.code == 401) {
$location.path('/login');
}
});
}
function showMoreDialogs () {
if (!hasMore || !offset) {
return;
}
AppMessagesManager.getDialogs(offset, limit).then(function (dialogsResult) {
offset += limit;
hasMore = offset < dialogsResult.count;
angular.forEach(dialogsResult.dialogs, function (dialog) {
$scope.dialogs.push(AppMessagesManager.wrapForDialog(dialog.top_message, dialog.unread_count));
});
$scope.$broadcast('ui_dialogs_append');
});
}
})
.controller('AppImHistoryController', function ($scope, $location, $timeout, MtpApiManager, AppUsersManager, AppChatsManager, AppMessagesManager, AppPeersManager, ApiUpdatesManager) {
$scope.$watch('curDialog.peer', applyDialogSelect);
ApiUpdatesManager.attach();
$scope.history = [];
$scope.typing = {};
var peerID, offset, hasMore, maxID, limit = 20;
function applyDialogSelect (newPeer) {
newPeer = newPeer || $scope.curDialog.peer || '';
peerID = AppPeersManager.getPeerID(newPeer);
$scope.curDialog.peerID = peerID;
$scope.curDialog.inputPeer = AppPeersManager.getInputPeer(newPeer);
if (peerID) {
loadHistory(peerID);
} else {
showEmptyHistory();
}
}
function showMoreHistory () {
if (!hasMore || !offset) {
return;
}
console.trace('load history');
AppMessagesManager.getHistory($scope.curDialog.inputPeer, maxID, limit).then(function (historyResult) {
offset += limit;
hasMore = offset < historyResult.count;
maxID = historyResult.history[historyResult.history.length - 1];
angular.forEach(historyResult.history, function (id) {
$scope.history.unshift(AppMessagesManager.wrapForHistory(id));
});
$scope.$broadcast('ui_history_prepend');
}, function () {
$scope.state = {error: true};
});
}
function loadHistory () {
hasMore = false;
offset = 0;
maxID = 0;
AppMessagesManager.getHistory($scope.curDialog.inputPeer, maxID, limit).then(function (historyResult) {
offset += limit;
hasMore = offset < historyResult.count;
maxID = historyResult.history[historyResult.history.length - 1];
$scope.history = [];
angular.forEach(historyResult.history, function (id) {
$scope.history.push(AppMessagesManager.wrapForHistory(id));
});
$scope.history.reverse();
$scope.historyPeer = {
id: peerID,
data: AppPeersManager.getPeer(peerID),
photo: AppPeersManager.getPeerPhoto(peerID, 'User', 'Group')
};
$scope.typing = {};
MtpApiManager.getUserID().then(function (id) {
$scope.ownPhoto = AppUsersManager.getUserPhoto(id, 'User');
});
$scope.state = {loaded: true};
$scope.$broadcast('ui_history_change');
AppMessagesManager.readHistory($scope.curDialog.inputPeer);
}, function () {
$scope.state = {error: true};
});
}
function showEmptyHistory () {
$scope.state = {notSelected: true};
$scope.history = [];
}
var typingTimeouts = {};
$scope.$on('history_append', function (e, addedMessage) {
if (addedMessage.peerID == $scope.curDialog.peerID) {
dLog('append', addedMessage);
// console.trace();
$scope.history.push(AppMessagesManager.wrapForHistory(addedMessage.messageID));
$scope.typing = {};
$scope.$broadcast('ui_history_append');
offset++
}
});
$scope.$on('apiUpdate', function (e, update) {
// dLog('on apiUpdate inline', update);
switch (update._) {
case 'updateUserTyping':
if (update.user_id == $scope.curDialog.peerID) {
$scope.typing = {user: AppUsersManager.getUser(update.user_id)};
$timeout.cancel(typingTimeouts[update.user_id]);
typingTimeouts[update.user_id] = $timeout(function () {
$scope.typing = {};
}, 6000);
}
break;
case 'updateChatUserTyping':
if (-update.chat_id == $scope.curDialog.peerID) {
$scope.typing = {user: AppUsersManager.getUser(update.user_id)};
$timeout.cancel(typingTimeouts[update.user_id]);
typingTimeouts[update.user_id] = $timeout(function () {
$scope.typing = {};
}, 6000);
}
break;
}
});
$scope.$on('history_need_more', function () {
showMoreHistory();
});
})
.controller('AppImPanelController', function($scope) {
$scope.$on('user_update', angular.noop);
})
.controller('AppImSendController', function ($scope, MtpApiManager, AppPeersManager, AppMessagesManager, ApiUpdatesManager, MtpApiFileManager) {
$scope.$watch('curDialog.peer', resetDraft);
$scope.$on('user_update', angular.noop);
$scope.draftMessage = {text: ''};
var lastTyping = false;
$scope.$watch('draftMessage.text', function (newVal) {
AppMessagesManager.readHistory($scope.curDialog.inputPeer);
var now = +new Date();
if (newVal === undefined || !newVal.length || now - lastTyping < 6000) {
return;
}
lastTyping = now;
MtpApiManager.invokeApi('messages.setTyping', {
peer: $scope.curDialog.inputPeer,
typing: true
});
});
$scope.sendMessage = sendMessage;
$scope.$watch('draftMessage.files', onFilesSelected);
function sendMessage (e) {
cancelEvent(e);
var text = $scope.draftMessage.text;
if ($scope.draftMessage.sending || !text.length) {
return false;
}
text = text.replace(/:\s*(.+?)\s*:/g, function (all, name) {
var utfChar = $.emojiarea.reverseIcons[name];
if (utfChar !== undefined) {
return utfChar;
}
return all;
});
$scope.draftMessage.sending = true;
MtpApiManager.invokeApi('messages.sendMessage', {
peer: $scope.curDialog.inputPeer,
message: text,
random_id: $scope.draftMessage.randomID
}).then(function (result) {
if (ApiUpdatesManager.saveSeq(result.seq)) {
MtpApiManager.getUserID().then(function (fromID) {
ApiUpdatesManager.saveUpdate({
_: 'updateNewMessage',
message: {
_: 'message',
id: result.id,
from_id: fromID,
to_id: AppPeersManager.getOutputPeer($scope.curDialog.peerID),
out: true,
unread: true,
date: result.date,
message: text,
media: {_: 'messageMediaEmpty'}
},
pts: result.pts
});
});
}
$scope.$broadcast('ui_message_send');
resetDraft();
}, function () {
delete $scope.draftMessage.sending;
});
return cancelEvent(e);
}
function resetDraft () {
$scope.draftMessage = {
randomID: [nextRandomInt(0xFFFFFFFF), nextRandomInt(0xFFFFFFFF)],
text: ''
};
}
function onFilesSelected (newVal) {
if (!angular.isArray(newVal) || !newVal.length) {
return;
}
for (var i = 0; i < newVal.length; i++) {
(function (file, randomID) {
MtpApiFileManager.uploadFile(file).then(function (inputFile) {
var inputMedia;
if (file.type == 'image/jpeg') {
inputMedia = {_: 'inputMediaUploadedPhoto', file: inputFile};
} else {
inputMedia = {_: 'inputMediaUploadedDocument', file: inputFile, file_name: file.name, mime_type: file.type};
}
MtpApiManager.invokeApi('messages.sendMedia', {
peer: $scope.curDialog.inputPeer,
media: inputMedia,
random_id: randomID
}).then(function (result) {
if (ApiUpdatesManager.saveSeq(result.seq)) {
ApiUpdatesManager.saveUpdate({
_: 'updateNewMessage',
message: result.message,
pts: result.pts
});
}
$scope.$broadcast('ui_message_send');
});
}, function (error) {
dLog('upload error', error);
})
})(newVal[i], [nextRandomInt(0xFFFFFFFF), nextRandomInt(0xFFFFFFFF)]);
}
}
})
.controller('PhotoModalController', function ($scope, AppPhotosManager) {
$scope.photo = AppPhotosManager.wrapForFull($scope.photoID);
})
.controller('VideoModalController', function ($scope, AppVideoManager) {
$scope.video = AppVideoManager.wrapForFull($scope.videoID);
})
.controller('UserModalController', function ($scope, $location, AppUsersManager) {
$scope.user = AppUsersManager.wrapForFull($scope.userID);
$scope.goToHistory = function () {
$scope.$close();
$location.url('/im?p=' + $scope.user.peerString);
};
})
.controller('ChatModalController', function ($scope, AppUsersManager, AppChatsManager, fullChat) {
$scope.chatFull = AppChatsManager.wrapForFull($scope.chatID, fullChat);
})

512
app/js/directives.js

@ -0,0 +1,512 @@ @@ -0,0 +1,512 @@
/*!
* Webogram v0.1 - messaging web application for MTProto
* https://github.com/zhukov/webogram
* Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com>
* https://github.com/zhukov/webogram/blob/master/LICENSE
*/
'use strict';
/* Directives */
angular.module('myApp.directives', ['myApp.filters'])
.directive('myDialog', function() {
return {
restrict: 'AE',
scope: true,
translude: false,
templateUrl: 'partials/dialog.html'
};
})
.directive('myMessage', function() {
return {
restrict: 'AE',
scope: true,
translude: false,
templateUrl: 'partials/message.html'
};
})
.directive('myDialogsList', function($window, $timeout) {
return {
link: link
};
function link (scope, element, attrs) {
var dialogsWrap = $('.im_dialogs_wrap')[0],
scrollableWrap = $('.im_dialogs_scrollable_wrap')[0],
// dialogsSearch = $('im_dialogs_search')[0],
moreNotified = false;
onContentLoaded(function () {
$(dialogsWrap).nanoScroller({preventPageScrolling: true, tabIndex: -1});
});
var updateScroller = function () {
onContentLoaded(function () {
$(dialogsWrap).nanoScroller();
});
}
scope.$on('ui_dialogs_prepend', updateScroller);
scope.$on('ui_dialogs_append', function () {
onContentLoaded(function () {
updateScroller();
moreNotified = false;
});
});
scope.$on('ui_dialogs_change', function () {
onContentLoaded(function () {
updateScroller();
moreNotified = false;
});
});
$(scrollableWrap).on('scroll', function (e) {
if (!moreNotified && scrollableWrap.scrollTop >= scrollableWrap.scrollHeight - scrollableWrap.clientHeight - 300) {
scope.$emit('dialogs_need_more');
moreNotified = true;
}
});
function updateSizes () {
$(element).css({
height: $($window).height() - 162
});
}
$($window).on('resize', updateSizes);
updateSizes();
};
})
.directive('myHistory', function ($window, $timeout) {
return {
link: link
};
function link (scope, element, attrs) {
var historyWrap = $('.im_history_wrap')[0],
scrollableWrap = $('.im_history_scrollable_wrap')[0],
scrollable = $('.im_history_scrollable')[0],
panelWrap = $('.im_history_panel_wrap', element)[0],
sendFormWrap = $('.im_send_form_wrap', element)[0],
moreNotified = false;
onContentLoaded(function () {
$(historyWrap).nanoScroller({preventPageScrolling: true, scroll: 'bottom', tabIndex: -1});
});
var updateScroller = function (delay) {
$timeout(function () {
$(historyWrap).nanoScroller();
}, delay || 0);
}
scope.$on('ui_history_append', function () {
var st = scrollableWrap.scrollTop;
$(scrollableWrap).addClass('im_history_to_bottom');
if (atBottom) {
onContentLoaded(function () {
$(scrollableWrap).removeClass('im_history_to_bottom');
updateSizes();
$(historyWrap).nanoScroller({scrollBottom: 0});
// scrollableWrap.scrollTop = st;
// $(scrollableWrap).animate({
// scrollTop: scrollableWrap.scrollHeight - scrollableWrap.clientHeight
// }, 200);
updateScroller();
});
}
});
scope.$on('ui_history_change', function () {
$(scrollableWrap).addClass('im_history_to_bottom');
$(scrollable).css({bottom: 0});
onContentLoaded(function () {
$(scrollableWrap).removeClass('im_history_to_bottom');
$(scrollable).css({bottom: ''});
updateSizes();
$(historyWrap).nanoScroller();
$(historyWrap).nanoScroller({scrollBottom: 0});
updateScroller(100);
moreNotified = false;
});
});
scope.$on('ui_history_prepend', function () {
var sh = scrollableWrap.scrollHeight,
st = scrollableWrap.scrollTop,
ch = scrollableWrap.clientHeight;
$(scrollableWrap).addClass('im_history_to_bottom');
$(scrollable).css({bottom: -(sh - st - ch)});
onContentLoaded(function () {
$(scrollableWrap).removeClass('im_history_to_bottom');
$(scrollable).css({bottom: ''});
$(historyWrap).nanoScroller();
$(historyWrap).nanoScroller({scrollTop: st + scrollableWrap.scrollHeight - sh});
// updateScroller();
updateScroller(50);
moreNotified = false;
});
});
var atBottom = true;
$(scrollableWrap).on('scroll', function (e) {
atBottom = scrollableWrap.scrollTop >= scrollableWrap.scrollHeight - scrollableWrap.clientHeight;
if (!moreNotified && scrollableWrap.scrollTop <= 300) {
moreNotified = true;
scope.$emit('history_need_more');
}
});
function updateSizes () {
$(historyWrap).css({
height: $($window).height() - panelWrap.offsetHeight - sendFormWrap.offsetHeight - 90
});
if (atBottom) {
onContentLoaded(function () {
$(historyWrap).nanoScroller({scroll: 'bottom'});
});
}
updateScroller(100);
}
$($window).on('resize', updateSizes);
onContentLoaded(updateSizes);
}
})
.directive('mySendForm', function ($timeout) {
return {
link: link,
scope: {
draftMessage: '='
}
};
function link (scope, element, attrs) {
var messageField = $('textarea', element)[0],
fileSelect = $('input', element)[0],
dropbox = $('.im_send_dropbox_wrap', element)[0],
emojiButton = $('.im_emoji_btn', element)[0],
editorElement = messageField,
dragStarted, dragTimeout,
emojiArea = $(messageField).emojiarea({button: emojiButton}),
emojiMenu = $('.emoji-menu')[0],
richTextarea = $('.emoji-wysiwyg-editor', element)[0];
if (richTextarea) {
editorElement = richTextarea;
$(richTextarea).addClass('form-control');
$(richTextarea).attr('placeholder', $(messageField).attr('placeholder'));
}
// $(emojiMenu.firstChild).addClass('nano').nanoScroller({preventPageScrolling: true, tabIndex: -1});
$(fileSelect).on('change', function () {
scope.$apply(function () {
scope.draftMessage.files = Array.prototype.slice.call(fileSelect.files);
});
});
var sendOnEnter = true;
$(editorElement).on('keydown', function (e) {
if (e.keyCode != 13) {
return;
}
var submit = false;
if (sendOnEnter && !e.shiftKey) {
submit = true;
} else if (!sendOnEnter && (e.ctrlKey || e.metaKey)) {
submit = true;
}
if (submit) {
$(element).trigger('submit');
dLog('after submit');
return cancelEvent(e);
}
});
if (richTextarea) {
scope.$watch('draftMessage.text', function (newVal) {
if (!newVal.length && !messageField.value.length) {
$timeout(function () {
$(richTextarea).html('');
}, 0);
}
});
}
$('body').on('dragenter dragleave dragover drop', onDragDropEvent);
scope.$on('ui_history_change', focusField);
scope.$on('ui_message_send', focusField);
scope.$on('$destroy', function cleanup() {
$('body').off('dragenter dragleave dragover drop', onDragDropEvent);
});
focusField();
function focusField () {
onContentLoaded(function () {
$(editorElement).focus();
});
}
function onDragDropEvent(e) {
var dragStateChanged = false;
if (!dragStarted || dragStarted == 1) {
dragStarted = checkDragEvent(e) ? 2 : 1;
dragStateChanged = true;
}
if (dragStarted == 2) {
if (dragTimeout) {
setTimeout(function () {
clearTimeout(dragTimeout);
dragTimeout = false;
}, 0);
}
if (e.type == 'dragenter' || e.type == 'dragover') {
if (dragStateChanged) {
$(dropbox)
.css({height: $(editorElement).height() + 12, width: $(editorElement).width() + 12})
.show();
}
} else {
if (e.type == 'drop') {
scope.$apply(function () {
scope.draftMessage.files = Array.prototype.slice.call(e.originalEvent.dataTransfer.files);
});
}
dragTimeout = setTimeout(function () {
$(dropbox).hide();
dragStarted = false;
dragTimeout = false;
}, 300);
}
}
return cancelEvent(e);
};
}
})
.directive('myLoadThumb', function(MtpApiFileManager) {
return {
link: link,
scope: {
thumb: '='
}
};
function link (scope, element, attrs) {
scope.$watch('thumb.location', function (newVal) {
if (!scope.thumb) dLog(222, scope);
if (!scope.thumb.location) {
element.attr('src', scope.thumb.placeholder || '');
return;
}
MtpApiFileManager.downloadSmallFile(scope.thumb.location, scope.thumb.size).then(function (url) {
element.attr('src', url);
}, function (e) {
dLog('Download image failed', e, scope.thumb.location);
element.attr('src', scope.thumb.placeholder || '');
});
})
}
})
.directive('myLoadFullPhoto', function(MtpApiFileManager) {
return {
link: link,
transclude: true,
template:
'<div class="img_fullsize_with_progress_wrap" ng-style="{width: fullPhoto.width + \'px\', height: fullPhoto.height + \'px\'}">\
<div class="img_fullsize_progress_overlay" ng-show="progress.enabled">\
<div class="img_fullsize_progress_wrap" ng-style="{width: fullPhoto.width + \'px\', height: fullPhoto.height + \'px\'}">\
<div class="img_fullsize_progress progress tg_progress">\
<div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="{{progress.percent}}" aria-valuemin="0" aria-valuemax="100" style="width: {{progress.percent}}%">\
<span class="sr-only">{{progress.percent}}% Complete (success)</span>\
</div>\
</div>\
</div>\
</div>\
<div class="photo_full_wrap">\
<a class="photo_modal_image">\
<img class="photo_modal_image" width="{{fullPhoto.width}}" height="{{fullPhoto.height}}" />\
</a>\
</div>\
</div>',
scope: {
fullPhoto: '=',
thumbLocation: '='
}
};
function link (scope, element, attrs) {
var imgElement = $('img', element),
fullLoaded = false;
if (!scope.fullPhoto.location) {
imgElement.attr('src', scope.fullPhoto.placeholder || '');
return;
}
MtpApiFileManager.getCachedFile(scope.thumbLocation).then(function (url) {
if (!fullLoaded) {
imgElement
.attr('src', url)
.addClass('thumb_blurred')
.addClass('thumb_blur_animation');
}
});
var apiPromise;
if (scope.fullPhoto.size) {
var inputLocation = {
_: 'inputFileLocation',
volume_id: scope.fullPhoto.location.volume_id,
local_id: scope.fullPhoto.location.local_id,
secret: scope.fullPhoto.location.secret
};
apiPromise = MtpApiFileManager.downloadFile(scope.fullPhoto.location.dc_id, inputLocation, scope.fullPhoto.size);
} else {
apiPromise = MtpApiFileManager.downloadSmallFile(scope.fullPhoto.location);
}
scope.progress = {enabled: true, percent: 1};
apiPromise.then(function (url) {
fullLoaded = true;
scope.progress.enabled = false;
imgElement
.attr('src', url)
.removeClass('thumb_blurred');
}, function (e) {
dLog('Download image failed', e, scope.fullPhoto.location);
scope.progress.enabled = false;
imgElement
.attr('src', scope.fullPhoto.placeholder || '')
.removeClass('thumb_blurred');
}, function (progress) {
scope.progress.percent = Math.max(1, Math.floor(100 * progress.done / progress.total));
});
}
})
.directive('myLoadVideo', function($sce, MtpApiFileManager) {
return {
link: link,
transclude: true,
template:
'<div class="img_fullsize_with_progress_wrap" ng-style="{width: video.full.width + \'px\', height: video.full.height + \'px\'}">\
<div class="img_fullsize_progress_overlay" ng-show="progress.enabled">\
<div class="img_fullsize_progress_wrap" ng-style="{width: video.full.width + \'px\', height: video.full.height + \'px\'}">\
<div class="img_fullsize_progress progress tg_progress">\
<div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="{{progress.percent}}" aria-valuemin="0" aria-valuemax="100" style="width: {{progress.percent}}%">\
<span class="sr-only">{{progress.percent}}% Complete (success)</span>\
</div>\
</div>\
</div>\
</div>\
<div class="img_fullsize_wrap" ng-if="!player.src">\
<img class="img_fullsize" my-load-thumb thumb="video.thumb" width="{{video.full.width}}" height="{{video.full.height}}" />\
</div>\
<div class="video_full_player" ng-if="player.src">\
<video width="{{video.full.width}}" height="{{video.full.height}}" controls autoplay>\
<source ng-src="{{player.src}}" type="video/mp4">\
</video>\
</div>\
</div>',
scope: {
video: '='
}
};
function link (scope, element, attrs) {
scope.progress = {enabled: true, percent: 1};
scope.player = {};
var inputLocation = {
_: 'inputVideoFileLocation',
id: scope.video.id,
access_hash: scope.video.access_hash
};
MtpApiFileManager.downloadFile(scope.video.dc_id, inputLocation, scope.video.size).then(function (url) {
scope.progress.enabled = false;
// scope.progress = {enabled: true, percent: 50};
scope.player.src = $sce.trustAsResourceUrl(url);
}, function (e) {
dLog('Download image failed', e, scope.fullPhoto.location);
scope.progress.enabled = false;
scope.player.src = '';
}, function (progress) {
scope.progress.percent = Math.max(1, Math.floor(100 * progress.done / progress.total));
});
}
})
.directive('myMapPoint', function(ExternalResourcesManager) {
return {
link: link,
scope: {
point: '='
}
};
function link (scope, element, attrs) {
var apiKey = 'AIzaSyC32ij28dCa0YzEV_HqbWfIwTZQql-RNS0';
var src = 'https://maps.googleapis.com/maps/api/staticmap?sensor=false&center=' + scope.point['lat'] + ',' + scope.point['long'] + '&zoom=13&size=200x100&scale=2&key=' + apiKey;
ExternalResourcesManager.downloadImage(src).then(function (url) {
element.append('<img src="' + url + '" width="200" height="100"/>');
});
element.attr('href','https://maps.google.com/?q=' + scope.point['lat'] + ',' + scope.point['long']);
element.attr('target','_blank');
}
})

126
app/js/filters.js

@ -0,0 +1,126 @@ @@ -0,0 +1,126 @@
/*!
* Webogram v0.1 - messaging web application for MTProto
* https://github.com/zhukov/webogram
* Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com>
* https://github.com/zhukov/webogram/blob/master/LICENSE
*/
'use strict';
/* Filters */
angular.module('myApp.filters', [])
.filter('userName', [function() {
return function (user) {
if (!user || !user.first_name && !user.last_name) {
return 'DELETED';
}
return user.first_name + ' ' + user.last_name;
}
}])
.filter('userFirstName', [function() {
return function (user) {
if (!user || !user.first_name && !user.last_name) {
return 'DELETED';
}
return user.first_name || user.last_name;
}
}])
.filter('userStatus', ['$filter', function($filter) {
return function (user) {
if (!user || !user.status || user.status._ == 'userStatusEmpty') {
return 'offline';
}
if (user.status._ == 'userStatusOnline') {
return 'online';
}
return 'last seen ' + $filter('relativeTime')(user.status.was_online);
}
}])
.filter('chatTitle', [function() {
return function (chat) {
if (!chat || !chat.title) {
return 'DELETED';
}
return chat.title;
}
}])
.filter('dateOrTime', ['$filter', function($filter) {
return function (timestamp) {
var ticks = timestamp * 1000,
diff = Math.abs(+new Date() - ticks),
format = 'HH:mm';
if (diff > 518400000) { // 6 days
format = 'shortDate';
}
else if (diff > 43200000) { // 12 hours
format = 'EEE';
}
return $filter('date')(ticks, format);
}
}])
.filter('duration', [function() {
return function (duration) {
var secs = duration % 60,
mins = Math.floor((duration - secs) / 60.0);
if (secs < 10) {
secs = '0' + secs;
}
return mins + ':' + secs;
}
}])
.filter('phoneNumber', [function() {
return function (phoneRaw) {
if (phoneRaw.charAt(0) == '7') {
return '+' + phoneRaw.charAt(0) + ' (' + phoneRaw.substr(1, 3) + ') ' + phoneRaw.substr(4, 3) + '-' + phoneRaw.substr(7, 2) + '-' + phoneRaw.substr(9, 2);
}
return '+' + phoneRaw;
}
}])
.filter('formatSize', [function () {
return function (size) {
return Math.round(size / 1024) + 'Kb';
}
}])
.filter('nl2br', [function () {
return function (text) {
return text.replace(/\n/g, '<br/>');
}
}])
.filter('richText', ['$filter', function ($filter) {
return function (text) {
return $filter('linky')(text, '_blank').replace(/\n|&#10;/g, '<br/>');
}
}])
.filter('relativeTime', ['$filter', function($filter) {
return function (timestamp) {
var ticks = timestamp * 1000,
diff = Math.abs(+new Date() - ticks);
if (diff < 60000) {
return 'just now';
}
if (diff < 3000000) {
return Math.ceil(diff / 60000) + ' minutes ago';
}
if (diff < 10000000) {
return Math.ceil(diff / 3600000) + ' hours ago';
}
return $filter('dateOrTime')(timestamp);
}
}])

21
app/js/lib/aes_worker.js

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
/*!
* Webogram v0.1 - messaging web application for MTProto
* https://github.com/zhukov/webogram
* Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com>
* https://github.com/zhukov/webogram/blob/master/LICENSE
*/
importScripts('mtproto.js', '../../vendor/jsbn/jsbn_combined.js', '../../vendor/cryptoJS/crypto.js');
onmessage = function (e) {
// console.log('AES worker in', e.data);
var taskID = e.data.taskID,
result;
if (e.data.task == 'encrypt') {
result = aesEncrypt(e.data.bytes, e.data.keyBytes, e.data.ivBytes);
} else {
result = aesDecrypt(e.data.encryptedBytes, e.data.keyBytes, e.data.ivBytes);
}
postMessage({taskID: taskID, result: result});
}

14
app/js/lib/config.js

File diff suppressed because one or more lines are too long

2612
app/js/lib/mtproto.js

File diff suppressed because it is too large Load Diff

12
app/js/lib/pq_worker.js

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
/*!
* Webogram v0.1 - messaging web application for MTProto
* https://github.com/zhukov/webogram
* Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com>
* https://github.com/zhukov/webogram/blob/master/LICENSE
*/
importScripts('mtproto.js', 'jsbn.js');
onmessage = function (e) {
postMessage(pqPrimeFactorization(e.data));
}

14
app/js/lib/sha1_worker.js

@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
/*!
* Webogram v0.1 - messaging web application for MTProto
* https://github.com/zhukov/webogram
* Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com>
* https://github.com/zhukov/webogram/blob/master/LICENSE
*/
importScripts('mtproto.js', '../../vendor/cryptoJS/crypto.js');
onmessage = function (e) {
var taskID = e.data.taskID;
postMessage({taskID: taskID, result: sha1Hash(e.data.bytes)});
}

1439
app/js/services.js

File diff suppressed because it is too large Load Diff

55
app/js/util.js

@ -0,0 +1,55 @@ @@ -0,0 +1,55 @@
/*!
* Webogram v0.1 - messaging web application for MTProto
* https://github.com/zhukov/webogram
* Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com>
* https://github.com/zhukov/webogram/blob/master/LICENSE
*/
function checkClick (e, noprevent) {
if (e.which == 1 && (e.ctrlKey || e.metaKey) || e.which == 2) {
return true;
}
if (!noprevent) {
e.preventDefault();
}
return false;
}
function checkDragEvent(e) {
if (!e || e.target && (e.target.tagName == 'IMG' || e.target.tagName == 'A')) return false;
if (e.dataTransfer && e.dataTransfer.types) {
for (var i = 0; i < e.dataTransfer.types.length; i++) {
if (e.dataTransfer.types[i] == 'Files') {
return true;
}
}
} else {
return true;
}
return false;
}
function cancelEvent (event) {
event = event || window.event;
event.stopPropagation && event.stopPropagation();
event.preventDefault && event.preventDefault();
return false;
}
function onCtrlEnter (textarea, cb) {
$(textarea).on('keydown', function (e) {
if (e.keyCode == 13 && (e.ctrlKey || e.metaKey)) {
cb();
return cancelEvent(e);
}
});
}
function onContentLoaded (cb) {
setTimeout(cb, 0);
};

18
app/manifest.json

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
{
"name": "Telegram UNOFFICIAL",
"version": "0.0.9",
"short_name": "Webogram",
"manifest_version": 2,
"app": {
"background": {
"scripts": ["vendor/angular/angular.js", "js/background.js"]
}
},
"permissions": [
"webview",
{"fileSystem": ["write"]},
"storage"
],
"icons": { "16": "img/icons/icon16.png",
"128": "img/icons/icon128.png" }
}

41
app/partials/chat_modal.html

@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
<div class="chat_modal_wrap">
<div class="modal-header">
<!-- <a class="modal-close-link" ng-click="$close()">Close</a> -->
<h4 class="modal-title">Group Info</h4>
</div>
<div class="modal-body">
<div class="chat_modal_image_wrap pull-left">
<img class="chat_modal_image" my-load-thumb thumb="chatFull.thumb"/>
</div>
<div class="chat_modal_info_wrap clearfix">
<h4 class="chat_modal_header">{{chatFull.chat | chatTitle}}</h4>
<p class="chat_modal_members_count" ng-if="chatFull.participants._ == 'chatParticipants'">
<ng-pluralize count="chatFull.participants.participants.length"
when="{'0': 'No members', 'one': '1 member', 'other': '{} members'}">
</ng-pluralize>
</p>
<button class="btn btn-link chat_modal_invite_btn">Add member</button>
</div>
<h5 class="chat_modal_members_header">Members</h5>
<div class="chat_modal_members_list">
<div class="chat_modal_participant_wrap clearfix" ng-repeat="participant in chatFull.participants.participants">
<a ng-click="openUser(participant.user_id)" class="chat_modal_participant_photo pull-left">
<img class="chat_modal_participant_photo" my-load-thumb thumb="participant.userPhoto"/>
</a>
<div class="chat_modal_participant_name">{{participant.user | userName}}</div>
<!--div class="chat_modal_participant_desc">invited by {{participant.inviter | userName}} {{participant.date | dateOrTime}}</div-->
<div class="chat_modal_participant_status">{{participant.user | userStatus}}</div>
</div>
</div>
</div>
</div>

73
app/partials/dialog.html

@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
<a class="im_dialog clearfix" href="#/im?p={{dialogMessage.peerString}}">
<div class="im_dialog_meta pull-right text-right">
<div class="im_dialog_date">
{{dialogMessage.date | dateOrTime}}
</div>
<span class="im_dialog_badge badge" ng-show="dialogMessage.unreadCount > 0">
{{dialogMessage.unreadCount}}
</span>
</div>
<div class="im_dialog_photo pull-left">
<img class="im_dialog_photo" my-load-thumb thumb="dialogMessage.peerPhoto" aaa="{{dialogMessage.peerPhoto.location}}" />
</div>
<div class="im_dialog_message_wrap">
<div class="im_dialog_peer">
<span class="im_dialog_chat" ng-if="dialogMessage.chatID">
<i class="icon icon-group"></i> <span ng-bind-html="dialogMessage.peerData.rTitle"></span>
</span>
<span class="im_dialog_user" ng-if="dialogMessage.peerID > 0" ng-bind-html="dialogMessage.peerData.rFullName"></span>
</div>
<div class="im_dialog_message">
<span class="im_dialog_chat_from_wrap">
<span class="im_dialog_chat_from" ng-if="!dialogMessage.out &amp;&amp; dialogMessage.chatID" ng-bind-html="dialogMessage.fromUser.rFirstName"></span><span class="im_dialog_chat_from" ng-if="dialogMessage.out">You</span>{{((dialogMessage.out || dialogMessage.peerID &lt; 0) &amp;&amp; (dialogMessage.message.length || dialogMessage.media &amp;&amp; dialogMessage.media._ != 'messageMediaEmpty')) ? ':' : ''}}
</span>
<span class="im_dialog_message_media" ng-if="dialogMessage.media &amp;&amp; dialogMessage.media._ != 'messageMediaEmpty'" ng-switch="dialogMessage.media._">
<span ng-switch-when="messageMediaPhoto">
<span class="glyphicon glyphicon-camera"></span> Photo
</span>
<span ng-switch-when="messageMediaVideo">
<span class="glyphicon glyphicon-facetime-video"></span> Video
</span>
<span ng-switch-when="messageMediaDocument">
<span class="glyphicon glyphicon-document"></span> Document
</span>
<span ng-switch-when="messageMediaGeo">
<span class="glyphicon glyphicon-map-marker"></span> Location
</span>
<span ng-switch-when="messageMediaContact">
<span class="glyphicon glyphicon-user"></span> Contact
</span>
</span>
<span class="im_dialog_message_service" ng-if="dialogMessage._ == 'messageService'" ng-switch="dialogMessage.action._">
<span ng-switch-when="messageActionChatCreate">
created the group
</span>
<span ng-switch-when="messageActionChatEditTitle">
changed group name
</span>
<span ng-switch-when="messageActionChatEditPhoto">
changed group photo
</span>
<span ng-switch-when="messageActionChatDeletePhoto">
removed group photo
</span>
<span ng-switch-when="messageActionChatAddUser">
invited <span ng-bind-html="dialogMessage.action.user.rFullName"></span>
</span>
<span ng-switch-when="messageActionChatDeleteUser">
kicked <span ng-bind-html="dialogMessage.action.user.rFullName"></span>
</span>
</span>
<span class="im_dialog_message_text" ng-if="dialogMessage.message" ng-bind-html="dialogMessage.richMessage"></span>
</div>
</div>
</a>

17
app/partials/head.html

@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
<div class="tg_page_head">
<div class="navbar navbar-static-top navbar-inverse" role="navigation">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="https://zhukov.github.io/webogram"><img src="img/Logo_2x.png" class="tg_head_logo" alt="Telegram logo" width="110" height="31" /> <span class="navbar-brand-alpha font-light">alpha</span></a>
</div>
<div class="navbar-collapse collapse">
<!-- <ul class="nav navbar-nav"></ul> -->
<ul class="nav navbar-nav navbar-right">
<li><a href="https://github.com/zhukov/webogram" target="_blank">About</a></li>
<li ng-if="isLoggedIn"><a href="" ng-click="logOut()">Log out</a></li>
</ul>
</div><!--/.nav-collapse -->
</div>
</div>
</div>

118
app/partials/im.html

@ -0,0 +1,118 @@ @@ -0,0 +1,118 @@
<div ng-include="'partials/head.html'"></div>
<div class="im_page_wrap">
<div class="im_page_split clearfix">
<div class="im_dialogs_col_wrap" ng-controller="AppImDialogsController">
<div class="im_dialogs_search">
<input class="form-control im_dialogs_search_field" type="search" placeholder="Search" ng-model="searchQuery"/>
</div>
<div my-dialogs-list class="im_dialogs_col">
<div class="im_dialogs_wrap nano">
<div class="im_dialogs_scrollable_wrap content">
<ul class="nav nav-pills nav-stacked">
<li my-dialog dialog-message="dialogMessage" ng-repeat="dialogMessage in dialogs | filter:searchQuery" ng-class="{active: curDialog.peerID == dialogMessage.peerID}"></li>
</ul>
</div>
</div>
</div>
</div>
<div class="im_history_col_wrap" ng-controller="AppImHistoryController">
<div ng-if="state.notSelected" class="im_history_not_selected">No history to display</div>
<div ng-if="state.loaded">
<div my-history class="im_history_col">
<div class="im_history_panel_wrap">
<div class="im_history_panel clearfix" ng-controller="AppImPanelController">
<div class="im_history_panel_title">
<div ng-if="historyPeer.id < 0">
<h4>
<!-- <a class="im_history_panel_info_link pull-right" ng-click="openChat(-historyPeer.id)">Info</a> -->
<span ng-bind-html="historyPeer.data.rTitle"></span>
<small class="im_chat_users">
<ng-pluralize count="historyPeer.data.participants_count"
when="{'0': 'No members', 'one': '1 member', 'other': '{} members'}">
</ng-pluralize>
</small>
</h4>
</div>
<div ng-if="historyPeer.id > 0">
<h4>
<!-- <a class="im_history_panel_info_link pull-right" ng-click="openUser(historyPeer.id)">Info</a> -->
<span ng-bind-html="historyPeer.data.rFullName"></span>
<small class="im_peer_online">{{historyPeer.data | userStatus}}</small>
</h4>
</div>
</div>
</div>
</div>
<div class="im_history_wrap nano">
<div class="im_history_scrollable_wrap content">
<div class="im_history_scrollable">
<div class="im_history">
<div class="im_history_message_wrap" my-message ng-repeat="historyMessage in history"></div>
</div>
<div class="im_history_typing_wrap">
<div class="im_history_typing" ng-animate="{enter:'animate-show', leave:'animate-hide'}" ng-if="typing.user">
<strong class="im_history_typing_author" ng-bind-html="typing.user.rFullName"></strong> is typing..
</div>
</div>
</div>
</div>
</div>
<div class="im_send_form_wrap clearfix" ng-controller="AppImSendController">
<div class="pull-right im_panel_peer_photo" ng-click="openUser(historyPeer.id)" ng-if="historyPeer.id > 0">
<img class="im_panel_peer_photo" my-load-thumb thumb="historyPeer.photo" />
<i class="icon im_panel_peer_online" ng-show="historyPeer.data.status._ == 'userStatusOnline'"></i>
</div>
<div class="pull-right im_panel_peer_photo" ng-click="openChat(-historyPeer.id)" ng-if="historyPeer.id < 0">
<img class="im_panel_peer_photo" my-load-thumb thumb="historyPeer.photo" />
</div>
<div class="pull-left im_panel_own_photo">
<img class="im_panel_own_photo" my-load-thumb thumb="ownPhoto" />
</div>
<form my-send-form draft-message="draftMessage" class="im_send_form" ng-submit="sendMessage($event)">
<div class="im_send_dropbox_wrap"> Drop photos here to send </div>
<textarea ng-model="draftMessage.text" placeholder="Write a message..." class="form-control im_message_field"></textarea>
<div class="im_emoji_btn pull-right">
<i class="icon icon-emoji"></i>
</div>
<div class="im_attach pull-right">
<input type="file" class="im_attach_input" size="28" multiple="true" />
<i class="icon icon-paperclip"></i>
</div>
<button type="submit" class="btn btn-tg im_submit">Send</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>

28
app/partials/login.html

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
<div ng-include="'partials/head.html'"></div>
<div class="login_form_wrap">
<div class="error" ng-if="error.message">{{ error.message }}</div>
<form name="mySendCodeForm" ng-if="!credentials.phone_code_hash" ng-submit="sendCode()">
<h3 class="login_form_head">Sign in</h3>
<p class="login_form_lead">Welcome to an experimental web-client of Telegram messenger</p>
<div class="form-group" ng-class="{'has-error': error.field == 'phone'}">
<label class="control-label" for="phone_number" ng-if="error.field == 'phone'">Incorrect phone number</label>
<input type="text" class="form-control" name="phone_number" ng-model="credentials.phone_number" placeholder="Enter your phone" required>
</div>
<button class="btn btn-tg btn-block" type="submit">Next</button>
</form>
<form name="myLoginForm" ng-if="credentials.phone_code_hash" ng-submit="logIn()">
<h3 class="login_form_head">{{ credentials.phone_number | phoneNumber }} <small>(<a href="#/auth">edit</a>)</small></h3>
<p class="login_form_lead">We have sent you a code via SMS.<br/>Please enter it below.</p>
<div class="form-group" ng-class="{'has-error': error.field == 'phone_code'}">
<label class="control-label" for="phone_code" ng-if="error.field == 'phone_code'">Incorrect SMS code</label>
<input type="text" class="form-control" name="phone_code" ng-model="credentials.phone_code" placeholder="Enter your code" required>
</div>
<button class="btn btn-tg btn-block" type="submit">Sign in</button>
</form>
</div>

91
app/partials/message.html

@ -0,0 +1,91 @@ @@ -0,0 +1,91 @@
<div class="im_message_wrap clearfix" ng-class="{im_message_out: historyMessage.out}">
<div class="im_service_message_wrap" ng-if="historyMessage._ == 'messageService'">
<div class="im_service_message">
<a ng-click="openUser(historyMessage.from_id)" class="im_message_author" ng-bind-html="historyMessage.fromUser.rFirstName"></a>
<span class="im_message_service" ng-switch="historyMessage.action['_']">
<span ng-switch-when="messageActionChatCreate">
created the group &laquo;<strong>{{historyMessage.action.title}}</strong>&raquo;
</span>
<span ng-switch-when="messageActionChatEditTitle">
changed group name to &laquo;<strong>{{historyMessage.action.title}}</strong>&raquo;
</span>
<span ng-switch-when="messageActionChatEditPhoto">
changed group photo
</span>
<span ng-switch-when="messageActionChatDeletePhoto">
removed group photo
</span>
<span ng-switch-when="messageActionChatAddUser">
invited <span ng-bind-html="historyMessage.action.user.rFullName"></span>
</span>
<span ng-switch-when="messageActionChatDeleteUser">
kicked <span ng-bind-html="historyMessage.action.user.rFullName"></span>
</span>
<span ng-switch-default>
unsupported action {{historyMessage.action}}
</span>
</span>
</div>
<a ng-if="historyMessage.action._ == 'messageActionChatEditPhoto'" class="im_service_message_photo_thumb" href="" ng-click="openPhoto(historyMessage.action.photo.id)">
<img class="im_service_message_photo_thumb" my-load-thumb thumb="historyMessage.action.photo.thumb" width="{{historyMessage.action.photo.thumb.width}}" height="{{historyMessage.action.photo.thumb.height}}" />
</a>
</div>
<div class="im_content_message_wrap" ng-if="historyMessage._ != 'messageService'">
<i ng-if="historyMessage.unread" class="icon icon-message-status-unread"></i>
<a ng-click="openUser(historyMessage.from_id)" class="im_message_from_photo pull-left">
<img class="im_message_from_photo" my-load-thumb thumb="historyMessage.fromPhoto"/>
</a>
<div class="im_message_meta pull-right text-right">
<i ng-if="historyMessage.out" class="icon icon-message-status-tick" ng-class="{'message-status-delivered-tick': true, 'message-status-unread-tick': historyMessage.unread}"></i>
<span class="im_message_date">{{historyMessage.date | dateOrTime}}</span>
</div>
<div class="im_message_body">
<div class="im_message_author" ng-bind-html="historyMessage.fromUser.rFirstName"></div>
<div class="im_message_media" ng-if="historyMessage.media &amp;&amp; historyMessage.media._ != 'messageMediaEmpty'" ng-switch="historyMessage.media._">
<a ng-switch-when="messageMediaPhoto" class="im_message_photo_thumb" href="" ng-click="openPhoto(historyMessage.media.photo.id)" >
<img class="im_message_photo_thumb" my-load-thumb thumb="historyMessage.media.photo.thumb" width="{{historyMessage.media.photo.thumb.width}}" height="{{historyMessage.media.photo.thumb.height}}" />
</a>
<a ng-switch-when="messageMediaVideo" class="im_message_video_thumb" href="" ng-click="openVideo(historyMessage.media.video.id)">
<img class="im_message_video_thumb" my-load-thumb thumb="historyMessage.media.video.thumb" width="{{historyMessage.media.video.thumb.width}}" height="{{historyMessage.media.video.thumb.height}}" />
<div class="im_message_video_duration_wrap" style="width: {{historyMessage.media.video.thumb.width}}px;">
<span class="im_message_video_duration pull-right">{{historyMessage.media.video.duration | duration}}</span>
<span class="glyphicon glyphicon-facetime-video"></span>
</div>
</a>
<a ng-switch-when="messageMediaDocument" class="im_message_document" href="" ng-click="openDoc(historyMessage.media.document.id)">
<i class="icon icon-document"></i>
<div class="im_message_document_name"><strong>{{historyMessage.media.document.file_name}}</strong> {{historyMessage.media.document.size | formatSize}}</div>
</a>
<a ng-switch-when="messageMediaGeo" my-map-point point="historyMessage.media.geo" class="im_message_geopoint">
<i class="icon icon-geo-point"></i>
</a>
<div ng-switch-when="messageMediaContact">
<a ng-click="openUser(historyMessage.media.user.id)" class="im_message_contact_photo pull-left" ng-if="historyMessage.media.user">
<img class="im_message_contact_photo" my-load-thumb thumb="historyMessage.media.userPhoto"/>
</a>
<div class="im_message_contact_name"><span class="glyphicon glyphicon-user"></span> {{historyMessage.media.first_name}} {{historyMessage.media.last_name}}</div>
<div class="im_message_contact_phone">{{historyMessage.media.phone_number}}</div>
</div>
</div>
<div class="im_message_text" ng-if="historyMessage.message" ng-bind-html="historyMessage.richMessage"></div>
</div>
</div>
</div>

11
app/partials/photo_modal.html

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
<div class="media_modal_wrap photo_modal_wrap">
<div class="modal-body">
<div class="photo_modal_image_wrap" my-load-full-photo full-photo="photo.full" thumb-location="photo.thumb.location" ng-click="$close()"> </div>
<p class="media_modal_info">From: <span class="media_modal_author">{{photo.fromUser | userName}}</span>, {{photo.date | dateOrTime}}</p>
</div>
</div>

25
app/partials/user_modal.html

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
<div class="user_modal_wrap">
<div class="modal-header">
<!-- <a class="modal-close-link" ng-click="$close()">Close</a> -->
<h4 class="modal-title">Info</h4>
</div>
<div class="modal-body">
<div class="user_modal_image_wrap pull-left">
<img class="user_modal_image" my-load-thumb thumb="user.thumb"/>
</div>
<div class="user_modal_info_wrap clearfix">
<h4 class="user_modal_header">{{user | userName}}</h4>
<p class="user_modal_status" ng-if="user.status">{{user | userStatus}}</p>
<p class="user_modal_phone" ng-if="user.phone">{{user.phone | phoneNumber}}</p>
<button class="btn btn-link user_modal_send_btn" ng-click="goToHistory()">Send message</button>
</div>
</div>
</div>

11
app/partials/video_modal.html

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
<div class="media_modal_wrap video_modal_wrap">
<div class="modal-body">
<div class="video_modal_image_wrap" my-load-video video="video"></div>
<p class="media_modal_info">From: <span class="media_modal_author">{{video.fromUser | userName}}</span>, {{video.date | dateOrTime}}</p>
</div>
</div>

5
app/partials/welcome.html

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
<!-- <div class="jumbotron">
<h1>Welcome to Telegram!</h1>
<p>You are currently not authorized. Would you like to log in?</p>
<p><a class="btn btn-primary btn-lg" role="button" href="#/login">Log in</a></p>
</div> -->

70
app/vendor/README.md vendored

@ -0,0 +1,70 @@ @@ -0,0 +1,70 @@
## Third party libraries
### [AngularJS](http://angularjs.org/)
**Author**: Google, Inc.
License - MIT, https://github.com/angular/angular.js/blob/master/LICENSE
### [JSBN](http://www-cs-students.stanford.edu/~tjw/jsbn/)
**Author**: Tom Wu
**License**: BSD, http://www-cs-students.stanford.edu/~tjw/jsbn/LICENSE
Biginteger for RSA, PQ prime factorization
### [jQuery Emojiarea](https://github.com/diy/jquery-emojiarea)
**Author**: diy
**License**: Apache, Version 2.0, https://github.com/diy/jquery-emojiarea#license
Emoji keyboard and rich-textfield for composing messages with emoticons
### [UI Bootstrap](http://angular-ui.github.io/bootstrap/)
**Author**: AngularUI Team, https://github.com/organizations/angular-ui/teams/291112
**License**: MIT, https://github.com/angular-ui/bootstrap/blob/master/LICENSE
Modal windows
### [jQuery](https://github.com/jquery/jquery)
**Author**: jQuery Foundation and other contributors
**License**: MIT, https://github.com/jquery/jquery/blob/master/MIT-LICENSE.txt
Dom manupulations.
### [Bootstrap](https://github.com/twbs/bootstrap)
**Author**: Twitter, Inc
**License**: MIT, https://github.com/twbs/bootstrap/blob/master/LICENSE
Normalize, CSS-framework
### [nanoScrollerJS](https://github.com/jamesflorentino/nanoScrollerJS)
**Author**: James Florentino
**License**: MIT, https://github.com/jamesflorentino/nanoScrollerJS/blob/master/LICENSE-MIT
Beautiful OS X Lion-like scrollbars
### [CryptoJS](https://code.google.com/p/crypto-js/)
**Author**: Jeff Mott
**License**: BSD-3-Clause, https://code.google.com/p/crypto-js/wiki/License
AES, SHA-1 implementation
### [zlib.js](https://github.com/imaya/zlib.js)
**Author**: imaya
**License**: MIT, https://github.com/imaya/zlib.js/blob/master/LICENSE
GZIP for decompressing server responses
### [emoji-data](https://github.com/iamcal/emoji-data)
**Author**: iamcal
**License**: not specified
Build emoji list in apropriate format. Generate sheet in future, when needed.
### [gemoji](https://github.com/github/gemoji)
**Author**: GitHub Inc, 37signals, id Software, whynne@deviantart, Apple Inc.
**License**: https://github.com/github/gemoji/blob/master/LICENSE
Emoji images

1294
app/vendor/angular/angular-animate.js vendored

File diff suppressed because it is too large Load Diff

22
app/vendor/angular/angular-animate.min.js vendored

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
/*
AngularJS v1.2.3
(c) 2010-2014 Google, Inc. http://angularjs.org
License: MIT
*/
(function(C,k,F){'use strict';k.module("ngAnimate",["ng"]).config(["$provide","$animateProvider",function(M,G){var p=k.noop,r=k.forEach,N=G.$$selectors,T=1,h="$$ngAnimateState",H="ng-animate",l={running:!0};M.decorator("$animate",["$delegate","$injector","$sniffer","$rootElement","$timeout","$rootScope","$document",function(v,C,I,g,s,q,F){function O(a){if(a){var d=[],c={};a=a.substr(1).split(".");(I.transitions||I.animations)&&a.push("");for(var e=0;e<a.length;e++){var b=a[e],h=N[b];h&&!c[b]&&(d.push(C.get(h)),
c[b]=!0)}return d}}function m(a,d,c,e,b,l,q){function w(a){t();if(!0===a)u();else{if(a=c.data(h))a.done=u,c.data(h,a);m(x,"after",u)}}function m(e,b,h){var k=b+"End";r(e,function(l,f){var B=function(){a:{var B=b+"Complete",a=e[f];a[B]=!0;(a[k]||p)();for(a=0;a<e.length;a++)if(!e[a][B])break a;h()}};"before"!=b||"enter"!=a&&"move"!=a?l[b]?l[k]=y?l[b](c,d,B):l[b](c,B):B():B()})}function g(){q&&s(q,0,!1)}function t(){t.hasBeenRun||(t.hasBeenRun=!0,l())}function u(){if(!u.hasBeenRun){u.hasBeenRun=!0;var a=
c.data(h);a&&(y?z(c):(a.closeAnimationTimeout=s(function(){z(c)},0,!1),c.data(h,a)));g()}}var k=c.attr("class")||"",v=(" "+(k+" "+d)).replace(/\s+/g,".");e||(e=b?b.parent():c.parent());var v=O(v),y="addClass"==a||"removeClass"==a;b=c.data(h)||{};if(J(c,e)||0===v.length)t(),u();else{var x=[];b.running&&y&&b.structural||r(v,function(b){if(!b.allowCancel||b.allowCancel(c,a,d)){var e=b[a];"leave"==a?(b=e,e=null):b=b["before"+a.charAt(0).toUpperCase()+a.substr(1)];x.push({before:b,after:e})}});0===x.length?
(t(),g()):(e=" "+k+" ",b.running&&(s.cancel(b.closeAnimationTimeout),z(c),L(b.animations),b.beforeComplete?(b.done||p)(!0):y&&!b.structural&&(e="removeClass"==b.event?e.replace(b.className,""):e+b.className+" ")),k=" "+d+" ","addClass"==a&&0<=e.indexOf(k)||"removeClass"==a&&-1==e.indexOf(k)?(t(),g()):(c.addClass(H),c.data(h,{running:!0,event:a,className:d,structural:!y,animations:x,done:w}),m(x,"before",w)))}}function E(a){a=a[0];a.nodeType==T&&r(a.querySelectorAll("."+H),function(a){a=k.element(a);
var c=a.data(h);c&&(L(c.animations),z(a))})}function L(a){r(a,function(d){a.beforeComplete||(d.beforeEnd||p)(!0);a.afterComplete||(d.afterEnd||p)(!0)})}function z(a){a[0]==g[0]?l.disabled||(l.running=!1,l.structural=!1):(a.removeClass(H),a.removeData(h))}function J(a,d){if(l.disabled)return!0;if(a[0]==g[0])return l.disabled||l.running;do{if(0===d.length)break;var c=d[0]==g[0],e=c?l:d.data(h),e=e&&(!!e.disabled||!!e.running);if(c||e)return e;if(c)break}while(d=d.parent());return!0}g.data(h,l);q.$$postDigest(function(){q.$$postDigest(function(){l.running=
!1})});return{enter:function(a,d,c,e){this.enabled(!1,a);v.enter(a,d,c);q.$$postDigest(function(){m("enter","ng-enter",a,d,c,p,e)})},leave:function(a,d){E(a);this.enabled(!1,a);q.$$postDigest(function(){m("leave","ng-leave",a,null,null,function(){v.leave(a)},d)})},move:function(a,d,c,e){E(a);this.enabled(!1,a);v.move(a,d,c);q.$$postDigest(function(){m("move","ng-move",a,d,c,p,e)})},addClass:function(a,d,c){m("addClass",d,a,null,null,function(){v.addClass(a,d)},c)},removeClass:function(a,d,c){m("removeClass",
d,a,null,null,function(){v.removeClass(a,d)},c)},enabled:function(a,d){switch(arguments.length){case 2:if(a)z(d);else{var c=d.data(h)||{};c.disabled=!0;d.data(h,c)}break;case 1:l.disabled=!a;break;default:a=!l.disabled}return!!a}}}]);G.register("",["$window","$sniffer","$timeout",function(l,h,I){function g(f){R.push(f);I.cancel(S);S=I(function(){r(R,function(f){f()});R=[];S=null;D={}},10,!1)}function s(f,a){var K=a?D[a]:null;if(!K){var b=0,c=0,d=0,e=0,h,k,g,m;r(f,function(f){if(f.nodeType==T){f=l.getComputedStyle(f)||
{};g=f[A+G];b=Math.max(q(g),b);m=f[A+t];h=f[A+u];c=Math.max(q(h),c);k=f[w+u];e=Math.max(q(k),e);var a=q(f[w+G]);0<a&&(a*=parseInt(f[w+M],10)||1);d=Math.max(a,d)}});K={total:0,transitionPropertyStyle:m,transitionDurationStyle:g,transitionDelayStyle:h,transitionDelay:c,transitionDuration:b,animationDelayStyle:k,animationDelay:e,animationDuration:d};a&&(D[a]=K)}return K}function q(f){var a=0;f=k.isString(f)?f.split(/\s*,\s*/):[];r(f,function(f){a=Math.max(parseFloat(f)||0,a)});return a}function H(f){var a=
f.parent(),b=a.data(V);b||(a.data(V,++U),b=U);return b+"-"+f[0].className}function O(f,a){var b=H(f),c=b+" "+a,d={},e=D[c]?++D[c].total:0;if(0<e){var h=a+"-stagger",d=b+" "+h;(b=!D[d])&&f.addClass(h);d=s(f,d);b&&f.removeClass(h)}f.addClass(a);c=s(f,c);h=Math.max(c.transitionDuration,c.animationDuration);if(0===h)return f.removeClass(a),!1;var k="";0<c.transitionDuration?(f.addClass(x),k+=N+" ",f[0].style[A+t]="none"):f[0].style[w]="none 0s";r(a.split(" "),function(a,f){k+=(0<f?" ":"")+a+"-active"});
f.data(y,{className:a,activeClassName:k,maxDuration:h,classes:a+" "+k,timings:c,stagger:d,ii:e});return!0}function m(a){a=a[0];var b=A+t;a.style[b]&&0<a.style[b].length&&(a.style[b]="")}function E(a){var b=a[0],c=w;b.style[c]&&0<b.style[c].length&&(a[0].style[c]="")}function L(a,d,e){function k(a){a.stopPropagation();a=a.originalEvent||a;var f=a.$manualTimeStamp||a.timeStamp||Date.now();Math.max(f-w,0)>=v&&a.elapsedTime>=q&&e()}var n=a.data(y);if(a.hasClass(d)&&n){var l=a[0],g=n.timings,m=n.stagger,
q=n.maxDuration,r=n.activeClassName,v=1E3*Math.max(g.transitionDelay,g.animationDelay),w=Date.now(),t=Q+" "+P,u=n.ii,x,n="",p=[];if(0<g.transitionDuration){var s=g.transitionPropertyStyle;-1==s.indexOf("all")&&(x=!0,n+=b+"transition-property: "+s+", "+(h.msie?"-ms-zoom":"border-spacing")+"; ",n+=b+"transition-duration: "+g.transitionDurationStyle+", "+g.transitionDuration+"s; ",p.push(b+"transition-property"),p.push(b+"transition-duration"))}0<u&&(0<m.transitionDelay&&0===m.transitionDuration&&(s=
g.transitionDelayStyle,x&&(s+=", "+g.transitionDelay+"s"),n+=b+"transition-delay: "+z(s,m.transitionDelay,u)+"; ",p.push(b+"transition-delay")),0<m.animationDelay&&0===m.animationDuration&&(n+=b+"animation-delay: "+z(g.animationDelayStyle,m.animationDelay,u)+"; ",p.push(b+"animation-delay")));0<p.length&&(g=l.getAttribute("style")||"",l.setAttribute("style",g+" "+n));a.on(t,k);a.addClass(r);return function(b){a.off(t,k);a.removeClass(r);c(a,d);for(var e in p)l.style.removeProperty(p[e])}}e()}function z(a,
b,c){var d="";r(a.split(","),function(a,f){d+=(0<f?",":"")+(c*b+parseInt(a,10))+"s"});return d}function J(a,b){if(O(a,b))return function(d){d&&c(a,b)}}function a(a,b,d){if(a.data(y))return L(a,b,d);c(a,b);d()}function d(f,b,c){var d=J(f,b);if(d){var e=d;g(function(){m(f);E(f);e=a(f,b,c)});return function(a){(e||p)(a)}}c()}function c(a,b){a.removeClass(b);a.removeClass(x);a.removeData(y)}function e(a,b){var c="";a=k.isArray(a)?a:a.split(/\s+/);r(a,function(a,f){a&&0<a.length&&(c+=(0<f?" ":"")+a+b)});
return c}var b="",A,P,w,Q;C.ontransitionend===F&&C.onwebkittransitionend!==F?(b="-webkit-",A="WebkitTransition",P="webkitTransitionEnd transitionend"):(A="transition",P="transitionend");C.onanimationend===F&&C.onwebkitanimationend!==F?(b="-webkit-",w="WebkitAnimation",Q="webkitAnimationEnd animationend"):(w="animation",Q="animationend");var G="Duration",t="Property",u="Delay",M="IterationCount",V="$$ngAnimateKey",y="$$ngAnimateCSS3Data",x="ng-animate-start",N="ng-animate-active",D={},U=0,R=[],S;return{allowCancel:function(a,
b,c){var d=(a.data(y)||{}).classes;if(!d||0<=["enter","leave","move"].indexOf(b))return!0;var h=a.parent(),g=k.element(a[0].cloneNode());g.attr("style","position:absolute; top:-9999px; left:-9999px");g.removeAttr("id");g.html("");r(d.split(" "),function(a){g.removeClass(a)});g.addClass(e(c,"addClass"==b?"-add":"-remove"));h.append(g);a=s(g);g.remove();return 0<Math.max(a.transitionDuration,a.animationDuration)},enter:function(a,b){return d(a,"ng-enter",b)},leave:function(a,b){return d(a,"ng-leave",
b)},move:function(a,b){return d(a,"ng-move",b)},beforeAddClass:function(a,b,c){if(b=J(a,e(b,"-add")))return g(function(){m(a);E(a);c()}),b;c()},addClass:function(b,c,d){return a(b,e(c,"-add"),d)},beforeRemoveClass:function(a,b,c){if(b=J(a,e(b,"-remove")))return g(function(){m(a);E(a);c()}),b;c()},removeClass:function(b,c,d){return a(b,e(c,"-remove"),d)}}}])}])})(window,window.angular);
//# sourceMappingURL=angular-animate.min.js.map

8
app/vendor/angular/angular-animate.min.js.map vendored

File diff suppressed because one or more lines are too long

202
app/vendor/angular/angular-cookies.js vendored

@ -0,0 +1,202 @@ @@ -0,0 +1,202 @@
/**
* @license AngularJS v1.2.3
* (c) 2010-2014 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, angular, undefined) {'use strict';
/**
* @ngdoc overview
* @name ngCookies
* @description
*
* # ngCookies
*
* The `ngCookies` module provides a convenient wrapper for reading and writing browser cookies.
*
* {@installModule cookies}
*
* <div doc-module-components="ngCookies"></div>
*
* See {@link ngCookies.$cookies `$cookies`} and
* {@link ngCookies.$cookieStore `$cookieStore`} for usage.
*/
angular.module('ngCookies', ['ng']).
/**
* @ngdoc object
* @name ngCookies.$cookies
* @requires $browser
*
* @description
* Provides read/write access to browser's cookies.
*
* Only a simple Object is exposed and by adding or removing properties to/from
* this object, new cookies are created/deleted at the end of current $eval.
*
* Requires the {@link ngCookies `ngCookies`} module to be installed.
*
* @example
<doc:example>
<doc:source>
<script>
function ExampleController($cookies) {
// Retrieving a cookie
var favoriteCookie = $cookies.myFavorite;
// Setting a cookie
$cookies.myFavorite = 'oatmeal';
}
</script>
</doc:source>
</doc:example>
*/
factory('$cookies', ['$rootScope', '$browser', function ($rootScope, $browser) {
var cookies = {},
lastCookies = {},
lastBrowserCookies,
runEval = false,
copy = angular.copy,
isUndefined = angular.isUndefined;
//creates a poller fn that copies all cookies from the $browser to service & inits the service
$browser.addPollFn(function() {
var currentCookies = $browser.cookies();
if (lastBrowserCookies != currentCookies) { //relies on browser.cookies() impl
lastBrowserCookies = currentCookies;
copy(currentCookies, lastCookies);
copy(currentCookies, cookies);
if (runEval) $rootScope.$apply();
}
})();
runEval = true;
//at the end of each eval, push cookies
//TODO: this should happen before the "delayed" watches fire, because if some cookies are not
// strings or browser refuses to store some cookies, we update the model in the push fn.
$rootScope.$watch(push);
return cookies;
/**
* Pushes all the cookies from the service to the browser and verifies if all cookies were
* stored.
*/
function push() {
var name,
value,
browserCookies,
updated;
//delete any cookies deleted in $cookies
for (name in lastCookies) {
if (isUndefined(cookies[name])) {
$browser.cookies(name, undefined);
}
}
//update all cookies updated in $cookies
for(name in cookies) {
value = cookies[name];
if (!angular.isString(value)) {
if (angular.isDefined(lastCookies[name])) {
cookies[name] = lastCookies[name];
} else {
delete cookies[name];
}
} else if (value !== lastCookies[name]) {
$browser.cookies(name, value);
updated = true;
}
}
//verify what was actually stored
if (updated){
updated = false;
browserCookies = $browser.cookies();
for (name in cookies) {
if (cookies[name] !== browserCookies[name]) {
//delete or reset all cookies that the browser dropped from $cookies
if (isUndefined(browserCookies[name])) {
delete cookies[name];
} else {
cookies[name] = browserCookies[name];
}
updated = true;
}
}
}
}
}]).
/**
* @ngdoc object
* @name ngCookies.$cookieStore
* @requires $cookies
*
* @description
* Provides a key-value (string-object) storage, that is backed by session cookies.
* Objects put or retrieved from this storage are automatically serialized or
* deserialized by angular's toJson/fromJson.
*
* Requires the {@link ngCookies `ngCookies`} module to be installed.
*
* @example
*/
factory('$cookieStore', ['$cookies', function($cookies) {
return {
/**
* @ngdoc method
* @name ngCookies.$cookieStore#get
* @methodOf ngCookies.$cookieStore
*
* @description
* Returns the value of given cookie key
*
* @param {string} key Id to use for lookup.
* @returns {Object} Deserialized cookie value.
*/
get: function(key) {
var value = $cookies[key];
return value ? angular.fromJson(value) : value;
},
/**
* @ngdoc method
* @name ngCookies.$cookieStore#put
* @methodOf ngCookies.$cookieStore
*
* @description
* Sets a value for given cookie key
*
* @param {string} key Id for the `value`.
* @param {Object} value Value to be stored.
*/
put: function(key, value) {
$cookies[key] = angular.toJson(value);
},
/**
* @ngdoc method
* @name ngCookies.$cookieStore#remove
* @methodOf ngCookies.$cookieStore
*
* @description
* Remove given cookie
*
* @param {string} key Id of the key-value pair to delete.
*/
remove: function(key) {
delete $cookies[key];
}
};
}]);
})(window, window.angular);

8
app/vendor/angular/angular-cookies.min.js vendored

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
/*
AngularJS v1.2.3
(c) 2010-2014 Google, Inc. http://angularjs.org
License: MIT
*/
(function(p,f,n){'use strict';f.module("ngCookies",["ng"]).factory("$cookies",["$rootScope","$browser",function(d,b){var c={},g={},h,k=!1,l=f.copy,m=f.isUndefined;b.addPollFn(function(){var a=b.cookies();h!=a&&(h=a,l(a,g),l(a,c),k&&d.$apply())})();k=!0;d.$watch(function(){var a,e,d;for(a in g)m(c[a])&&b.cookies(a,n);for(a in c)(e=c[a],f.isString(e))?e!==g[a]&&(b.cookies(a,e),d=!0):f.isDefined(g[a])?c[a]=g[a]:delete c[a];if(d)for(a in e=b.cookies(),c)c[a]!==e[a]&&(m(e[a])?delete c[a]:c[a]=e[a])});
return c}]).factory("$cookieStore",["$cookies",function(d){return{get:function(b){return(b=d[b])?f.fromJson(b):b},put:function(b,c){d[b]=f.toJson(c)},remove:function(b){delete d[b]}}}])})(window,window.angular);
//# sourceMappingURL=angular-cookies.min.js.map

8
app/vendor/angular/angular-cookies.min.js.map vendored

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
{
"version":3,
"file":"angular-cookies.min.js",
"lineCount":7,
"mappings":"A;;;;;aAKC,SAAQ,CAACA,CAAD,CAASC,CAAT,CAAkBC,CAAlB,CAA6B,CAoBtCD,CAAAE,OAAA,CAAe,WAAf,CAA4B,CAAC,IAAD,CAA5B,CAAAC,QAAA,CA4BW,UA5BX,CA4BuB,CAAC,YAAD,CAAe,UAAf,CAA2B,QAAS,CAACC,CAAD,CAAaC,CAAb,CAAuB,CAAA,IACxEC,EAAU,EAD8D,CAExEC,EAAc,EAF0D,CAGxEC,CAHwE,CAIxEC,EAAU,CAAA,CAJ8D,CAKxEC,EAAOV,CAAAU,KALiE,CAMxEC,EAAcX,CAAAW,YAGlBN,EAAAO,UAAA,CAAmB,QAAQ,EAAG,CAC5B,IAAIC,EAAiBR,CAAAC,QAAA,EACjBE,EAAJ,EAA0BK,CAA1B,GACEL,CAGA,CAHqBK,CAGrB,CAFAH,CAAA,CAAKG,CAAL,CAAqBN,CAArB,CAEA,CADAG,CAAA,CAAKG,CAAL,CAAqBP,CAArB,CACA,CAAIG,CAAJ,EAAaL,CAAAU,OAAA,EAJf,CAF4B,CAA9B,CAAA,EAUAL,EAAA,CAAU,CAAA,CAKVL,EAAAW,OAAA,CASAC,QAAa,EAAG,CAAA,IACVC,CADU,CAEVC,CAFU,CAIVC,CAGJ,KAAKF,CAAL,GAAaV,EAAb,CACMI,CAAA,CAAYL,CAAA,CAAQW,CAAR,CAAZ,CAAJ,EACEZ,CAAAC,QAAA,CAAiBW,CAAjB,CAAuBhB,CAAvB,CAKJ,KAAIgB,CAAJ,GAAYX,EAAZ,CAEE,CADAY,CACK,CADGZ,CAAA,CAAQW,CAAR,CACH,CAAAjB,CAAAoB,SAAA,CAAiBF,CAAjB,CAAL,EAMWA,CANX,GAMqBX,CAAA,CAAYU,CAAZ,CANrB,GAOEZ,CAAAC,QAAA,CAAiBW,CAAjB,CAAuBC,CAAvB,CACA,CAAAC,CAAA,CAAU,CAAA,CARZ,EACMnB,CAAAqB,UAAA,CAAkBd,CAAA,CAAYU,CAAZ,CAAlB,CAAJ,CACEX,CAAA,CAAQW,CAAR,CADF,CACkBV,CAAA,CAAYU,CAAZ,CADlB,CAGE,OAAOX,CAAA,CAAQW,CAAR,CASb,IAAIE,CAAJ,CAIE,IAAKF,CAAL,GAFAK,EAEahB,CAFID,CAAAC,QAAA,EAEJA,CAAAA,CAAb,CACMA,CAAA,CAAQW,CAAR,CAAJ,GAAsBK,CAAA,CAAeL,CAAf,CAAtB,GAEMN,CAAA,CAAYW,CAAA,CAAeL,CAAf,CAAZ,CAAJ,CACE,OAAOX,CAAA,CAAQW,CAAR,CADT,CAGEX,CAAA,CAAQW,CAAR,CAHF,CAGkBK,CAAA,CAAeL,CAAf,CALpB,CAlCU,CAThB,CAEA;MAAOX,EA1BqE,CAA3D,CA5BvB,CAAAH,QAAA,CA4HW,cA5HX,CA4H2B,CAAC,UAAD,CAAa,QAAQ,CAACoB,CAAD,CAAW,CAErD,MAAO,KAYAC,QAAQ,CAACC,CAAD,CAAM,CAEjB,MAAO,CADHP,CACG,CADKK,CAAA,CAASE,CAAT,CACL,EAAQzB,CAAA0B,SAAA,CAAiBR,CAAjB,CAAR,CAAkCA,CAFxB,CAZd,KA4BAS,QAAQ,CAACF,CAAD,CAAMP,CAAN,CAAa,CACxBK,CAAA,CAASE,CAAT,CAAA,CAAgBzB,CAAA4B,OAAA,CAAeV,CAAf,CADQ,CA5BrB,QA0CGW,QAAQ,CAACJ,CAAD,CAAM,CACpB,OAAOF,CAAA,CAASE,CAAT,CADa,CA1CjB,CAF8C,CAAhC,CA5H3B,CApBsC,CAArC,CAAA,CAoME1B,MApMF,CAoMUA,MAAAC,QApMV;",
"sources":["angular-cookies.js"],
"names":["window","angular","undefined","module","factory","$rootScope","$browser","cookies","lastCookies","lastBrowserCookies","runEval","copy","isUndefined","addPollFn","currentCookies","$apply","$watch","push","name","value","updated","isString","isDefined","browserCookies","$cookies","get","key","fromJson","put","toJson","remove"]
}

24
app/vendor/angular/angular-csp.css vendored

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
/* Include this file in your html if you are using the CSP mode. */
@charset "UTF-8";
[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak],
.ng-cloak, .x-ng-cloak,
.ng-hide {
display: none !important;
}
ng\:form {
display: block;
}
/* The styles below ensure that the CSS transition will ALWAYS
* animate and close. A nasty bug occurs with CSS transitions where
* when the active class isn't set, or if the active class doesn't
* contain any styles to transition to, then, if ngAnimate is used,
* it will appear as if the webpage is broken due to the forever hanging
* animations. The border-spacing (!ie) and zoom (ie) CSS properties are
* used below since they trigger a transition without making the browser
* animate anything and they're both highly underused CSS properties */
.ng-animate-start { border-spacing:1px 1px; -ms-zoom:1.0001; }
.ng-animate-active { border-spacing:0px 0px; -ms-zoom:1; }

410
app/vendor/angular/angular-loader.js vendored

@ -0,0 +1,410 @@ @@ -0,0 +1,410 @@
/**
* @license AngularJS v1.2.3
* (c) 2010-2014 Google, Inc. http://angularjs.org
* License: MIT
*/
(function() {'use strict';
/**
* @description
*
* This object provides a utility for producing rich Error messages within
* Angular. It can be called as follows:
*
* var exampleMinErr = minErr('example');
* throw exampleMinErr('one', 'This {0} is {1}', foo, bar);
*
* The above creates an instance of minErr in the example namespace. The
* resulting error will have a namespaced error code of example.one. The
* resulting error will replace {0} with the value of foo, and {1} with the
* value of bar. The object is not restricted in the number of arguments it can
* take.
*
* If fewer arguments are specified than necessary for interpolation, the extra
* interpolation markers will be preserved in the final string.
*
* Since data will be parsed statically during a build step, some restrictions
* are applied with respect to how minErr instances are created and called.
* Instances should have names of the form namespaceMinErr for a minErr created
* using minErr('namespace') . Error codes, namespaces and template strings
* should all be static strings, not variables or general expressions.
*
* @param {string} module The namespace to use for the new minErr instance.
* @returns {function(string, string, ...): Error} instance
*/
function minErr(module) {
return function () {
var code = arguments[0],
prefix = '[' + (module ? module + ':' : '') + code + '] ',
template = arguments[1],
templateArgs = arguments,
stringify = function (obj) {
if (typeof obj === 'function') {
return obj.toString().replace(/ \{[\s\S]*$/, '');
} else if (typeof obj === 'undefined') {
return 'undefined';
} else if (typeof obj !== 'string') {
return JSON.stringify(obj);
}
return obj;
},
message, i;
message = prefix + template.replace(/\{\d+\}/g, function (match) {
var index = +match.slice(1, -1), arg;
if (index + 2 < templateArgs.length) {
arg = templateArgs[index + 2];
if (typeof arg === 'function') {
return arg.toString().replace(/ ?\{[\s\S]*$/, '');
} else if (typeof arg === 'undefined') {
return 'undefined';
} else if (typeof arg !== 'string') {
return toJson(arg);
}
return arg;
}
return match;
});
message = message + '\nhttp://errors.angularjs.org/1.2.3/' +
(module ? module + '/' : '') + code;
for (i = 2; i < arguments.length; i++) {
message = message + (i == 2 ? '?' : '&') + 'p' + (i-2) + '=' +
encodeURIComponent(stringify(arguments[i]));
}
return new Error(message);
};
}
/**
* @ngdoc interface
* @name angular.Module
* @description
*
* Interface for configuring angular {@link angular.module modules}.
*/
function setupModuleLoader(window) {
var $injectorMinErr = minErr('$injector');
var ngMinErr = minErr('ng');
function ensure(obj, name, factory) {
return obj[name] || (obj[name] = factory());
}
var angular = ensure(window, 'angular', Object);
// We need to expose `angular.$$minErr` to modules such as `ngResource` that reference it during bootstrap
angular.$$minErr = angular.$$minErr || minErr;
return ensure(angular, 'module', function() {
/** @type {Object.<string, angular.Module>} */
var modules = {};
/**
* @ngdoc function
* @name angular.module
* @description
*
* The `angular.module` is a global place for creating, registering and retrieving Angular
* modules.
* All modules (angular core or 3rd party) that should be available to an application must be
* registered using this mechanism.
*
* When passed two or more arguments, a new module is created. If passed only one argument, an
* existing module (the name passed as the first argument to `module`) is retrieved.
*
*
* # Module
*
* A module is a collection of services, directives, filters, and configuration information.
* `angular.module` is used to configure the {@link AUTO.$injector $injector}.
*
* <pre>
* // 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('!');
* });
* </pre>
*
* Then you can create an injector and load your modules like this:
*
* <pre>
* var injector = angular.injector(['ng', 'MyModule'])
* </pre>
*
* 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.<string>=} requires If specified then new module is being created. If
* unspecified then the the module is being retrieved for further configuration.
* @param {Function} configFn Optional configuration function for the module. Same as
* {@link angular.Module#methods_config Module#config()}.
* @returns {module} new module with the {@link angular.Module} api.
*/
return function module(name, requires, configFn) {
var assertNotHasOwnProperty = function(name, context) {
if (name === 'hasOwnProperty') {
throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context);
}
};
assertNotHasOwnProperty(name, 'module');
if (requires && modules.hasOwnProperty(name)) {
modules[name] = null;
}
return ensure(modules, name, function() {
if (!requires) {
throw $injectorMinErr('nomod', "Module '{0}' is not available! You either misspelled " +
"the module name or forgot to load it. If registering a module ensure that you " +
"specify the dependencies as the second argument.", name);
}
/** @type {!Array.<Array.<*>>} */
var invokeQueue = [];
/** @type {!Array.<Function>} */
var runBlocks = [];
var config = invokeLater('$injector', 'invoke');
/** @type {angular.Module} */
var moduleInstance = {
// Private state
_invokeQueue: invokeQueue,
_runBlocks: runBlocks,
/**
* @ngdoc property
* @name angular.Module#requires
* @propertyOf angular.Module
* @returns {Array.<string>} List of module names which must be loaded before this module.
* @description
* Holds the list of modules which the injector will load before the current module is
* loaded.
*/
requires: requires,
/**
* @ngdoc property
* @name angular.Module#name
* @propertyOf angular.Module
* @returns {string} Name of the module.
* @description
*/
name: name,
/**
* @ngdoc method
* @name angular.Module#provider
* @methodOf angular.Module
* @param {string} name service name
* @param {Function} providerType Construction function for creating new instance of the
* service.
* @description
* See {@link AUTO.$provide#provider $provide.provider()}.
*/
provider: invokeLater('$provide', 'provider'),
/**
* @ngdoc method
* @name angular.Module#factory
* @methodOf angular.Module
* @param {string} name service name
* @param {Function} providerFunction Function for creating new instance of the service.
* @description
* See {@link AUTO.$provide#factory $provide.factory()}.
*/
factory: invokeLater('$provide', 'factory'),
/**
* @ngdoc method
* @name angular.Module#service
* @methodOf angular.Module
* @param {string} name service name
* @param {Function} constructor A constructor function that will be instantiated.
* @description
* See {@link AUTO.$provide#service $provide.service()}.
*/
service: invokeLater('$provide', 'service'),
/**
* @ngdoc method
* @name angular.Module#value
* @methodOf angular.Module
* @param {string} name service name
* @param {*} object Service instance object.
* @description
* See {@link AUTO.$provide#value $provide.value()}.
*/
value: invokeLater('$provide', 'value'),
/**
* @ngdoc method
* @name angular.Module#constant
* @methodOf angular.Module
* @param {string} name constant name
* @param {*} object Constant value.
* @description
* Because the constant are fixed, they get applied before other provide methods.
* See {@link AUTO.$provide#constant $provide.constant()}.
*/
constant: invokeLater('$provide', 'constant', 'unshift'),
/**
* @ngdoc method
* @name angular.Module#animation
* @methodOf angular.Module
* @param {string} name animation name
* @param {Function} animationFactory Factory function for creating new instance of an
* animation.
* @description
*
* **NOTE**: animations take effect only if the **ngAnimate** module is loaded.
*
*
* Defines an animation hook that can be later used with
* {@link ngAnimate.$animate $animate} service and directives that use this service.
*
* <pre>
* module.animation('.animation-name', function($inject1, $inject2) {
* return {
* eventName : function(element, done) {
* //code to run the animation
* //once complete, then run done()
* return function cancellationFunction(element) {
* //code to cancel the animation
* }
* }
* }
* })
* </pre>
*
* See {@link ngAnimate.$animateProvider#register $animateProvider.register()} and
* {@link ngAnimate ngAnimate module} for more information.
*/
animation: invokeLater('$animateProvider', 'register'),
/**
* @ngdoc method
* @name angular.Module#filter
* @methodOf angular.Module
* @param {string} name Filter name.
* @param {Function} filterFactory Factory function for creating new instance of filter.
* @description
* See {@link ng.$filterProvider#register $filterProvider.register()}.
*/
filter: invokeLater('$filterProvider', 'register'),
/**
* @ngdoc method
* @name angular.Module#controller
* @methodOf angular.Module
* @param {string|Object} name Controller name, or an object map of controllers where the
* keys are the names and the values are the constructors.
* @param {Function} constructor Controller constructor function.
* @description
* See {@link ng.$controllerProvider#register $controllerProvider.register()}.
*/
controller: invokeLater('$controllerProvider', 'register'),
/**
* @ngdoc method
* @name angular.Module#directive
* @methodOf angular.Module
* @param {string|Object} name Directive name, or an object map of directives where the
* keys are the names and the values are the factories.
* @param {Function} directiveFactory Factory function for creating new instance of
* directives.
* @description
* See {@link ng.$compileProvider#methods_directive $compileProvider.directive()}.
*/
directive: invokeLater('$compileProvider', 'directive'),
/**
* @ngdoc method
* @name angular.Module#config
* @methodOf angular.Module
* @param {Function} configFn Execute this function on module load. Useful for service
* configuration.
* @description
* Use this method to register work which needs to be performed on module loading.
*/
config: config,
/**
* @ngdoc method
* @name angular.Module#run
* @methodOf angular.Module
* @param {Function} initializationFn Execute this function after injector creation.
* Useful for application initialization.
* @description
* Use this method to register work which should be performed when the injector is done
* loading all modules.
*/
run: function(block) {
runBlocks.push(block);
return this;
}
};
if (configFn) {
config(configFn);
}
return moduleInstance;
/**
* @param {string} provider
* @param {string} method
* @param {String=} insertMethod
* @returns {angular.Module}
*/
function invokeLater(provider, method, insertMethod) {
return function() {
invokeQueue[insertMethod || 'push']([provider, method, arguments]);
return moduleInstance;
};
}
});
};
});
}
setupModuleLoader(window);
})(window);
/**
* Closure compiler type information
*
* @typedef { {
* requires: !Array.<string>,
* invokeQueue: !Array.<Array.<*>>,
*
* service: function(string, Function):angular.Module,
* factory: function(string, Function):angular.Module,
* value: function(string, *):angular.Module,
*
* filter: function(string, Function):angular.Module,
*
* init: function(Function):angular.Module
* } }
*/
angular.Module;

9
app/vendor/angular/angular-loader.min.js vendored

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
/*
AngularJS v1.2.3
(c) 2010-2014 Google, Inc. http://angularjs.org
License: MIT
*/
(function(){'use strict';function d(a){return function(){var c=arguments[0],b,c="["+(a?a+":":"")+c+"] http://errors.angularjs.org/1.2.3/"+(a?a+"/":"")+c;for(b=1;b<arguments.length;b++)c=c+(1==b?"?":"&")+"p"+(b-1)+"="+encodeURIComponent("function"==typeof arguments[b]?arguments[b].toString().replace(/ \{[\s\S]*$/,""):"undefined"==typeof arguments[b]?"undefined":"string"!=typeof arguments[b]?JSON.stringify(arguments[b]):arguments[b]);return Error(c)}}(function(a){var c=d("$injector"),b=d("ng");a=a.angular||
(a.angular={});a.$$minErr=a.$$minErr||d;return a.module||(a.module=function(){var a={};return function(e,d,f){if("hasOwnProperty"===e)throw b("badname","module");d&&a.hasOwnProperty(e)&&(a[e]=null);return a[e]||(a[e]=function(){function a(c,d,e){return function(){b[e||"push"]([c,d,arguments]);return g}}if(!d)throw c("nomod",e);var b=[],h=[],k=a("$injector","invoke"),g={_invokeQueue:b,_runBlocks:h,requires:d,name:e,provider:a("$provide","provider"),factory:a("$provide","factory"),service:a("$provide",
"service"),value:a("$provide","value"),constant:a("$provide","constant","unshift"),animation:a("$animateProvider","register"),filter:a("$filterProvider","register"),controller:a("$controllerProvider","register"),directive:a("$compileProvider","directive"),config:k,run:function(a){h.push(a);return this}};f&&k(f);return g}())}}())})(window)})(window);
//# sourceMappingURL=angular-loader.min.js.map

8
app/vendor/angular/angular-loader.min.js.map vendored

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
{
"version":3,
"file":"angular-loader.min.js",
"lineCount":8,
"mappings":"A;;;;;aAMC,SAAQ,EAAG,CCNZA,QAAS,EAAM,CAAC,CAAD,CAAS,CAWtB,MAAO,SAAS,EAAG,CAAA,IACb,EAAO,SAAA,CAAU,CAAV,CADM,CAIf,CAJe,CAKjB,EAHW,GAGX,EAHkB,CAAA,CAAS,CAAT,CAAkB,GAAlB,CAAwB,EAG1C,EAHgD,CAGhD,CAAmB,sCAAnB,EAA2D,CAAA,CAAS,CAAT,CAAkB,GAAlB,CAAwB,EAAnF,EAAyF,CACzF,KAAK,CAAL,CAAS,CAAT,CAAY,CAAZ,CAAgB,SAAA,OAAhB,CAAkC,CAAA,EAAlC,CACE,CAAA,CAAU,CAAV,EAA0B,CAAL,EAAA,CAAA,CAAS,GAAT,CAAe,GAApC,EAA2C,GAA3C,EAAkD,CAAlD,CAAoD,CAApD,EAAyD,GAAzD,CACE,kBAAA,CAjBc,UAAlB,EAAI,MAiB6B,UAAA,CAAU,CAAV,CAjBjC,CAiBiC,SAAA,CAAU,CAAV,CAhBxB,SAAA,EAAA,QAAA,CAAuB,aAAvB,CAAsC,EAAtC,CADT,CAEyB,WAAlB,EAAI,MAesB,UAAA,CAAU,CAAV,CAf1B,CACE,WADF,CAEoB,QAApB,EAAM,MAaoB,UAAA,CAAU,CAAV,CAb1B,CACE,IAAA,UAAA,CAYwB,SAAA,CAAU,CAAV,CAZxB,CADF,CAa0B,SAAA,CAAU,CAAV,CAA7B,CAEJ,OAAW,MAAJ,CAAU,CAAV,CAVU,CAXG,CD0FxBC,SAA0B,CAACC,CAAD,CAAS,CAEjC,IAAIC,EAAkBH,CAAA,CAAO,WAAP,CAAtB,CACII,EAAWJ,CAAA,CAAO,IAAP,CAMXK,EAAAA,CAAiBH,CAHZ,QAGLG;CAAiBH,CAHE,QAGnBG,CAH+B,EAG/BA,CAGJA,EAAAC,SAAA,CAAmBD,CAAAC,SAAnB,EAAuCN,CAEvC,OAAcK,EARL,OAQT,GAAcA,CARS,OAQvB,CAAiCE,QAAQ,EAAG,CAE1C,IAAIC,EAAU,EAoDd,OAAOC,SAAe,CAACC,CAAD,CAAOC,CAAP,CAAiBC,CAAjB,CAA2B,CAE7C,GAAa,gBAAb,GAKsBF,CALtB,CACE,KAAMN,EAAA,CAAS,SAAT,CAIoBS,QAJpB,CAAN,CAKAF,CAAJ,EAAgBH,CAAAM,eAAA,CAAuBJ,CAAvB,CAAhB,GACEF,CAAA,CAAQE,CAAR,CADF,CACkB,IADlB,CAGA,OAAcF,EAzET,CAyEkBE,CAzElB,CAyEL,GAAcF,CAzEK,CAyEIE,CAzEJ,CAyEnB,CAA6BH,QAAQ,EAAG,CAgNtCQ,QAASA,EAAW,CAACC,CAAD,CAAWC,CAAX,CAAmBC,CAAnB,CAAiC,CACnD,MAAO,SAAQ,EAAG,CAChBC,CAAA,CAAYD,CAAZ,EAA4B,MAA5B,CAAA,CAAoC,CAACF,CAAD,CAAWC,CAAX,CAAmBG,SAAnB,CAApC,CACA,OAAOC,EAFS,CADiC,CA/MrD,GAAI,CAACV,CAAL,CACE,KAAMR,EAAA,CAAgB,OAAhB,CAEiDO,CAFjD,CAAN,CAMF,IAAIS,EAAc,EAAlB,CAGIG,EAAY,EAHhB,CAKIC,EAASR,CAAA,CAAY,WAAZ,CAAyB,QAAzB,CALb,CAQIM,EAAiB,cAELF,CAFK,YAGPG,CAHO,UAcTX,CAdS,MAuBbD,CAvBa,UAoCTK,CAAA,CAAY,UAAZ,CAAwB,UAAxB,CApCS,SA+CVA,CAAA,CAAY,UAAZ,CAAwB,SAAxB,CA/CU,SA0DVA,CAAA,CAAY,UAAZ;AAAwB,SAAxB,CA1DU,OAqEZA,CAAA,CAAY,UAAZ,CAAwB,OAAxB,CArEY,UAiFTA,CAAA,CAAY,UAAZ,CAAwB,UAAxB,CAAoC,SAApC,CAjFS,WAmHRA,CAAA,CAAY,kBAAZ,CAAgC,UAAhC,CAnHQ,QA8HXA,CAAA,CAAY,iBAAZ,CAA+B,UAA/B,CA9HW,YA0IPA,CAAA,CAAY,qBAAZ,CAAmC,UAAnC,CA1IO,WAuJRA,CAAA,CAAY,kBAAZ,CAAgC,WAAhC,CAvJQ,QAkKXQ,CAlKW,KA8KdC,QAAQ,CAACC,CAAD,CAAQ,CACnBH,CAAAI,KAAA,CAAeD,CAAf,CACA,OAAO,KAFY,CA9KF,CAoLjBb,EAAJ,EACEW,CAAA,CAAOX,CAAP,CAGF,OAAQS,EAxM8B,CAzET,EAyE/B,CAX+C,CAtDP,CART,EAQnC,CAdiC,CAAnCpB,CA2SA,CAAkBC,MAAlB,CA/XY,CAAX,CAAA,CAgYEA,MAhYF;",
"sources":["angular-loader.js","MINERR_ASSET"],
"names":["minErr","setupModuleLoader","window","$injectorMinErr","ngMinErr","angular","$$minErr","factory","modules","module","name","requires","configFn","context","hasOwnProperty","invokeLater","provider","method","insertMethod","invokeQueue","arguments","moduleInstance","runBlocks","config","run","block","push"]
}

546
app/vendor/angular/angular-resource.js vendored

@ -0,0 +1,546 @@ @@ -0,0 +1,546 @@
/**
* @license AngularJS v1.2.3
* (c) 2010-2014 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, angular, undefined) {'use strict';
var $resourceMinErr = angular.$$minErr('$resource');
// Helper functions and regex to lookup a dotted path on an object
// stopping at undefined/null. The path must be composed of ASCII
// identifiers (just like $parse)
var MEMBER_NAME_REGEX = /^(\.[a-zA-Z_$][0-9a-zA-Z_$]*)+$/;
function isValidDottedPath(path) {
return (path != null && path !== '' && path !== 'hasOwnProperty' &&
MEMBER_NAME_REGEX.test('.' + path));
}
function lookupDottedPath(obj, path) {
if (!isValidDottedPath(path)) {
throw $resourceMinErr('badmember', 'Dotted member path "@{0}" is invalid.', path);
}
var keys = path.split('.');
for (var i = 0, ii = keys.length; i < ii && obj !== undefined; i++) {
var key = keys[i];
obj = (obj !== null) ? obj[key] : undefined;
}
return obj;
}
/**
* @ngdoc overview
* @name ngResource
* @description
*
* # ngResource
*
* The `ngResource` module provides interaction support with RESTful services
* via the $resource service.
*
* {@installModule resource}
*
* <div doc-module-components="ngResource"></div>
*
* See {@link ngResource.$resource `$resource`} for usage.
*/
/**
* @ngdoc object
* @name ngResource.$resource
* @requires $http
*
* @description
* A factory which creates a resource object that lets you interact with
* [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources.
*
* The returned resource object has action methods which provide high-level behaviors without
* the need to interact with the low level {@link ng.$http $http} service.
*
* Requires the {@link ngResource `ngResource`} module to be installed.
*
* @param {string} url A parametrized URL template with parameters prefixed by `:` as in
* `/user/:username`. If you are using a URL with a port number (e.g.
* `http://example.com:8080/api`), it will be respected.
*
* If you are using a url with a suffix, just add the suffix, like this:
* `$resource('http://example.com/resource.json')` or `$resource('http://example.com/:id.json')`
* or even `$resource('http://example.com/resource/:resource_id.:format')`
* If the parameter before the suffix is empty, :resource_id in this case, then the `/.` will be
* collapsed down to a single `.`. If you need this sequence to appear and not collapse then you
* can escape it with `/\.`.
*
* @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in
* `actions` methods. If any of the parameter value is a function, it will be executed every time
* when a param value needs to be obtained for a request (unless the param was overridden).
*
* Each key value in the parameter object is first bound to url template if present and then any
* excess keys are appended to the url search query after the `?`.
*
* Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in
* URL `/path/greet?salutation=Hello`.
*
* If the parameter value is prefixed with `@` then the value of that parameter is extracted from
* the data object (useful for non-GET operations).
*
* @param {Object.<Object>=} actions Hash with declaration of custom action that should extend the
* default set of resource actions. The declaration should be created in the format of {@link
* ng.$http#usage_parameters $http.config}:
*
* {action1: {method:?, params:?, isArray:?, headers:?, ...},
* action2: {method:?, params:?, isArray:?, headers:?, ...},
* ...}
*
* Where:
*
* - **`action`** {string} The name of action. This name becomes the name of the method on
* your resource object.
* - **`method`** {string} HTTP request method. Valid methods are: `GET`, `POST`, `PUT`,
* `DELETE`, and `JSONP`.
* - **`params`** {Object=} Optional set of pre-bound parameters for this action. If any of
* the parameter value is a function, it will be executed every time when a param value needs to
* be obtained for a request (unless the param was overridden).
* - **`url`** {string} action specific `url` override. The url templating is supported just
* like for the resource-level urls.
* - **`isArray`** {boolean=} If true then the returned object for this action is an array,
* see `returns` section.
* - **`transformRequest`**
* `{function(data, headersGetter)|Array.<function(data, headersGetter)>}`
* transform function or an array of such functions. The transform function takes the http
* request body and headers and returns its transformed (typically serialized) version.
* - **`transformResponse`**
* `{function(data, headersGetter)|Array.<function(data, headersGetter)>}`
* transform function or an array of such functions. The transform function takes the http
* response body and headers and returns its transformed (typically deserialized) version.
* - **`cache`** `{boolean|Cache}` If true, a default $http cache will be used to cache the
* GET request, otherwise if a cache instance built with
* {@link ng.$cacheFactory $cacheFactory}, this cache will be used for
* caching.
* - **`timeout`** `{number|Promise}` timeout in milliseconds, or {@link ng.$q promise} that
* should abort the request when resolved.
* - **`withCredentials`** - `{boolean}` - whether to set the `withCredentials` flag on the
* XHR object. See {@link https://developer.mozilla.org/en/http_access_control#section_5
* requests with credentials} for more information.
* - **`responseType`** - `{string}` - see {@link
* https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType requestType}.
* - **`interceptor`** - `{Object=}` - The interceptor object has two optional methods -
* `response` and `responseError`. Both `response` and `responseError` interceptors get called
* with `http response` object. See {@link ng.$http $http interceptors}.
*
* @returns {Object} A resource "class" object with methods for the default set of resource actions
* optionally extended with custom `actions`. The default set contains these actions:
*
* { 'get': {method:'GET'},
* 'save': {method:'POST'},
* 'query': {method:'GET', isArray:true},
* 'remove': {method:'DELETE'},
* 'delete': {method:'DELETE'} };
*
* Calling these methods invoke an {@link ng.$http} with the specified http method,
* destination and parameters. When the data is returned from the server then the object is an
* instance of the resource class. The actions `save`, `remove` and `delete` are available on it
* as methods with the `$` prefix. This allows you to easily perform CRUD operations (create,
* read, update, delete) on server-side data like this:
* <pre>
var User = $resource('/user/:userId', {userId:'@id'});
var user = User.get({userId:123}, function() {
user.abc = true;
user.$save();
});
</pre>
*
* It is important to realize that invoking a $resource object method immediately returns an
* empty reference (object or array depending on `isArray`). Once the data is returned from the
* server the existing reference is populated with the actual data. This is a useful trick since
* usually the resource is assigned to a model which is then rendered by the view. Having an empty
* object results in no rendering, once the data arrives from the server then the object is
* populated with the data and the view automatically re-renders itself showing the new data. This
* means that in most cases one never has to write a callback function for the action methods.
*
* The action methods on the class object or instance object can be invoked with the following
* parameters:
*
* - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])`
* - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])`
* - non-GET instance actions: `instance.$action([parameters], [success], [error])`
*
* Success callback is called with (value, responseHeaders) arguments. Error callback is called
* with (httpResponse) argument.
*
* Class actions return empty instance (with additional properties below).
* Instance actions return promise of the action.
*
* The Resource instances and collection have these additional properties:
*
* - `$promise`: the {@link ng.$q promise} of the original server interaction that created this
* instance or collection.
*
* On success, the promise is resolved with the same resource instance or collection object,
* updated with data from server. This makes it easy to use in
* {@link ngRoute.$routeProvider resolve section of $routeProvider.when()} to defer view
* rendering until the resource(s) are loaded.
*
* On failure, the promise is resolved with the {@link ng.$http http response} object, without
* the `resource` property.
*
* - `$resolved`: `true` after first server interaction is completed (either with success or
* rejection), `false` before that. Knowing if the Resource has been resolved is useful in
* data-binding.
*
* @example
*
* # Credit card resource
*
* <pre>
// Define CreditCard class
var CreditCard = $resource('/user/:userId/card/:cardId',
{userId:123, cardId:'@id'}, {
charge: {method:'POST', params:{charge:true}}
});
// We can retrieve a collection from the server
var cards = CreditCard.query(function() {
// GET: /user/123/card
// server returns: [ {id:456, number:'1234', name:'Smith'} ];
var card = cards[0];
// each item is an instance of CreditCard
expect(card instanceof CreditCard).toEqual(true);
card.name = "J. Smith";
// non GET methods are mapped onto the instances
card.$save();
// POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'}
// server returns: {id:456, number:'1234', name: 'J. Smith'};
// our custom method is mapped as well.
card.$charge({amount:9.99});
// POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'}
});
// we can create an instance as well
var newCard = new CreditCard({number:'0123'});
newCard.name = "Mike Smith";
newCard.$save();
// POST: /user/123/card {number:'0123', name:'Mike Smith'}
// server returns: {id:789, number:'01234', name: 'Mike Smith'};
expect(newCard.id).toEqual(789);
* </pre>
*
* The object returned from this function execution is a resource "class" which has "static" method
* for each action in the definition.
*
* Calling these methods invoke `$http` on the `url` template with the given `method`, `params` and
* `headers`.
* When the data is returned from the server then the object is an instance of the resource type and
* all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD
* operations (create, read, update, delete) on server-side data.
<pre>
var User = $resource('/user/:userId', {userId:'@id'});
var user = User.get({userId:123}, function() {
user.abc = true;
user.$save();
});
</pre>
*
* It's worth noting that the success callback for `get`, `query` and other methods gets passed
* in the response that came from the server as well as $http header getter function, so one
* could rewrite the above example and get access to http headers as:
*
<pre>
var User = $resource('/user/:userId', {userId:'@id'});
User.get({userId:123}, function(u, getResponseHeaders){
u.abc = true;
u.$save(function(u, putResponseHeaders) {
//u => saved user object
//putResponseHeaders => $http header getter
});
});
</pre>
*/
angular.module('ngResource', ['ng']).
factory('$resource', ['$http', '$q', function($http, $q) {
var DEFAULT_ACTIONS = {
'get': {method:'GET'},
'save': {method:'POST'},
'query': {method:'GET', isArray:true},
'remove': {method:'DELETE'},
'delete': {method:'DELETE'}
};
var noop = angular.noop,
forEach = angular.forEach,
extend = angular.extend,
copy = angular.copy,
isFunction = angular.isFunction;
/**
* We need our custom method because encodeURIComponent is too aggressive and doesn't follow
* http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path
* segments:
* segment = *pchar
* pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
* pct-encoded = "%" HEXDIG HEXDIG
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
* sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
* / "*" / "+" / "," / ";" / "="
*/
function encodeUriSegment(val) {
return encodeUriQuery(val, true).
replace(/%26/gi, '&').
replace(/%3D/gi, '=').
replace(/%2B/gi, '+');
}
/**
* This method is intended for encoding *key* or *value* parts of query component. We need a
* custom method because encodeURIComponent is too aggressive and encodes stuff that doesn't
* have to be encoded per http://tools.ietf.org/html/rfc3986:
* query = *( pchar / "/" / "?" )
* pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
* pct-encoded = "%" HEXDIG HEXDIG
* sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
* / "*" / "+" / "," / ";" / "="
*/
function encodeUriQuery(val, pctEncodeSpaces) {
return encodeURIComponent(val).
replace(/%40/gi, '@').
replace(/%3A/gi, ':').
replace(/%24/g, '$').
replace(/%2C/gi, ',').
replace(/%20/g, (pctEncodeSpaces ? '%20' : '+'));
}
function Route(template, defaults) {
this.template = template;
this.defaults = defaults || {};
this.urlParams = {};
}
Route.prototype = {
setUrlParams: function(config, params, actionUrl) {
var self = this,
url = actionUrl || self.template,
val,
encodedVal;
var urlParams = self.urlParams = {};
forEach(url.split(/\W/), function(param){
if (param === 'hasOwnProperty') {
throw $resourceMinErr('badname', "hasOwnProperty is not a valid parameter name.");
}
if (!(new RegExp("^\\d+$").test(param)) && param &&
(new RegExp("(^|[^\\\\]):" + param + "(\\W|$)").test(url))) {
urlParams[param] = true;
}
});
url = url.replace(/\\:/g, ':');
params = params || {};
forEach(self.urlParams, function(_, urlParam){
val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam];
if (angular.isDefined(val) && val !== null) {
encodedVal = encodeUriSegment(val);
url = url.replace(new RegExp(":" + urlParam + "(\\W|$)", "g"), encodedVal + "$1");
} else {
url = url.replace(new RegExp("(\/?):" + urlParam + "(\\W|$)", "g"), function(match,
leadingSlashes, tail) {
if (tail.charAt(0) == '/') {
return tail;
} else {
return leadingSlashes + tail;
}
});
}
});
// strip trailing slashes and set the url
url = url.replace(/\/+$/, '');
// then replace collapse `/.` if found in the last URL path segment before the query
// E.g. `http://url.com/id./format?q=x` becomes `http://url.com/id.format?q=x`
url = url.replace(/\/\.(?=\w+($|\?))/, '.');
// replace escaped `/\.` with `/.`
config.url = url.replace(/\/\\\./, '/.');
// set params - delegate param encoding to $http
forEach(params, function(value, key){
if (!self.urlParams[key]) {
config.params = config.params || {};
config.params[key] = value;
}
});
}
};
function resourceFactory(url, paramDefaults, actions) {
var route = new Route(url);
actions = extend({}, DEFAULT_ACTIONS, actions);
function extractParams(data, actionParams){
var ids = {};
actionParams = extend({}, paramDefaults, actionParams);
forEach(actionParams, function(value, key){
if (isFunction(value)) { value = value(); }
ids[key] = value && value.charAt && value.charAt(0) == '@' ?
lookupDottedPath(data, value.substr(1)) : value;
});
return ids;
}
function defaultResponseInterceptor(response) {
return response.resource;
}
function Resource(value){
copy(value || {}, this);
}
forEach(actions, function(action, name) {
var hasBody = /^(POST|PUT|PATCH)$/i.test(action.method);
Resource[name] = function(a1, a2, a3, a4) {
var params = {}, data, success, error;
/* jshint -W086 */ /* (purposefully fall through case statements) */
switch(arguments.length) {
case 4:
error = a4;
success = a3;
//fallthrough
case 3:
case 2:
if (isFunction(a2)) {
if (isFunction(a1)) {
success = a1;
error = a2;
break;
}
success = a2;
error = a3;
//fallthrough
} else {
params = a1;
data = a2;
success = a3;
break;
}
case 1:
if (isFunction(a1)) success = a1;
else if (hasBody) data = a1;
else params = a1;
break;
case 0: break;
default:
throw $resourceMinErr('badargs',
"Expected up to 4 arguments [params, data, success, error], got {0} arguments",
arguments.length);
}
/* jshint +W086 */ /* (purposefully fall through case statements) */
var isInstanceCall = this instanceof Resource;
var value = isInstanceCall ? data : (action.isArray ? [] : new Resource(data));
var httpConfig = {};
var responseInterceptor = action.interceptor && action.interceptor.response ||
defaultResponseInterceptor;
var responseErrorInterceptor = action.interceptor && action.interceptor.responseError ||
undefined;
forEach(action, function(value, key) {
if (key != 'params' && key != 'isArray' && key != 'interceptor') {
httpConfig[key] = copy(value);
}
});
if (hasBody) httpConfig.data = data;
route.setUrlParams(httpConfig,
extend({}, extractParams(data, action.params || {}), params),
action.url);
var promise = $http(httpConfig).then(function(response) {
var data = response.data,
promise = value.$promise;
if (data) {
// Need to convert action.isArray to boolean in case it is undefined
// jshint -W018
if ( angular.isArray(data) !== (!!action.isArray) ) {
throw $resourceMinErr('badcfg', 'Error in resource configuration. Expected ' +
'response to contain an {0} but got an {1}',
action.isArray?'array':'object', angular.isArray(data)?'array':'object');
}
// jshint +W018
if (action.isArray) {
value.length = 0;
forEach(data, function(item) {
value.push(new Resource(item));
});
} else {
copy(data, value);
value.$promise = promise;
}
}
value.$resolved = true;
response.resource = value;
return response;
}, function(response) {
value.$resolved = true;
(error||noop)(response);
return $q.reject(response);
});
promise = promise.then(
function(response) {
var value = responseInterceptor(response);
(success||noop)(value, response.headers);
return value;
},
responseErrorInterceptor);
if (!isInstanceCall) {
// we are creating instance / collection
// - set the initial promise
// - return the instance / collection
value.$promise = promise;
value.$resolved = false;
return value;
}
// instance call
return promise;
};
Resource.prototype['$' + name] = function(params, success, error) {
if (isFunction(params)) {
error = success; success = params; params = {};
}
var result = Resource[name].call(this, params, this, success, error);
return result.$promise || result;
};
});
Resource.bind = function(additionalParamDefaults){
return resourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions);
};
return Resource;
}
return resourceFactory;
}]);
})(window, window.angular);

12
app/vendor/angular/angular-resource.min.js vendored

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
/*
AngularJS v1.2.3
(c) 2010-2014 Google, Inc. http://angularjs.org
License: MIT
*/
(function(H,f,z){'use strict';var u=f.$$minErr("$resource"),A=/^(\.[a-zA-Z_$][0-9a-zA-Z_$]*)+$/;f.module("ngResource",["ng"]).factory("$resource",["$http","$q",function(D,E){function n(f,h){this.template=f;this.defaults=h||{};this.urlParams={}}function v(m,h,k){function r(d,c){var e={};c=w({},h,c);s(c,function(a,c){t(a)&&(a=a());var g;if(a&&a.charAt&&"@"==a.charAt(0)){g=d;var b=a.substr(1);if(null==b||""===b||"hasOwnProperty"===b||!A.test("."+b))throw u("badmember",b);for(var b=b.split("."),f=0,h=
b.length;f<h&&g!==z;f++){var q=b[f];g=null!==g?g[q]:z}}else g=a;e[c]=g});return e}function e(b){return b.resource}function b(b){B(b||{},this)}var F=new n(m);k=w({},G,k);s(k,function(d,c){var h=/^(POST|PUT|PATCH)$/i.test(d.method);b[c]=function(a,c,g,m){var p={},k,q,x;switch(arguments.length){case 4:x=m,q=g;case 3:case 2:if(t(c)){if(t(a)){q=a;x=c;break}q=c;x=g}else{p=a;k=c;q=g;break}case 1:t(a)?q=a:h?k=a:p=a;break;case 0:break;default:throw u("badargs",arguments.length);}var n=this instanceof b,l=
n?k:d.isArray?[]:new b(k),y={},v=d.interceptor&&d.interceptor.response||e,A=d.interceptor&&d.interceptor.responseError||z;s(d,function(b,a){"params"!=a&&("isArray"!=a&&"interceptor"!=a)&&(y[a]=B(b))});h&&(y.data=k);F.setUrlParams(y,w({},r(k,d.params||{}),p),d.url);p=D(y).then(function(a){var c=a.data,g=l.$promise;if(c){if(f.isArray(c)!==!!d.isArray)throw u("badcfg",d.isArray?"array":"object",f.isArray(c)?"array":"object");d.isArray?(l.length=0,s(c,function(a){l.push(new b(a))})):(B(c,l),l.$promise=
g)}l.$resolved=!0;a.resource=l;return a},function(a){l.$resolved=!0;(x||C)(a);return E.reject(a)});p=p.then(function(a){var c=v(a);(q||C)(c,a.headers);return c},A);return n?p:(l.$promise=p,l.$resolved=!1,l)};b.prototype["$"+c]=function(a,d,g){t(a)&&(g=d,d=a,a={});a=b[c].call(this,a,this,d,g);return a.$promise||a}});b.bind=function(b){return v(m,w({},h,b),k)};return b}var G={get:{method:"GET"},save:{method:"POST"},query:{method:"GET",isArray:!0},remove:{method:"DELETE"},"delete":{method:"DELETE"}},
C=f.noop,s=f.forEach,w=f.extend,B=f.copy,t=f.isFunction;n.prototype={setUrlParams:function(m,h,k){var r=this,e=k||r.template,b,n,d=r.urlParams={};s(e.split(/\W/),function(c){if("hasOwnProperty"===c)throw u("badname");!/^\d+$/.test(c)&&(c&&RegExp("(^|[^\\\\]):"+c+"(\\W|$)").test(e))&&(d[c]=!0)});e=e.replace(/\\:/g,":");h=h||{};s(r.urlParams,function(c,d){b=h.hasOwnProperty(d)?h[d]:r.defaults[d];f.isDefined(b)&&null!==b?(n=encodeURIComponent(b).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,
"$").replace(/%2C/gi,",").replace(/%20/g,"%20").replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+"),e=e.replace(RegExp(":"+d+"(\\W|$)","g"),n+"$1")):e=e.replace(RegExp("(/?):"+d+"(\\W|$)","g"),function(a,c,b){return"/"==b.charAt(0)?b:c+b})});e=e.replace(/\/+$/,"");e=e.replace(/\/\.(?=\w+($|\?))/,".");m.url=e.replace(/\/\\\./,"/.");s(h,function(b,d){r.urlParams[d]||(m.params=m.params||{},m.params[d]=b)})}};return v}])})(window,window.angular);
//# sourceMappingURL=angular-resource.min.js.map

8
app/vendor/angular/angular-resource.min.js.map vendored

File diff suppressed because one or more lines are too long

891
app/vendor/angular/angular-route.js vendored

@ -0,0 +1,891 @@ @@ -0,0 +1,891 @@
/**
* @license AngularJS v1.2.3
* (c) 2010-2014 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, angular, undefined) {'use strict';
/**
* @ngdoc overview
* @name ngRoute
* @description
*
* # ngRoute
*
* The `ngRoute` module provides routing and deeplinking services and directives for angular apps.
*
* ## Example
* See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`.
*
* {@installModule route}
*
* <div doc-module-components="ngRoute"></div>
*/
/* global -ngRouteModule */
var ngRouteModule = angular.module('ngRoute', ['ng']).
provider('$route', $RouteProvider);
/**
* @ngdoc object
* @name ngRoute.$routeProvider
* @function
*
* @description
*
* Used for configuring routes.
*
* ## Example
* See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`.
*
* ## Dependencies
* Requires the {@link ngRoute `ngRoute`} module to be installed.
*/
function $RouteProvider(){
function inherit(parent, extra) {
return angular.extend(new (angular.extend(function() {}, {prototype:parent}))(), extra);
}
var routes = {};
/**
* @ngdoc method
* @name ngRoute.$routeProvider#when
* @methodOf ngRoute.$routeProvider
*
* @param {string} path Route path (matched against `$location.path`). If `$location.path`
* contains redundant trailing slash or is missing one, the route will still match and the
* `$location.path` will be updated to add or drop the trailing slash to exactly match the
* route definition.
*
* * `path` can contain named groups starting with a colon (`:name`). All characters up
* to the next slash are matched and stored in `$routeParams` under the given `name`
* when the route matches.
* * `path` can contain named groups starting with a colon and ending with a star (`:name*`).
* All characters are eagerly stored in `$routeParams` under the given `name`
* when the route matches.
* * `path` can contain optional named groups with a question mark (`:name?`).
*
* For example, routes like `/color/:color/largecode/:largecode*\/edit` will match
* `/color/brown/largecode/code/with/slashs/edit` and extract:
*
* * `color: brown`
* * `largecode: code/with/slashs`.
*
*
* @param {Object} route Mapping information to be assigned to `$route.current` on route
* match.
*
* Object properties:
*
* - `controller` `{(string|function()=}` Controller fn that should be associated with
* newly created scope or the name of a {@link angular.Module#controller registered
* controller} if passed as a string.
* - `controllerAs` `{string=}` A controller alias name. If present the controller will be
* published to scope under the `controllerAs` name.
* - `template` `{string=|function()=}` html template as a string or a function that
* returns an html template as a string which should be used by {@link
* ngRoute.directive:ngView ngView} or {@link ng.directive:ngInclude ngInclude} directives.
* This property takes precedence over `templateUrl`.
*
* If `template` is a function, it will be called with the following parameters:
*
* - `{Array.<Object>}` - route parameters extracted from the current
* `$location.path()` by applying the current route
*
* - `templateUrl` `{string=|function()=}` path or function that returns a path to an html
* template that should be used by {@link ngRoute.directive:ngView ngView}.
*
* If `templateUrl` is a function, it will be called with the following parameters:
*
* - `{Array.<Object>}` - route parameters extracted from the current
* `$location.path()` by applying the current route
*
* - `resolve` - `{Object.<string, function>=}` - An optional map of dependencies which should
* be injected into the controller. If any of these dependencies are promises, the router
* will wait for them all to be resolved or one to be rejected before the controller is
* instantiated.
* If all the promises are resolved successfully, the values of the resolved promises are
* injected and {@link ngRoute.$route#$routeChangeSuccess $routeChangeSuccess} event is
* fired. If any of the promises are rejected the
* {@link ngRoute.$route#$routeChangeError $routeChangeError} event is fired. The map object
* is:
*
* - `key` `{string}`: a name of a dependency to be injected into the controller.
* - `factory` - `{string|function}`: If `string` then it is an alias for a service.
* Otherwise if function, then it is {@link api/AUTO.$injector#invoke injected}
* and the return value is treated as the dependency. If the result is a promise, it is
* resolved before its value is injected into the controller. Be aware that
* `ngRoute.$routeParams` will still refer to the previous route within these resolve
* functions. Use `$route.current.params` to access the new route parameters, instead.
*
* - `redirectTo` {(string|function())=} value to update
* {@link ng.$location $location} path with and trigger route redirection.
*
* If `redirectTo` is a function, it will be called with the following parameters:
*
* - `{Object.<string>}` - route parameters extracted from the current
* `$location.path()` by applying the current route templateUrl.
* - `{string}` - current `$location.path()`
* - `{Object}` - current `$location.search()`
*
* The custom `redirectTo` function is expected to return a string which will be used
* to update `$location.path()` and `$location.search()`.
*
* - `[reloadOnSearch=true]` - {boolean=} - reload route when only `$location.search()`
* or `$location.hash()` changes.
*
* If the option is set to `false` and url in the browser changes, then
* `$routeUpdate` event is broadcasted on the root scope.
*
* - `[caseInsensitiveMatch=false]` - {boolean=} - match routes without being case sensitive
*
* If the option is set to `true`, then the particular route can be matched without being
* case sensitive
*
* @returns {Object} self
*
* @description
* Adds a new route definition to the `$route` service.
*/
this.when = function(path, route) {
routes[path] = angular.extend(
{reloadOnSearch: true},
route,
path && pathRegExp(path, route)
);
// create redirection for trailing slashes
if (path) {
var redirectPath = (path[path.length-1] == '/')
? path.substr(0, path.length-1)
: path +'/';
routes[redirectPath] = angular.extend(
{redirectTo: path},
pathRegExp(redirectPath, route)
);
}
return this;
};
/**
* @param path {string} path
* @param opts {Object} options
* @return {?Object}
*
* @description
* Normalizes the given path, returning a regular expression
* and the original path.
*
* Inspired by pathRexp in visionmedia/express/lib/utils.js.
*/
function pathRegExp(path, opts) {
var insensitive = opts.caseInsensitiveMatch,
ret = {
originalPath: path,
regexp: path
},
keys = ret.keys = [];
path = path
.replace(/([().])/g, '\\$1')
.replace(/(\/)?:(\w+)([\?|\*])?/g, function(_, slash, key, option){
var optional = option === '?' ? option : null;
var star = option === '*' ? option : null;
keys.push({ name: key, optional: !!optional });
slash = slash || '';
return ''
+ (optional ? '' : slash)
+ '(?:'
+ (optional ? slash : '')
+ (star && '(.+?)' || '([^/]+)')
+ (optional || '')
+ ')'
+ (optional || '');
})
.replace(/([\/$\*])/g, '\\$1');
ret.regexp = new RegExp('^' + path + '$', insensitive ? 'i' : '');
return ret;
}
/**
* @ngdoc method
* @name ngRoute.$routeProvider#otherwise
* @methodOf ngRoute.$routeProvider
*
* @description
* Sets route definition that will be used on route change when no other route definition
* is matched.
*
* @param {Object} params Mapping information to be assigned to `$route.current`.
* @returns {Object} self
*/
this.otherwise = function(params) {
this.when(null, params);
return this;
};
this.$get = ['$rootScope',
'$location',
'$routeParams',
'$q',
'$injector',
'$http',
'$templateCache',
'$sce',
function($rootScope, $location, $routeParams, $q, $injector, $http, $templateCache, $sce) {
/**
* @ngdoc object
* @name ngRoute.$route
* @requires $location
* @requires $routeParams
*
* @property {Object} current Reference to the current route definition.
* The route definition contains:
*
* - `controller`: The controller constructor as define in route definition.
* - `locals`: A map of locals which is used by {@link ng.$controller $controller} service for
* controller instantiation. The `locals` contain
* the resolved values of the `resolve` map. Additionally the `locals` also contain:
*
* - `$scope` - The current route scope.
* - `$template` - The current route template HTML.
*
* @property {Array.<Object>} routes Array of all configured routes.
*
* @description
* `$route` is used for deep-linking URLs to controllers and views (HTML partials).
* It watches `$location.url()` and tries to map the path to an existing route definition.
*
* Requires the {@link ngRoute `ngRoute`} module to be installed.
*
* You can define routes through {@link ngRoute.$routeProvider $routeProvider}'s API.
*
* The `$route` service is typically used in conjunction with the
* {@link ngRoute.directive:ngView `ngView`} directive and the
* {@link ngRoute.$routeParams `$routeParams`} service.
*
* @example
This example shows how changing the URL hash causes the `$route` to match a route against the
URL, and the `ngView` pulls in the partial.
Note that this example is using {@link ng.directive:script inlined templates}
to get it working on jsfiddle as well.
<example module="ngViewExample" deps="angular-route.js">
<file name="index.html">
<div ng-controller="MainCntl">
Choose:
<a href="Book/Moby">Moby</a> |
<a href="Book/Moby/ch/1">Moby: Ch1</a> |
<a href="Book/Gatsby">Gatsby</a> |
<a href="Book/Gatsby/ch/4?key=value">Gatsby: Ch4</a> |
<a href="Book/Scarlet">Scarlet Letter</a><br/>
<div ng-view></div>
<hr />
<pre>$location.path() = {{$location.path()}}</pre>
<pre>$route.current.templateUrl = {{$route.current.templateUrl}}</pre>
<pre>$route.current.params = {{$route.current.params}}</pre>
<pre>$route.current.scope.name = {{$route.current.scope.name}}</pre>
<pre>$routeParams = {{$routeParams}}</pre>
</div>
</file>
<file name="book.html">
controller: {{name}}<br />
Book Id: {{params.bookId}}<br />
</file>
<file name="chapter.html">
controller: {{name}}<br />
Book Id: {{params.bookId}}<br />
Chapter Id: {{params.chapterId}}
</file>
<file name="script.js">
angular.module('ngViewExample', ['ngRoute'])
.config(function($routeProvider, $locationProvider) {
$routeProvider.when('/Book/:bookId', {
templateUrl: 'book.html',
controller: BookCntl,
resolve: {
// I will cause a 1 second delay
delay: function($q, $timeout) {
var delay = $q.defer();
$timeout(delay.resolve, 1000);
return delay.promise;
}
}
});
$routeProvider.when('/Book/:bookId/ch/:chapterId', {
templateUrl: 'chapter.html',
controller: ChapterCntl
});
// configure html5 to get links working on jsfiddle
$locationProvider.html5Mode(true);
});
function MainCntl($scope, $route, $routeParams, $location) {
$scope.$route = $route;
$scope.$location = $location;
$scope.$routeParams = $routeParams;
}
function BookCntl($scope, $routeParams) {
$scope.name = "BookCntl";
$scope.params = $routeParams;
}
function ChapterCntl($scope, $routeParams) {
$scope.name = "ChapterCntl";
$scope.params = $routeParams;
}
</file>
<file name="scenario.js">
it('should load and compile correct template', function() {
element('a:contains("Moby: Ch1")').click();
var content = element('.doc-example-live [ng-view]').text();
expect(content).toMatch(/controller\: ChapterCntl/);
expect(content).toMatch(/Book Id\: Moby/);
expect(content).toMatch(/Chapter Id\: 1/);
element('a:contains("Scarlet")').click();
sleep(2); // promises are not part of scenario waiting
content = element('.doc-example-live [ng-view]').text();
expect(content).toMatch(/controller\: BookCntl/);
expect(content).toMatch(/Book Id\: Scarlet/);
});
</file>
</example>
*/
/**
* @ngdoc event
* @name ngRoute.$route#$routeChangeStart
* @eventOf ngRoute.$route
* @eventType broadcast on root scope
* @description
* Broadcasted before a route change. At this point the route services starts
* resolving all of the dependencies needed for the route change to occurs.
* Typically this involves fetching the view template as well as any dependencies
* defined in `resolve` route property. Once all of the dependencies are resolved
* `$routeChangeSuccess` is fired.
*
* @param {Object} angularEvent Synthetic event object.
* @param {Route} next Future route information.
* @param {Route} current Current route information.
*/
/**
* @ngdoc event
* @name ngRoute.$route#$routeChangeSuccess
* @eventOf ngRoute.$route
* @eventType broadcast on root scope
* @description
* Broadcasted after a route dependencies are resolved.
* {@link ngRoute.directive:ngView ngView} listens for the directive
* to instantiate the controller and render the view.
*
* @param {Object} angularEvent Synthetic event object.
* @param {Route} current Current route information.
* @param {Route|Undefined} previous Previous route information, or undefined if current is
* first route entered.
*/
/**
* @ngdoc event
* @name ngRoute.$route#$routeChangeError
* @eventOf ngRoute.$route
* @eventType broadcast on root scope
* @description
* Broadcasted if any of the resolve promises are rejected.
*
* @param {Object} angularEvent Synthetic event object
* @param {Route} current Current route information.
* @param {Route} previous Previous route information.
* @param {Route} rejection Rejection of the promise. Usually the error of the failed promise.
*/
/**
* @ngdoc event
* @name ngRoute.$route#$routeUpdate
* @eventOf ngRoute.$route
* @eventType broadcast on root scope
* @description
*
* The `reloadOnSearch` property has been set to false, and we are reusing the same
* instance of the Controller.
*/
var forceReload = false,
$route = {
routes: routes,
/**
* @ngdoc method
* @name ngRoute.$route#reload
* @methodOf ngRoute.$route
*
* @description
* Causes `$route` service to reload the current route even if
* {@link ng.$location $location} hasn't changed.
*
* As a result of that, {@link ngRoute.directive:ngView ngView}
* creates new scope, reinstantiates the controller.
*/
reload: function() {
forceReload = true;
$rootScope.$evalAsync(updateRoute);
}
};
$rootScope.$on('$locationChangeSuccess', updateRoute);
return $route;
/////////////////////////////////////////////////////
/**
* @param on {string} current url
* @param route {Object} route regexp to match the url against
* @return {?Object}
*
* @description
* Check if the route matches the current url.
*
* Inspired by match in
* visionmedia/express/lib/router/router.js.
*/
function switchRouteMatcher(on, route) {
var keys = route.keys,
params = {};
if (!route.regexp) return null;
var m = route.regexp.exec(on);
if (!m) return null;
for (var i = 1, len = m.length; i < len; ++i) {
var key = keys[i - 1];
var val = 'string' == typeof m[i]
? decodeURIComponent(m[i])
: m[i];
if (key && val) {
params[key.name] = val;
}
}
return params;
}
function updateRoute() {
var next = parseRoute(),
last = $route.current;
if (next && last && next.$$route === last.$$route
&& angular.equals(next.pathParams, last.pathParams)
&& !next.reloadOnSearch && !forceReload) {
last.params = next.params;
angular.copy(last.params, $routeParams);
$rootScope.$broadcast('$routeUpdate', last);
} else if (next || last) {
forceReload = false;
$rootScope.$broadcast('$routeChangeStart', next, last);
$route.current = next;
if (next) {
if (next.redirectTo) {
if (angular.isString(next.redirectTo)) {
$location.path(interpolate(next.redirectTo, next.params)).search(next.params)
.replace();
} else {
$location.url(next.redirectTo(next.pathParams, $location.path(), $location.search()))
.replace();
}
}
}
$q.when(next).
then(function() {
if (next) {
var locals = angular.extend({}, next.resolve),
template, templateUrl;
angular.forEach(locals, function(value, key) {
locals[key] = angular.isString(value) ?
$injector.get(value) : $injector.invoke(value);
});
if (angular.isDefined(template = next.template)) {
if (angular.isFunction(template)) {
template = template(next.params);
}
} else if (angular.isDefined(templateUrl = next.templateUrl)) {
if (angular.isFunction(templateUrl)) {
templateUrl = templateUrl(next.params);
}
templateUrl = $sce.getTrustedResourceUrl(templateUrl);
if (angular.isDefined(templateUrl)) {
next.loadedTemplateUrl = templateUrl;
template = $http.get(templateUrl, {cache: $templateCache}).
then(function(response) { return response.data; });
}
}
if (angular.isDefined(template)) {
locals['$template'] = template;
}
return $q.all(locals);
}
}).
// after route change
then(function(locals) {
if (next == $route.current) {
if (next) {
next.locals = locals;
angular.copy(next.params, $routeParams);
}
$rootScope.$broadcast('$routeChangeSuccess', next, last);
}
}, function(error) {
if (next == $route.current) {
$rootScope.$broadcast('$routeChangeError', next, last, error);
}
});
}
}
/**
* @returns the current active route, by matching it against the URL
*/
function parseRoute() {
// Match a route
var params, match;
angular.forEach(routes, function(route, path) {
if (!match && (params = switchRouteMatcher($location.path(), route))) {
match = inherit(route, {
params: angular.extend({}, $location.search(), params),
pathParams: params});
match.$$route = route;
}
});
// No route matched; fallback to "otherwise" route
return match || routes[null] && inherit(routes[null], {params: {}, pathParams:{}});
}
/**
* @returns interpolation of the redirect path with the parameters
*/
function interpolate(string, params) {
var result = [];
angular.forEach((string||'').split(':'), function(segment, i) {
if (i === 0) {
result.push(segment);
} else {
var segmentMatch = segment.match(/(\w+)(.*)/);
var key = segmentMatch[1];
result.push(params[key]);
result.push(segmentMatch[2] || '');
delete params[key];
}
});
return result.join('');
}
}];
}
ngRouteModule.provider('$routeParams', $RouteParamsProvider);
/**
* @ngdoc object
* @name ngRoute.$routeParams
* @requires $route
*
* @description
* The `$routeParams` service allows you to retrieve the current set of route parameters.
*
* Requires the {@link ngRoute `ngRoute`} module to be installed.
*
* The route parameters are a combination of {@link ng.$location `$location`}'s
* {@link ng.$location#methods_search `search()`} and {@link ng.$location#methods_path `path()`}.
* The `path` parameters are extracted when the {@link ngRoute.$route `$route`} path is matched.
*
* In case of parameter name collision, `path` params take precedence over `search` params.
*
* The service guarantees that the identity of the `$routeParams` object will remain unchanged
* (but its properties will likely change) even when a route change occurs.
*
* Note that the `$routeParams` are only updated *after* a route change completes successfully.
* This means that you cannot rely on `$routeParams` being correct in route resolve functions.
* Instead you can use `$route.current.params` to access the new route's parameters.
*
* @example
* <pre>
* // Given:
* // URL: http://server.com/index.html#/Chapter/1/Section/2?search=moby
* // Route: /Chapter/:chapterId/Section/:sectionId
* //
* // Then
* $routeParams ==> {chapterId:1, sectionId:2, search:'moby'}
* </pre>
*/
function $RouteParamsProvider() {
this.$get = function() { return {}; };
}
ngRouteModule.directive('ngView', ngViewFactory);
/**
* @ngdoc directive
* @name ngRoute.directive:ngView
* @restrict ECA
*
* @description
* # Overview
* `ngView` is a directive that complements the {@link ngRoute.$route $route} service by
* including the rendered template of the current route into the main layout (`index.html`) file.
* Every time the current route changes, the included view changes with it according to the
* configuration of the `$route` service.
*
* Requires the {@link ngRoute `ngRoute`} module to be installed.
*
* @animations
* enter - animation is used to bring new content into the browser.
* leave - animation is used to animate existing content away.
*
* The enter and leave animation occur concurrently.
*
* @scope
* @priority 400
* @example
<example module="ngViewExample" deps="angular-route.js" animations="true">
<file name="index.html">
<div ng-controller="MainCntl as main">
Choose:
<a href="Book/Moby">Moby</a> |
<a href="Book/Moby/ch/1">Moby: Ch1</a> |
<a href="Book/Gatsby">Gatsby</a> |
<a href="Book/Gatsby/ch/4?key=value">Gatsby: Ch4</a> |
<a href="Book/Scarlet">Scarlet Letter</a><br/>
<div class="view-animate-container">
<div ng-view class="view-animate"></div>
</div>
<hr />
<pre>$location.path() = {{main.$location.path()}}</pre>
<pre>$route.current.templateUrl = {{main.$route.current.templateUrl}}</pre>
<pre>$route.current.params = {{main.$route.current.params}}</pre>
<pre>$route.current.scope.name = {{main.$route.current.scope.name}}</pre>
<pre>$routeParams = {{main.$routeParams}}</pre>
</div>
</file>
<file name="book.html">
<div>
controller: {{book.name}}<br />
Book Id: {{book.params.bookId}}<br />
</div>
</file>
<file name="chapter.html">
<div>
controller: {{chapter.name}}<br />
Book Id: {{chapter.params.bookId}}<br />
Chapter Id: {{chapter.params.chapterId}}
</div>
</file>
<file name="animations.css">
.view-animate-container {
position:relative;
height:100px!important;
position:relative;
background:white;
border:1px solid black;
height:40px;
overflow:hidden;
}
.view-animate {
padding:10px;
}
.view-animate.ng-enter, .view-animate.ng-leave {
-webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s;
transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s;
display:block;
width:100%;
border-left:1px solid black;
position:absolute;
top:0;
left:0;
right:0;
bottom:0;
padding:10px;
}
.view-animate.ng-enter {
left:100%;
}
.view-animate.ng-enter.ng-enter-active {
left:0;
}
.view-animate.ng-leave.ng-leave-active {
left:-100%;
}
</file>
<file name="script.js">
angular.module('ngViewExample', ['ngRoute', 'ngAnimate'],
function($routeProvider, $locationProvider) {
$routeProvider.when('/Book/:bookId', {
templateUrl: 'book.html',
controller: BookCntl,
controllerAs: 'book'
});
$routeProvider.when('/Book/:bookId/ch/:chapterId', {
templateUrl: 'chapter.html',
controller: ChapterCntl,
controllerAs: 'chapter'
});
// configure html5 to get links working on jsfiddle
$locationProvider.html5Mode(true);
});
function MainCntl($route, $routeParams, $location) {
this.$route = $route;
this.$location = $location;
this.$routeParams = $routeParams;
}
function BookCntl($routeParams) {
this.name = "BookCntl";
this.params = $routeParams;
}
function ChapterCntl($routeParams) {
this.name = "ChapterCntl";
this.params = $routeParams;
}
</file>
<file name="scenario.js">
it('should load and compile correct template', function() {
element('a:contains("Moby: Ch1")').click();
var content = element('.doc-example-live [ng-view]').text();
expect(content).toMatch(/controller\: ChapterCntl/);
expect(content).toMatch(/Book Id\: Moby/);
expect(content).toMatch(/Chapter Id\: 1/);
element('a:contains("Scarlet")').click();
content = element('.doc-example-live [ng-view]').text();
expect(content).toMatch(/controller\: BookCntl/);
expect(content).toMatch(/Book Id\: Scarlet/);
});
</file>
</example>
*/
/**
* @ngdoc event
* @name ngRoute.directive:ngView#$viewContentLoaded
* @eventOf ngRoute.directive:ngView
* @eventType emit on the current ngView scope
* @description
* Emitted every time the ngView content is reloaded.
*/
ngViewFactory.$inject = ['$route', '$anchorScroll', '$compile', '$controller', '$animate'];
function ngViewFactory( $route, $anchorScroll, $compile, $controller, $animate) {
return {
restrict: 'ECA',
terminal: true,
priority: 400,
transclude: 'element',
link: function(scope, $element, attr, ctrl, $transclude) {
var currentScope,
currentElement,
autoScrollExp = attr.autoscroll,
onloadExp = attr.onload || '';
scope.$on('$routeChangeSuccess', update);
update();
function cleanupLastView() {
if (currentScope) {
currentScope.$destroy();
currentScope = null;
}
if(currentElement) {
$animate.leave(currentElement);
currentElement = null;
}
}
function update() {
var locals = $route.current && $route.current.locals,
template = locals && locals.$template;
if (template) {
var newScope = scope.$new();
// Note: This will also link all children of ng-view that were contained in the original
// html. If that content contains controllers, ... they could pollute/change the scope.
// However, using ng-view on an element with additional content does not make sense...
// Note: We can't remove them in the cloneAttchFn of $transclude as that
// function is called before linking the content, which would apply child
// directives to non existing elements.
var clone = $transclude(newScope, angular.noop);
clone.html(template);
$animate.enter(clone, null, currentElement || $element, function onNgViewEnter () {
if (angular.isDefined(autoScrollExp)
&& (!autoScrollExp || scope.$eval(autoScrollExp))) {
$anchorScroll();
}
});
cleanupLastView();
var link = $compile(clone.contents()),
current = $route.current;
currentScope = current.scope = newScope;
currentElement = clone;
if (current.controller) {
locals.$scope = currentScope;
var controller = $controller(current.controller, locals);
if (current.controllerAs) {
currentScope[current.controllerAs] = controller;
}
clone.data('$ngControllerController', controller);
clone.children().data('$ngControllerController', controller);
}
link(currentScope);
currentScope.$emit('$viewContentLoaded');
currentScope.$eval(onloadExp);
} else {
cleanupLastView();
}
}
}
};
}
})(window, window.angular);

14
app/vendor/angular/angular-route.min.js vendored

@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
/*
AngularJS v1.2.3
(c) 2010-2014 Google, Inc. http://angularjs.org
License: MIT
*/
(function(t,c,A){'use strict';function x(r,m,d,b,h){return{restrict:"ECA",terminal:!0,priority:400,transclude:"element",link:function(l,z,k,B,w){function v(){g&&(g.$destroy(),g=null);q&&(h.leave(q),q=null)}function u(){var a=r.current&&r.current.locals,e=a&&a.$template;if(e){var y=l.$new(),s=w(y,c.noop);s.html(e);h.enter(s,null,q||z,function(){!c.isDefined(n)||n&&!l.$eval(n)||m()});v();var e=d(s.contents()),f=r.current;g=f.scope=y;q=s;f.controller&&(a.$scope=g,a=b(f.controller,a),f.controllerAs&&
(g[f.controllerAs]=a),s.data("$ngControllerController",a),s.children().data("$ngControllerController",a));e(g);g.$emit("$viewContentLoaded");g.$eval(p)}else v()}var g,q,n=k.autoscroll,p=k.onload||"";l.$on("$routeChangeSuccess",u);u()}}}t=c.module("ngRoute",["ng"]).provider("$route",function(){function r(b,h){return c.extend(new (c.extend(function(){},{prototype:b})),h)}function m(b,c){var l=c.caseInsensitiveMatch,d={originalPath:b,regexp:b},k=d.keys=[];b=b.replace(/([().])/g,"\\$1").replace(/(\/)?:(\w+)([\?|\*])?/g,
function(b,c,h,d){b="?"===d?d:null;d="*"===d?d:null;k.push({name:h,optional:!!b});c=c||"";return""+(b?"":c)+"(?:"+(b?c:"")+(d&&"(.+?)"||"([^/]+)")+(b||"")+")"+(b||"")}).replace(/([\/$\*])/g,"\\$1");d.regexp=RegExp("^"+b+"$",l?"i":"");return d}var d={};this.when=function(b,h){d[b]=c.extend({reloadOnSearch:!0},h,b&&m(b,h));if(b){var l="/"==b[b.length-1]?b.substr(0,b.length-1):b+"/";d[l]=c.extend({redirectTo:b},m(l,h))}return this};this.otherwise=function(b){this.when(null,b);return this};this.$get=
["$rootScope","$location","$routeParams","$q","$injector","$http","$templateCache","$sce",function(b,h,l,m,k,t,w,v){function u(){var a=g(),e=p.current;if(a&&e&&a.$$route===e.$$route&&c.equals(a.pathParams,e.pathParams)&&!a.reloadOnSearch&&!n)e.params=a.params,c.copy(e.params,l),b.$broadcast("$routeUpdate",e);else if(a||e)n=!1,b.$broadcast("$routeChangeStart",a,e),(p.current=a)&&a.redirectTo&&(c.isString(a.redirectTo)?h.path(q(a.redirectTo,a.params)).search(a.params).replace():h.url(a.redirectTo(a.pathParams,
h.path(),h.search())).replace()),m.when(a).then(function(){if(a){var b=c.extend({},a.resolve),e,f;c.forEach(b,function(a,e){b[e]=c.isString(a)?k.get(a):k.invoke(a)});c.isDefined(e=a.template)?c.isFunction(e)&&(e=e(a.params)):c.isDefined(f=a.templateUrl)&&(c.isFunction(f)&&(f=f(a.params)),f=v.getTrustedResourceUrl(f),c.isDefined(f)&&(a.loadedTemplateUrl=f,e=t.get(f,{cache:w}).then(function(a){return a.data})));c.isDefined(e)&&(b.$template=e);return m.all(b)}}).then(function(d){a==p.current&&(a&&(a.locals=
d,c.copy(a.params,l)),b.$broadcast("$routeChangeSuccess",a,e))},function(c){a==p.current&&b.$broadcast("$routeChangeError",a,e,c)})}function g(){var a,b;c.forEach(d,function(d,l){var f;if(f=!b){var g=h.path();f=d.keys;var m={};if(d.regexp)if(g=d.regexp.exec(g)){for(var k=1,q=g.length;k<q;++k){var n=f[k-1],p="string"==typeof g[k]?decodeURIComponent(g[k]):g[k];n&&p&&(m[n.name]=p)}f=m}else f=null;else f=null;f=a=f}f&&(b=r(d,{params:c.extend({},h.search(),a),pathParams:a}),b.$$route=d)});return b||d[null]&&
r(d[null],{params:{},pathParams:{}})}function q(a,b){var d=[];c.forEach((a||"").split(":"),function(a,c){if(0===c)d.push(a);else{var g=a.match(/(\w+)(.*)/),h=g[1];d.push(b[h]);d.push(g[2]||"");delete b[h]}});return d.join("")}var n=!1,p={routes:d,reload:function(){n=!0;b.$evalAsync(u)}};b.$on("$locationChangeSuccess",u);return p}]});t.provider("$routeParams",function(){this.$get=function(){return{}}});t.directive("ngView",x);x.$inject=["$route","$anchorScroll","$compile","$controller","$animate"]})(window,
window.angular);
//# sourceMappingURL=angular-route.min.js.map

8
app/vendor/angular/angular-route.min.js.map vendored

File diff suppressed because one or more lines are too long

615
app/vendor/angular/angular-sanitize.js vendored

@ -0,0 +1,615 @@ @@ -0,0 +1,615 @@
/**
* @license AngularJS v1.2.3
* (c) 2010-2014 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, angular, undefined) {'use strict';
var $sanitizeMinErr = angular.$$minErr('$sanitize');
/**
* @ngdoc overview
* @name ngSanitize
* @description
*
* # ngSanitize
*
* The `ngSanitize` module provides functionality to sanitize HTML.
*
* {@installModule sanitize}
*
* <div doc-module-components="ngSanitize"></div>
*
* See {@link ngSanitize.$sanitize `$sanitize`} for usage.
*/
/*
* HTML Parser By Misko Hevery (misko@hevery.com)
* based on: HTML Parser By John Resig (ejohn.org)
* Original code by Erik Arvidsson, Mozilla Public License
* http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
*
* // Use like so:
* htmlParser(htmlString, {
* start: function(tag, attrs, unary) {},
* end: function(tag) {},
* chars: function(text) {},
* comment: function(text) {}
* });
*
*/
/**
* @ngdoc service
* @name ngSanitize.$sanitize
* @function
*
* @description
* The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are
* then serialized back to properly escaped html string. This means that no unsafe input can make
* it into the returned string, however, since our parser is more strict than a typical browser
* parser, it's possible that some obscure input, which would be recognized as valid HTML by a
* browser, won't make it through the sanitizer.
* The whitelist is configured using the functions `aHrefSanitizationWhitelist` and
* `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}.
*
* @param {string} html Html input.
* @returns {string} Sanitized html.
*
* @example
<doc:example module="ngSanitize">
<doc:source>
<script>
function Ctrl($scope, $sce) {
$scope.snippet =
'<p style="color:blue">an html\n' +
'<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' +
'snippet</p>';
$scope.deliberatelyTrustDangerousSnippet = function() {
return $sce.trustAsHtml($scope.snippet);
};
}
</script>
<div ng-controller="Ctrl">
Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
<table>
<tr>
<td>Directive</td>
<td>How</td>
<td>Source</td>
<td>Rendered</td>
</tr>
<tr id="bind-html-with-sanitize">
<td>ng-bind-html</td>
<td>Automatically uses $sanitize</td>
<td><pre>&lt;div ng-bind-html="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
<td><div ng-bind-html="snippet"></div></td>
</tr>
<tr id="bind-html-with-trust">
<td>ng-bind-html</td>
<td>Bypass $sanitize by explicitly trusting the dangerous value</td>
<td>
<pre>&lt;div ng-bind-html="deliberatelyTrustDangerousSnippet()"&gt;
&lt;/div&gt;</pre>
</td>
<td><div ng-bind-html="deliberatelyTrustDangerousSnippet()"></div></td>
</tr>
<tr id="bind-default">
<td>ng-bind</td>
<td>Automatically escapes</td>
<td><pre>&lt;div ng-bind="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
<td><div ng-bind="snippet"></div></td>
</tr>
</table>
</div>
</doc:source>
<doc:scenario>
it('should sanitize the html snippet by default', function() {
expect(using('#bind-html-with-sanitize').element('div').html()).
toBe('<p>an html\n<em>click here</em>\nsnippet</p>');
});
it('should inline raw snippet if bound to a trusted value', function() {
expect(using('#bind-html-with-trust').element("div").html()).
toBe("<p style=\"color:blue\">an html\n" +
"<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
"snippet</p>");
});
it('should escape snippet without any filter', function() {
expect(using('#bind-default').element('div').html()).
toBe("&lt;p style=\"color:blue\"&gt;an html\n" +
"&lt;em onmouseover=\"this.textContent='PWN3D!'\"&gt;click here&lt;/em&gt;\n" +
"snippet&lt;/p&gt;");
});
it('should update', function() {
input('snippet').enter('new <b onclick="alert(1)">text</b>');
expect(using('#bind-html-with-sanitize').element('div').html()).toBe('new <b>text</b>');
expect(using('#bind-html-with-trust').element('div').html()).toBe(
'new <b onclick="alert(1)">text</b>');
expect(using('#bind-default').element('div').html()).toBe(
"new &lt;b onclick=\"alert(1)\"&gt;text&lt;/b&gt;");
});
</doc:scenario>
</doc:example>
*/
function $SanitizeProvider() {
this.$get = ['$$sanitizeUri', function($$sanitizeUri) {
return function(html) {
var buf = [];
htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) {
return !/^unsafe/.test($$sanitizeUri(uri, isImage));
}));
return buf.join('');
};
}];
}
function sanitizeText(chars) {
var buf = [];
var writer = htmlSanitizeWriter(buf, angular.noop);
writer.chars(chars);
return buf.join('');
}
// Regular Expressions for parsing tags and attributes
var START_TAG_REGEXP =
/^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/,
END_TAG_REGEXP = /^<\s*\/\s*([\w:-]+)[^>]*>/,
ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,
BEGIN_TAG_REGEXP = /^</,
BEGING_END_TAGE_REGEXP = /^<\s*\//,
COMMENT_REGEXP = /<!--(.*?)-->/g,
DOCTYPE_REGEXP = /<!DOCTYPE([^>]*?)>/i,
CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g,
// Match everything outside of normal chars and " (quote character)
NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g;
// Good source of info about elements and attributes
// http://dev.w3.org/html5/spec/Overview.html#semantics
// http://simon.html5.org/html-elements
// Safe Void Elements - HTML5
// http://dev.w3.org/html5/spec/Overview.html#void-elements
var voidElements = makeMap("area,br,col,hr,img,wbr");
// Elements that you can, intentionally, leave open (and which close themselves)
// http://dev.w3.org/html5/spec/Overview.html#optional-tags
var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),
optionalEndTagInlineElements = makeMap("rp,rt"),
optionalEndTagElements = angular.extend({},
optionalEndTagInlineElements,
optionalEndTagBlockElements);
// Safe Block Elements - HTML5
var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article," +
"aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," +
"h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul"));
// Inline Elements - HTML5
var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b," +
"bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," +
"samp,small,span,strike,strong,sub,sup,time,tt,u,var"));
// Special Elements (can contain anything)
var specialElements = makeMap("script,style");
var validElements = angular.extend({},
voidElements,
blockElements,
inlineElements,
optionalEndTagElements);
//Attributes that have href and hence need to be sanitized
var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap");
var validAttrs = angular.extend({}, uriAttrs, makeMap(
'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+
'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+
'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+
'scope,scrolling,shape,span,start,summary,target,title,type,'+
'valign,value,vspace,width'));
function makeMap(str) {
var obj = {}, items = str.split(','), i;
for (i = 0; i < items.length; i++) obj[items[i]] = true;
return obj;
}
/**
* @example
* htmlParser(htmlString, {
* start: function(tag, attrs, unary) {},
* end: function(tag) {},
* chars: function(text) {},
* comment: function(text) {}
* });
*
* @param {string} html string
* @param {object} handler
*/
function htmlParser( html, handler ) {
var index, chars, match, stack = [], last = html;
stack.last = function() { return stack[ stack.length - 1 ]; };
while ( html ) {
chars = true;
// Make sure we're not in a script or style element
if ( !stack.last() || !specialElements[ stack.last() ] ) {
// Comment
if ( html.indexOf("<!--") === 0 ) {
// comments containing -- are not allowed unless they terminate the comment
index = html.indexOf("--", 4);
if ( index >= 0 && html.lastIndexOf("-->", index) === index) {
if (handler.comment) handler.comment( html.substring( 4, index ) );
html = html.substring( index + 3 );
chars = false;
}
// DOCTYPE
} else if ( DOCTYPE_REGEXP.test(html) ) {
match = html.match( DOCTYPE_REGEXP );
if ( match ) {
html = html.replace( match[0] , '');
chars = false;
}
// end tag
} else if ( BEGING_END_TAGE_REGEXP.test(html) ) {
match = html.match( END_TAG_REGEXP );
if ( match ) {
html = html.substring( match[0].length );
match[0].replace( END_TAG_REGEXP, parseEndTag );
chars = false;
}
// start tag
} else if ( BEGIN_TAG_REGEXP.test(html) ) {
match = html.match( START_TAG_REGEXP );
if ( match ) {
html = html.substring( match[0].length );
match[0].replace( START_TAG_REGEXP, parseStartTag );
chars = false;
}
}
if ( chars ) {
index = html.indexOf("<");
var text = index < 0 ? html : html.substring( 0, index );
html = index < 0 ? "" : html.substring( index );
if (handler.chars) handler.chars( decodeEntities(text) );
}
} else {
html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'),
function(all, text){
text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1");
if (handler.chars) handler.chars( decodeEntities(text) );
return "";
});
parseEndTag( "", stack.last() );
}
if ( html == last ) {
throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " +
"of html: {0}", html);
}
last = html;
}
// Clean up any remaining tags
parseEndTag();
function parseStartTag( tag, tagName, rest, unary ) {
tagName = angular.lowercase(tagName);
if ( blockElements[ tagName ] ) {
while ( stack.last() && inlineElements[ stack.last() ] ) {
parseEndTag( "", stack.last() );
}
}
if ( optionalEndTagElements[ tagName ] && stack.last() == tagName ) {
parseEndTag( "", tagName );
}
unary = voidElements[ tagName ] || !!unary;
if ( !unary )
stack.push( tagName );
var attrs = {};
rest.replace(ATTR_REGEXP,
function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) {
var value = doubleQuotedValue
|| singleQuotedValue
|| unquotedValue
|| '';
attrs[name] = decodeEntities(value);
});
if (handler.start) handler.start( tagName, attrs, unary );
}
function parseEndTag( tag, tagName ) {
var pos = 0, i;
tagName = angular.lowercase(tagName);
if ( tagName )
// Find the closest opened tag of the same type
for ( pos = stack.length - 1; pos >= 0; pos-- )
if ( stack[ pos ] == tagName )
break;
if ( pos >= 0 ) {
// Close all the open elements, up the stack
for ( i = stack.length - 1; i >= pos; i-- )
if (handler.end) handler.end( stack[ i ] );
// Remove the open elements from the stack
stack.length = pos;
}
}
}
/**
* decodes all entities into regular string
* @param value
* @returns {string} A string with decoded entities.
*/
var hiddenPre=document.createElement("pre");
function decodeEntities(value) {
if (!value) {
return '';
}
// Note: IE8 does not preserve spaces at the start/end of innerHTML
var spaceRe = /^(\s*)([\s\S]*?)(\s*)$/;
var parts = spaceRe.exec(value);
parts[0] = '';
if (parts[2]) {
hiddenPre.innerHTML=parts[2].replace(/</g,"&lt;");
parts[2] = hiddenPre.innerText || hiddenPre.textContent;
}
return parts.join('');
}
/**
* Escapes all potentially dangerous characters, so that the
* resulting string can be safely inserted into attribute or
* element text.
* @param value
* @returns escaped text
*/
function encodeEntities(value) {
return value.
replace(/&/g, '&amp;').
replace(NON_ALPHANUMERIC_REGEXP, function(value){
return '&#' + value.charCodeAt(0) + ';';
}).
replace(/</g, '&lt;').
replace(/>/g, '&gt;');
}
/**
* create an HTML/XML writer which writes to buffer
* @param {Array} buf use buf.jain('') to get out sanitized html string
* @returns {object} in the form of {
* start: function(tag, attrs, unary) {},
* end: function(tag) {},
* chars: function(text) {},
* comment: function(text) {}
* }
*/
function htmlSanitizeWriter(buf, uriValidator){
var ignore = false;
var out = angular.bind(buf, buf.push);
return {
start: function(tag, attrs, unary){
tag = angular.lowercase(tag);
if (!ignore && specialElements[tag]) {
ignore = tag;
}
if (!ignore && validElements[tag] === true) {
out('<');
out(tag);
angular.forEach(attrs, function(value, key){
var lkey=angular.lowercase(key);
var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background');
if (validAttrs[lkey] === true &&
(uriAttrs[lkey] !== true || uriValidator(value, isImage))) {
out(' ');
out(key);
out('="');
out(encodeEntities(value));
out('"');
}
});
out(unary ? '/>' : '>');
}
},
end: function(tag){
tag = angular.lowercase(tag);
if (!ignore && validElements[tag] === true) {
out('</');
out(tag);
out('>');
}
if (tag == ignore) {
ignore = false;
}
},
chars: function(chars){
if (!ignore) {
out(encodeEntities(chars));
}
}
};
}
// define ngSanitize module and register $sanitize service
angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
/* global sanitizeText: false */
/**
* @ngdoc filter
* @name ngSanitize.filter:linky
* @function
*
* @description
* Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and
* plain email address links.
*
* Requires the {@link ngSanitize `ngSanitize`} module to be installed.
*
* @param {string} text Input text.
* @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in.
* @returns {string} Html-linkified text.
*
* @usage
<span ng-bind-html="linky_expression | linky"></span>
*
* @example
<doc:example module="ngSanitize">
<doc:source>
<script>
function Ctrl($scope) {
$scope.snippet =
'Pretty text with some links:\n'+
'http://angularjs.org/,\n'+
'mailto:us@somewhere.org,\n'+
'another@somewhere.org,\n'+
'and one more: ftp://127.0.0.1/.';
$scope.snippetWithTarget = 'http://angularjs.org/';
}
</script>
<div ng-controller="Ctrl">
Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
<table>
<tr>
<td>Filter</td>
<td>Source</td>
<td>Rendered</td>
</tr>
<tr id="linky-filter">
<td>linky filter</td>
<td>
<pre>&lt;div ng-bind-html="snippet | linky"&gt;<br>&lt;/div&gt;</pre>
</td>
<td>
<div ng-bind-html="snippet | linky"></div>
</td>
</tr>
<tr id="linky-target">
<td>linky target</td>
<td>
<pre>&lt;div ng-bind-html="snippetWithTarget | linky:'_blank'"&gt;<br>&lt;/div&gt;</pre>
</td>
<td>
<div ng-bind-html="snippetWithTarget | linky:'_blank'"></div>
</td>
</tr>
<tr id="escaped-html">
<td>no filter</td>
<td><pre>&lt;div ng-bind="snippet"&gt;<br>&lt;/div&gt;</pre></td>
<td><div ng-bind="snippet"></div></td>
</tr>
</table>
</doc:source>
<doc:scenario>
it('should linkify the snippet with urls', function() {
expect(using('#linky-filter').binding('snippet | linky')).
toBe('Pretty text with some links:&#10;' +
'<a href="http://angularjs.org/">http://angularjs.org/</a>,&#10;' +
'<a href="mailto:us@somewhere.org">us@somewhere.org</a>,&#10;' +
'<a href="mailto:another@somewhere.org">another@somewhere.org</a>,&#10;' +
'and one more: <a href="ftp://127.0.0.1/">ftp://127.0.0.1/</a>.');
});
it ('should not linkify snippet without the linky filter', function() {
expect(using('#escaped-html').binding('snippet')).
toBe("Pretty text with some links:\n" +
"http://angularjs.org/,\n" +
"mailto:us@somewhere.org,\n" +
"another@somewhere.org,\n" +
"and one more: ftp://127.0.0.1/.");
});
it('should update', function() {
input('snippet').enter('new http://link.');
expect(using('#linky-filter').binding('snippet | linky')).
toBe('new <a href="http://link">http://link</a>.');
expect(using('#escaped-html').binding('snippet')).toBe('new http://link.');
});
it('should work with the target property', function() {
expect(using('#linky-target').binding("snippetWithTarget | linky:'_blank'")).
toBe('<a target="_blank" href="http://angularjs.org/">http://angularjs.org/</a>');
});
</doc:scenario>
</doc:example>
*/
angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
var LINKY_URL_REGEXP =
/((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>]/,
MAILTO_REGEXP = /^mailto:/;
return function(text, target) {
if (!text) return text;
var match;
var raw = text;
var html = [];
var url;
var i;
while ((match = raw.match(LINKY_URL_REGEXP))) {
// We can not end in these as they are sometimes found at the end of the sentence
url = match[0];
// if we did not match ftp/http/mailto then assume mailto
if (match[2] == match[3]) url = 'mailto:' + url;
i = match.index;
addText(raw.substr(0, i));
addLink(url, match[0].replace(MAILTO_REGEXP, ''));
raw = raw.substring(i + match[0].length);
}
addText(raw);
return $sanitize(html.join(''));
function addText(text) {
if (!text) {
return;
}
html.push(sanitizeText(text));
}
function addLink(url, text) {
html.push('<a ');
if (angular.isDefined(target)) {
html.push('target="');
html.push(target);
html.push('" ');
}
html.push('href="');
html.push(url);
html.push('">');
addText(text);
html.push('</a>');
}
};
}]);
})(window, window.angular);

14
app/vendor/angular/angular-sanitize.min.js vendored

@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
/*
AngularJS v1.2.3
(c) 2010-2014 Google, Inc. http://angularjs.org
License: MIT
*/
(function(n,h,q){'use strict';function F(a){var e=[];t(e,h.noop).chars(a);return e.join("")}function k(a){var e={};a=a.split(",");var d;for(d=0;d<a.length;d++)e[a[d]]=!0;return e}function G(a,e){function d(a,b,d,g){b=h.lowercase(b);if(u[b])for(;f.last()&&v[f.last()];)c("",f.last());w[b]&&f.last()==b&&c("",b);(g=x[b]||!!g)||f.push(b);var l={};d.replace(H,function(a,b,e,c,m){l[b]=r(e||c||m||"")});e.start&&e.start(b,l,g)}function c(a,b){var c=0,d;if(b=h.lowercase(b))for(c=f.length-1;0<=c&&f[c]!=b;c--);
if(0<=c){for(d=f.length-1;d>=c;d--)e.end&&e.end(f[d]);f.length=c}}var b,g,f=[],l=a;for(f.last=function(){return f[f.length-1]};a;){g=!0;if(f.last()&&y[f.last()])a=a.replace(RegExp("(.*)<\\s*\\/\\s*"+f.last()+"[^>]*>","i"),function(a,b){b=b.replace(I,"$1").replace(J,"$1");e.chars&&e.chars(r(b));return""}),c("",f.last());else{if(0===a.indexOf("\x3c!--"))b=a.indexOf("--",4),0<=b&&a.lastIndexOf("--\x3e",b)===b&&(e.comment&&e.comment(a.substring(4,b)),a=a.substring(b+3),g=!1);else if(z.test(a)){if(b=a.match(z))a=
a.replace(b[0],""),g=!1}else if(K.test(a)){if(b=a.match(A))a=a.substring(b[0].length),b[0].replace(A,c),g=!1}else L.test(a)&&(b=a.match(B))&&(a=a.substring(b[0].length),b[0].replace(B,d),g=!1);g&&(b=a.indexOf("<"),g=0>b?a:a.substring(0,b),a=0>b?"":a.substring(b),e.chars&&e.chars(r(g)))}if(a==l)throw M("badparse",a);l=a}c()}function r(a){if(!a)return"";a=/^(\s*)([\s\S]*?)(\s*)$/.exec(a);a[0]="";a[2]&&(s.innerHTML=a[2].replace(/</g,"&lt;"),a[2]=s.innerText||s.textContent);return a.join("")}function C(a){return a.replace(/&/g,
"&amp;").replace(N,function(a){return"&#"+a.charCodeAt(0)+";"}).replace(/</g,"&lt;").replace(/>/g,"&gt;")}function t(a,e){var d=!1,c=h.bind(a,a.push);return{start:function(a,g,f){a=h.lowercase(a);!d&&y[a]&&(d=a);d||!0!==D[a]||(c("<"),c(a),h.forEach(g,function(d,f){var g=h.lowercase(f),k="img"===a&&"src"===g||"background"===g;!0!==O[g]||!0===E[g]&&!e(d,k)||(c(" "),c(f),c('="'),c(C(d)),c('"'))}),c(f?"/>":">"))},end:function(a){a=h.lowercase(a);d||!0!==D[a]||(c("</"),c(a),c(">"));a==d&&(d=!1)},chars:function(a){d||
c(C(a))}}}var M=h.$$minErr("$sanitize"),B=/^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/,A=/^<\s*\/\s*([\w:-]+)[^>]*>/,H=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,L=/^</,K=/^<\s*\//,I=/\x3c!--(.*?)--\x3e/g,z=/<!DOCTYPE([^>]*?)>/i,J=/<!\[CDATA\[(.*?)]]\x3e/g,N=/([^\#-~| |!])/g,x=k("area,br,col,hr,img,wbr");n=k("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr");q=k("rp,rt");var w=h.extend({},q,n),u=h.extend({},n,k("address,article,aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")),
v=h.extend({},q,k("a,abbr,acronym,b,bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small,span,strike,strong,sub,sup,time,tt,u,var")),y=k("script,style"),D=h.extend({},x,u,v,w),E=k("background,cite,href,longdesc,src,usemap"),O=h.extend({},E,k("abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,scope,scrolling,shape,span,start,summary,target,title,type,valign,value,vspace,width")),
s=document.createElement("pre");h.module("ngSanitize",[]).provider("$sanitize",function(){this.$get=["$$sanitizeUri",function(a){return function(e){var d=[];G(e,t(d,function(c,b){return!/^unsafe/.test(a(c,b))}));return d.join("")}}]});h.module("ngSanitize").filter("linky",["$sanitize",function(a){var e=/((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>]/,d=/^mailto:/;return function(c,b){function g(a){a&&m.push(F(a))}function f(a,c){m.push("<a ");h.isDefined(b)&&(m.push('target="'),
m.push(b),m.push('" '));m.push('href="');m.push(a);m.push('">');g(c);m.push("</a>")}if(!c)return c;for(var l,k=c,m=[],p,n;l=k.match(e);)p=l[0],l[2]==l[3]&&(p="mailto:"+p),n=l.index,g(k.substr(0,n)),f(p,l[0].replace(d,"")),k=k.substring(n+l[0].length);g(k);return a(m.join(""))}}])})(window,window.angular);
//# sourceMappingURL=angular-sanitize.min.js.map

8
app/vendor/angular/angular-sanitize.min.js.map vendored

File diff suppressed because one or more lines are too long

563
app/vendor/angular/angular-touch.js vendored

@ -0,0 +1,563 @@ @@ -0,0 +1,563 @@
/**
* @license AngularJS v1.2.3
* (c) 2010-2014 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, angular, undefined) {'use strict';
/**
* @ngdoc overview
* @name ngTouch
* @description
*
* # ngTouch
*
* The `ngTouch` module provides touch events and other helpers for touch-enabled devices.
* The implementation is based on jQuery Mobile touch event handling
* ([jquerymobile.com](http://jquerymobile.com/)).
*
* {@installModule touch}
*
* See {@link ngTouch.$swipe `$swipe`} for usage.
*
* <div doc-module-components="ngTouch"></div>
*
*/
// define ngTouch module
/* global -ngTouch */
var ngTouch = angular.module('ngTouch', []);
/* global ngTouch: false */
/**
* @ngdoc object
* @name ngTouch.$swipe
*
* @description
* The `$swipe` service is a service that abstracts the messier details of hold-and-drag swipe
* behavior, to make implementing swipe-related directives more convenient.
*
* Requires the {@link ngTouch `ngTouch`} module to be installed.
*
* `$swipe` is used by the `ngSwipeLeft` and `ngSwipeRight` directives in `ngTouch`, and by
* `ngCarousel` in a separate component.
*
* # Usage
* The `$swipe` service is an object with a single method: `bind`. `bind` takes an element
* which is to be watched for swipes, and an object with four handler functions. See the
* documentation for `bind` below.
*/
ngTouch.factory('$swipe', [function() {
// The total distance in any direction before we make the call on swipe vs. scroll.
var MOVE_BUFFER_RADIUS = 10;
function getCoordinates(event) {
var touches = event.touches && event.touches.length ? event.touches : [event];
var e = (event.changedTouches && event.changedTouches[0]) ||
(event.originalEvent && event.originalEvent.changedTouches &&
event.originalEvent.changedTouches[0]) ||
touches[0].originalEvent || touches[0];
return {
x: e.clientX,
y: e.clientY
};
}
return {
/**
* @ngdoc method
* @name ngTouch.$swipe#bind
* @methodOf ngTouch.$swipe
*
* @description
* The main method of `$swipe`. It takes an element to be watched for swipe motions, and an
* object containing event handlers.
*
* The four events are `start`, `move`, `end`, and `cancel`. `start`, `move`, and `end`
* receive as a parameter a coordinates object of the form `{ x: 150, y: 310 }`.
*
* `start` is called on either `mousedown` or `touchstart`. After this event, `$swipe` is
* watching for `touchmove` or `mousemove` events. These events are ignored until the total
* distance moved in either dimension exceeds a small threshold.
*
* Once this threshold is exceeded, either the horizontal or vertical delta is greater.
* - If the horizontal distance is greater, this is a swipe and `move` and `end` events follow.
* - If the vertical distance is greater, this is a scroll, and we let the browser take over.
* A `cancel` event is sent.
*
* `move` is called on `mousemove` and `touchmove` after the above logic has determined that
* a swipe is in progress.
*
* `end` is called when a swipe is successfully completed with a `touchend` or `mouseup`.
*
* `cancel` is called either on a `touchcancel` from the browser, or when we begin scrolling
* as described above.
*
*/
bind: function(element, eventHandlers) {
// Absolute total movement, used to control swipe vs. scroll.
var totalX, totalY;
// Coordinates of the start position.
var startCoords;
// Last event's position.
var lastPos;
// Whether a swipe is active.
var active = false;
element.on('touchstart mousedown', function(event) {
startCoords = getCoordinates(event);
active = true;
totalX = 0;
totalY = 0;
lastPos = startCoords;
eventHandlers['start'] && eventHandlers['start'](startCoords, event);
});
element.on('touchcancel', function(event) {
active = false;
eventHandlers['cancel'] && eventHandlers['cancel'](event);
});
element.on('touchmove mousemove', function(event) {
if (!active) return;
// Android will send a touchcancel if it thinks we're starting to scroll.
// So when the total distance (+ or - or both) exceeds 10px in either direction,
// we either:
// - On totalX > totalY, we send preventDefault() and treat this as a swipe.
// - On totalY > totalX, we let the browser handle it as a scroll.
if (!startCoords) return;
var coords = getCoordinates(event);
totalX += Math.abs(coords.x - lastPos.x);
totalY += Math.abs(coords.y - lastPos.y);
lastPos = coords;
if (totalX < MOVE_BUFFER_RADIUS && totalY < MOVE_BUFFER_RADIUS) {
return;
}
// One of totalX or totalY has exceeded the buffer, so decide on swipe vs. scroll.
if (totalY > totalX) {
// Allow native scrolling to take over.
active = false;
eventHandlers['cancel'] && eventHandlers['cancel'](event);
return;
} else {
// Prevent the browser from scrolling.
event.preventDefault();
eventHandlers['move'] && eventHandlers['move'](coords, event);
}
});
element.on('touchend mouseup', function(event) {
if (!active) return;
active = false;
eventHandlers['end'] && eventHandlers['end'](getCoordinates(event), event);
});
}
};
}]);
/* global ngTouch: false */
/**
* @ngdoc directive
* @name ngTouch.directive:ngClick
*
* @description
* A more powerful replacement for the default ngClick designed to be used on touchscreen
* devices. Most mobile browsers wait about 300ms after a tap-and-release before sending
* the click event. This version handles them immediately, and then prevents the
* following click event from propagating.
*
* Requires the {@link ngTouch `ngTouch`} module to be installed.
*
* This directive can fall back to using an ordinary click event, and so works on desktop
* browsers as well as mobile.
*
* This directive also sets the CSS class `ng-click-active` while the element is being held
* down (by a mouse click or touch) so you can restyle the depressed element if you wish.
*
* @element ANY
* @param {expression} ngClick {@link guide/expression Expression} to evaluate
* upon tap. (Event object is available as `$event`)
*
* @example
<doc:example>
<doc:source>
<button ng-click="count = count + 1" ng-init="count=0">
Increment
</button>
count: {{ count }}
</doc:source>
</doc:example>
*/
ngTouch.config(['$provide', function($provide) {
$provide.decorator('ngClickDirective', ['$delegate', function($delegate) {
// drop the default ngClick directive
$delegate.shift();
return $delegate;
}]);
}]);
ngTouch.directive('ngClick', ['$parse', '$timeout', '$rootElement',
function($parse, $timeout, $rootElement) {
var TAP_DURATION = 750; // Shorter than 750ms is a tap, longer is a taphold or drag.
var MOVE_TOLERANCE = 12; // 12px seems to work in most mobile browsers.
var PREVENT_DURATION = 2500; // 2.5 seconds maximum from preventGhostClick call to click
var CLICKBUSTER_THRESHOLD = 25; // 25 pixels in any dimension is the limit for busting clicks.
var ACTIVE_CLASS_NAME = 'ng-click-active';
var lastPreventedTime;
var touchCoordinates;
// TAP EVENTS AND GHOST CLICKS
//
// Why tap events?
// Mobile browsers detect a tap, then wait a moment (usually ~300ms) to see if you're
// double-tapping, and then fire a click event.
//
// This delay sucks and makes mobile apps feel unresponsive.
// So we detect touchstart, touchmove, touchcancel and touchend ourselves and determine when
// the user has tapped on something.
//
// What happens when the browser then generates a click event?
// The browser, of course, also detects the tap and fires a click after a delay. This results in
// tapping/clicking twice. So we do "clickbusting" to prevent it.
//
// How does it work?
// We attach global touchstart and click handlers, that run during the capture (early) phase.
// So the sequence for a tap is:
// - global touchstart: Sets an "allowable region" at the point touched.
// - element's touchstart: Starts a touch
// (- touchmove or touchcancel ends the touch, no click follows)
// - element's touchend: Determines if the tap is valid (didn't move too far away, didn't hold
// too long) and fires the user's tap handler. The touchend also calls preventGhostClick().
// - preventGhostClick() removes the allowable region the global touchstart created.
// - The browser generates a click event.
// - The global click handler catches the click, and checks whether it was in an allowable region.
// - If preventGhostClick was called, the region will have been removed, the click is busted.
// - If the region is still there, the click proceeds normally. Therefore clicks on links and
// other elements without ngTap on them work normally.
//
// This is an ugly, terrible hack!
// Yeah, tell me about it. The alternatives are using the slow click events, or making our users
// deal with the ghost clicks, so I consider this the least of evils. Fortunately Angular
// encapsulates this ugly logic away from the user.
//
// Why not just put click handlers on the element?
// We do that too, just to be sure. The problem is that the tap event might have caused the DOM
// to change, so that the click fires in the same position but something else is there now. So
// the handlers are global and care only about coordinates and not elements.
// Checks if the coordinates are close enough to be within the region.
function hit(x1, y1, x2, y2) {
return Math.abs(x1 - x2) < CLICKBUSTER_THRESHOLD && Math.abs(y1 - y2) < CLICKBUSTER_THRESHOLD;
}
// Checks a list of allowable regions against a click location.
// Returns true if the click should be allowed.
// Splices out the allowable region from the list after it has been used.
function checkAllowableRegions(touchCoordinates, x, y) {
for (var i = 0; i < touchCoordinates.length; i += 2) {
if (hit(touchCoordinates[i], touchCoordinates[i+1], x, y)) {
touchCoordinates.splice(i, i + 2);
return true; // allowable region
}
}
return false; // No allowable region; bust it.
}
// Global click handler that prevents the click if it's in a bustable zone and preventGhostClick
// was called recently.
function onClick(event) {
if (Date.now() - lastPreventedTime > PREVENT_DURATION) {
return; // Too old.
}
var touches = event.touches && event.touches.length ? event.touches : [event];
var x = touches[0].clientX;
var y = touches[0].clientY;
// Work around desktop Webkit quirk where clicking a label will fire two clicks (on the label
// and on the input element). Depending on the exact browser, this second click we don't want
// to bust has either (0,0) or negative coordinates.
if (x < 1 && y < 1) {
return; // offscreen
}
// Look for an allowable region containing this click.
// If we find one, that means it was created by touchstart and not removed by
// preventGhostClick, so we don't bust it.
if (checkAllowableRegions(touchCoordinates, x, y)) {
return;
}
// If we didn't find an allowable region, bust the click.
event.stopPropagation();
event.preventDefault();
// Blur focused form elements
event.target && event.target.blur();
}
// Global touchstart handler that creates an allowable region for a click event.
// This allowable region can be removed by preventGhostClick if we want to bust it.
function onTouchStart(event) {
var touches = event.touches && event.touches.length ? event.touches : [event];
var x = touches[0].clientX;
var y = touches[0].clientY;
touchCoordinates.push(x, y);
$timeout(function() {
// Remove the allowable region.
for (var i = 0; i < touchCoordinates.length; i += 2) {
if (touchCoordinates[i] == x && touchCoordinates[i+1] == y) {
touchCoordinates.splice(i, i + 2);
return;
}
}
}, PREVENT_DURATION, false);
}
// On the first call, attaches some event handlers. Then whenever it gets called, it creates a
// zone around the touchstart where clicks will get busted.
function preventGhostClick(x, y) {
if (!touchCoordinates) {
$rootElement[0].addEventListener('click', onClick, true);
$rootElement[0].addEventListener('touchstart', onTouchStart, true);
touchCoordinates = [];
}
lastPreventedTime = Date.now();
checkAllowableRegions(touchCoordinates, x, y);
}
// Actual linking function.
return function(scope, element, attr) {
var clickHandler = $parse(attr.ngClick),
tapping = false,
tapElement, // Used to blur the element after a tap.
startTime, // Used to check if the tap was held too long.
touchStartX,
touchStartY;
function resetState() {
tapping = false;
element.removeClass(ACTIVE_CLASS_NAME);
}
element.on('touchstart', function(event) {
tapping = true;
tapElement = event.target ? event.target : event.srcElement; // IE uses srcElement.
// Hack for Safari, which can target text nodes instead of containers.
if(tapElement.nodeType == 3) {
tapElement = tapElement.parentNode;
}
element.addClass(ACTIVE_CLASS_NAME);
startTime = Date.now();
var touches = event.touches && event.touches.length ? event.touches : [event];
var e = touches[0].originalEvent || touches[0];
touchStartX = e.clientX;
touchStartY = e.clientY;
});
element.on('touchmove', function(event) {
resetState();
});
element.on('touchcancel', function(event) {
resetState();
});
element.on('touchend', function(event) {
var diff = Date.now() - startTime;
var touches = (event.changedTouches && event.changedTouches.length) ? event.changedTouches :
((event.touches && event.touches.length) ? event.touches : [event]);
var e = touches[0].originalEvent || touches[0];
var x = e.clientX;
var y = e.clientY;
var dist = Math.sqrt( Math.pow(x - touchStartX, 2) + Math.pow(y - touchStartY, 2) );
if (tapping && diff < TAP_DURATION && dist < MOVE_TOLERANCE) {
// Call preventGhostClick so the clickbuster will catch the corresponding click.
preventGhostClick(x, y);
// Blur the focused element (the button, probably) before firing the callback.
// This doesn't work perfectly on Android Chrome, but seems to work elsewhere.
// I couldn't get anything to work reliably on Android Chrome.
if (tapElement) {
tapElement.blur();
}
if (!angular.isDefined(attr.disabled) || attr.disabled === false) {
element.triggerHandler('click', [event]);
}
}
resetState();
});
// Hack for iOS Safari's benefit. It goes searching for onclick handlers and is liable to click
// something else nearby.
element.onclick = function(event) { };
// Actual click handler.
// There are three different kinds of clicks, only two of which reach this point.
// - On desktop browsers without touch events, their clicks will always come here.
// - On mobile browsers, the simulated "fast" click will call this.
// - But the browser's follow-up slow click will be "busted" before it reaches this handler.
// Therefore it's safe to use this directive on both mobile and desktop.
element.on('click', function(event, touchend) {
scope.$apply(function() {
clickHandler(scope, {$event: (touchend || event)});
});
});
element.on('mousedown', function(event) {
element.addClass(ACTIVE_CLASS_NAME);
});
element.on('mousemove mouseup', function(event) {
element.removeClass(ACTIVE_CLASS_NAME);
});
};
}]);
/* global ngTouch: false */
/**
* @ngdoc directive
* @name ngTouch.directive:ngSwipeLeft
*
* @description
* Specify custom behavior when an element is swiped to the left on a touchscreen device.
* A leftward swipe is a quick, right-to-left slide of the finger.
* Though ngSwipeLeft is designed for touch-based devices, it will work with a mouse click and drag
* too.
*
* Requires the {@link ngTouch `ngTouch`} module to be installed.
*
* @element ANY
* @param {expression} ngSwipeLeft {@link guide/expression Expression} to evaluate
* upon left swipe. (Event object is available as `$event`)
*
* @example
<doc:example>
<doc:source>
<div ng-show="!showActions" ng-swipe-left="showActions = true">
Some list content, like an email in the inbox
</div>
<div ng-show="showActions" ng-swipe-right="showActions = false">
<button ng-click="reply()">Reply</button>
<button ng-click="delete()">Delete</button>
</div>
</doc:source>
</doc:example>
*/
/**
* @ngdoc directive
* @name ngTouch.directive:ngSwipeRight
*
* @description
* Specify custom behavior when an element is swiped to the right on a touchscreen device.
* A rightward swipe is a quick, left-to-right slide of the finger.
* Though ngSwipeRight is designed for touch-based devices, it will work with a mouse click and drag
* too.
*
* Requires the {@link ngTouch `ngTouch`} module to be installed.
*
* @element ANY
* @param {expression} ngSwipeRight {@link guide/expression Expression} to evaluate
* upon right swipe. (Event object is available as `$event`)
*
* @example
<doc:example>
<doc:source>
<div ng-show="!showActions" ng-swipe-left="showActions = true">
Some list content, like an email in the inbox
</div>
<div ng-show="showActions" ng-swipe-right="showActions = false">
<button ng-click="reply()">Reply</button>
<button ng-click="delete()">Delete</button>
</div>
</doc:source>
</doc:example>
*/
function makeSwipeDirective(directiveName, direction, eventName) {
ngTouch.directive(directiveName, ['$parse', '$swipe', function($parse, $swipe) {
// The maximum vertical delta for a swipe should be less than 75px.
var MAX_VERTICAL_DISTANCE = 75;
// Vertical distance should not be more than a fraction of the horizontal distance.
var MAX_VERTICAL_RATIO = 0.3;
// At least a 30px lateral motion is necessary for a swipe.
var MIN_HORIZONTAL_DISTANCE = 30;
return function(scope, element, attr) {
var swipeHandler = $parse(attr[directiveName]);
var startCoords, valid;
function validSwipe(coords) {
// Check that it's within the coordinates.
// Absolute vertical distance must be within tolerances.
// Horizontal distance, we take the current X - the starting X.
// This is negative for leftward swipes and positive for rightward swipes.
// After multiplying by the direction (-1 for left, +1 for right), legal swipes
// (ie. same direction as the directive wants) will have a positive delta and
// illegal ones a negative delta.
// Therefore this delta must be positive, and larger than the minimum.
if (!startCoords) return false;
var deltaY = Math.abs(coords.y - startCoords.y);
var deltaX = (coords.x - startCoords.x) * direction;
return valid && // Short circuit for already-invalidated swipes.
deltaY < MAX_VERTICAL_DISTANCE &&
deltaX > 0 &&
deltaX > MIN_HORIZONTAL_DISTANCE &&
deltaY / deltaX < MAX_VERTICAL_RATIO;
}
$swipe.bind(element, {
'start': function(coords, event) {
startCoords = coords;
valid = true;
},
'cancel': function(event) {
valid = false;
},
'end': function(coords, event) {
if (validSwipe(coords)) {
scope.$apply(function() {
element.triggerHandler(eventName);
swipeHandler(scope, {$event: event});
});
}
}
});
};
}]);
}
// Left is negative X-coordinate, right is positive.
makeSwipeDirective('ngSwipeLeft', -1, 'swipeleft');
makeSwipeDirective('ngSwipeRight', 1, 'swiperight');
})(window, window.angular);

13
app/vendor/angular/angular-touch.min.js vendored

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
/*
AngularJS v1.2.3
(c) 2010-2014 Google, Inc. http://angularjs.org
License: MIT
*/
(function(y,v,z){'use strict';function t(g,a,b){q.directive(g,["$parse","$swipe",function(l,n){var r=75,h=0.3,d=30;return function(p,m,k){function e(e){if(!u)return!1;var c=Math.abs(e.y-u.y);e=(e.x-u.x)*a;return f&&c<r&&0<e&&e>d&&c/e<h}var c=l(k[g]),u,f;n.bind(m,{start:function(e,c){u=e;f=!0},cancel:function(e){f=!1},end:function(a,f){e(a)&&p.$apply(function(){m.triggerHandler(b);c(p,{$event:f})})}})}}])}var q=v.module("ngTouch",[]);q.factory("$swipe",[function(){function g(a){var b=a.touches&&a.touches.length?
a.touches:[a];a=a.changedTouches&&a.changedTouches[0]||a.originalEvent&&a.originalEvent.changedTouches&&a.originalEvent.changedTouches[0]||b[0].originalEvent||b[0];return{x:a.clientX,y:a.clientY}}return{bind:function(a,b){var l,n,r,h,d=!1;a.on("touchstart mousedown",function(a){r=g(a);d=!0;n=l=0;h=r;b.start&&b.start(r,a)});a.on("touchcancel",function(a){d=!1;b.cancel&&b.cancel(a)});a.on("touchmove mousemove",function(a){if(d&&r){var m=g(a);l+=Math.abs(m.x-h.x);n+=Math.abs(m.y-h.y);h=m;10>l&&10>n||
(n>l?(d=!1,b.cancel&&b.cancel(a)):(a.preventDefault(),b.move&&b.move(m,a)))}});a.on("touchend mouseup",function(a){d&&(d=!1,b.end&&b.end(g(a),a))})}}}]);q.config(["$provide",function(g){g.decorator("ngClickDirective",["$delegate",function(a){a.shift();return a}])}]);q.directive("ngClick",["$parse","$timeout","$rootElement",function(g,a,b){function l(a,c,b){for(var f=0;f<a.length;f+=2)if(Math.abs(a[f]-c)<d&&Math.abs(a[f+1]-b)<d)return a.splice(f,f+2),!0;return!1}function n(a){if(!(Date.now()-m>h)){var c=
a.touches&&a.touches.length?a.touches:[a],b=c[0].clientX,c=c[0].clientY;1>b&&1>c||l(k,b,c)||(a.stopPropagation(),a.preventDefault(),a.target&&a.target.blur())}}function r(b){b=b.touches&&b.touches.length?b.touches:[b];var c=b[0].clientX,d=b[0].clientY;k.push(c,d);a(function(){for(var a=0;a<k.length;a+=2)if(k[a]==c&&k[a+1]==d){k.splice(a,a+2);break}},h,!1)}var h=2500,d=25,p="ng-click-active",m,k;return function(a,c,d){function f(){q=!1;c.removeClass(p)}var h=g(d.ngClick),q=!1,s,t,w,x;c.on("touchstart",
function(a){q=!0;s=a.target?a.target:a.srcElement;3==s.nodeType&&(s=s.parentNode);c.addClass(p);t=Date.now();a=a.touches&&a.touches.length?a.touches:[a];a=a[0].originalEvent||a[0];w=a.clientX;x=a.clientY});c.on("touchmove",function(a){f()});c.on("touchcancel",function(a){f()});c.on("touchend",function(a){var h=Date.now()-t,e=a.changedTouches&&a.changedTouches.length?a.changedTouches:a.touches&&a.touches.length?a.touches:[a],g=e[0].originalEvent||e[0],e=g.clientX,g=g.clientY,p=Math.sqrt(Math.pow(e-
w,2)+Math.pow(g-x,2));q&&(750>h&&12>p)&&(k||(b[0].addEventListener("click",n,!0),b[0].addEventListener("touchstart",r,!0),k=[]),m=Date.now(),l(k,e,g),s&&s.blur(),v.isDefined(d.disabled)&&!1!==d.disabled||c.triggerHandler("click",[a]));f()});c.onclick=function(a){};c.on("click",function(b,c){a.$apply(function(){h(a,{$event:c||b})})});c.on("mousedown",function(a){c.addClass(p)});c.on("mousemove mouseup",function(a){c.removeClass(p)})}}]);t("ngSwipeLeft",-1,"swipeleft");t("ngSwipeRight",1,"swiperight")})(window,
window.angular);
//# sourceMappingURL=angular-touch.min.js.map

8
app/vendor/angular/angular-touch.min.js.map vendored

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save