ui-bootstrap.js 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476
  1. /*
  2. * angular-ui-bootstrap
  3. * http://angular-ui.github.io/bootstrap/
  4. * Version: 0.13.4 - 2015-09-03
  5. * License: MIT
  6. */
  7. angular.module("ui.bootstrap", ["ui.bootstrap.position","ui.bootstrap.dateparser","ui.bootstrap.datepicker","ui.bootstrap.tabs"]);
  8. angular.module('ui.bootstrap.position', [])
  9. /**
  10. * A set of utility methods that can be use to retrieve position of DOM elements.
  11. * It is meant to be used where we need to absolute-position DOM elements in
  12. * relation to other, existing elements (this is the case for tooltips, popovers,
  13. * typeahead suggestions etc.).
  14. */
  15. .factory('$position', ['$document', '$window', function($document, $window) {
  16. function getStyle(el, cssprop) {
  17. if (el.currentStyle) { //IE
  18. return el.currentStyle[cssprop];
  19. } else if ($window.getComputedStyle) {
  20. return $window.getComputedStyle(el)[cssprop];
  21. }
  22. // finally try and get inline style
  23. return el.style[cssprop];
  24. }
  25. /**
  26. * Checks if a given element is statically positioned
  27. * @param element - raw DOM element
  28. */
  29. function isStaticPositioned(element) {
  30. return (getStyle(element, 'position') || 'static' ) === 'static';
  31. }
  32. /**
  33. * returns the closest, non-statically positioned parentOffset of a given element
  34. * @param element
  35. */
  36. var parentOffsetEl = function(element) {
  37. var docDomEl = $document[0];
  38. var offsetParent = element.offsetParent || docDomEl;
  39. while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) {
  40. offsetParent = offsetParent.offsetParent;
  41. }
  42. return offsetParent || docDomEl;
  43. };
  44. return {
  45. /**
  46. * Provides read-only equivalent of jQuery's position function:
  47. * http://api.jquery.com/position/
  48. */
  49. position: function(element) {
  50. var elBCR = this.offset(element);
  51. var offsetParentBCR = { top: 0, left: 0 };
  52. var offsetParentEl = parentOffsetEl(element[0]);
  53. if (offsetParentEl != $document[0]) {
  54. offsetParentBCR = this.offset(angular.element(offsetParentEl));
  55. offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop;
  56. offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft;
  57. }
  58. var boundingClientRect = element[0].getBoundingClientRect();
  59. return {
  60. width: boundingClientRect.width || element.prop('offsetWidth'),
  61. height: boundingClientRect.height || element.prop('offsetHeight'),
  62. top: elBCR.top - offsetParentBCR.top,
  63. left: elBCR.left - offsetParentBCR.left
  64. };
  65. },
  66. /**
  67. * Provides read-only equivalent of jQuery's offset function:
  68. * http://api.jquery.com/offset/
  69. */
  70. offset: function(element) {
  71. var boundingClientRect = element[0].getBoundingClientRect();
  72. return {
  73. width: boundingClientRect.width || element.prop('offsetWidth'),
  74. height: boundingClientRect.height || element.prop('offsetHeight'),
  75. top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop),
  76. left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft)
  77. };
  78. },
  79. /**
  80. * Provides coordinates for the targetEl in relation to hostEl
  81. */
  82. positionElements: function(hostEl, targetEl, positionStr, appendToBody) {
  83. var positionStrParts = positionStr.split('-');
  84. var pos0 = positionStrParts[0], pos1 = positionStrParts[1] || 'center';
  85. var hostElPos,
  86. targetElWidth,
  87. targetElHeight,
  88. targetElPos;
  89. hostElPos = appendToBody ? this.offset(hostEl) : this.position(hostEl);
  90. targetElWidth = targetEl.prop('offsetWidth');
  91. targetElHeight = targetEl.prop('offsetHeight');
  92. var shiftWidth = {
  93. center: function() {
  94. return hostElPos.left + hostElPos.width / 2 - targetElWidth / 2;
  95. },
  96. left: function() {
  97. return hostElPos.left;
  98. },
  99. right: function() {
  100. return hostElPos.left + hostElPos.width;
  101. }
  102. };
  103. var shiftHeight = {
  104. center: function() {
  105. return hostElPos.top + hostElPos.height / 2 - targetElHeight / 2;
  106. },
  107. top: function() {
  108. return hostElPos.top;
  109. },
  110. bottom: function() {
  111. return hostElPos.top + hostElPos.height;
  112. }
  113. };
  114. switch (pos0) {
  115. case 'right':
  116. targetElPos = {
  117. top: shiftHeight[pos1](),
  118. left: shiftWidth[pos0]()
  119. };
  120. break;
  121. case 'left':
  122. targetElPos = {
  123. top: shiftHeight[pos1](),
  124. left: hostElPos.left - targetElWidth
  125. };
  126. break;
  127. case 'bottom':
  128. targetElPos = {
  129. top: shiftHeight[pos0](),
  130. left: shiftWidth[pos1]()
  131. };
  132. break;
  133. default:
  134. targetElPos = {
  135. top: hostElPos.top - targetElHeight,
  136. left: shiftWidth[pos1]()
  137. };
  138. break;
  139. }
  140. return targetElPos;
  141. }
  142. };
  143. }]);
  144. angular.module('ui.bootstrap.dateparser', [])
  145. .service('dateParser', ['$log', '$locale', 'orderByFilter', function($log, $locale, orderByFilter) {
  146. // Pulled from https://github.com/mbostock/d3/blob/master/src/format/requote.js
  147. var SPECIAL_CHARACTERS_REGEXP = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;
  148. this.parsers = {};
  149. var formatCodeToRegex = {
  150. 'yyyy': {
  151. regex: '\\d{4}',
  152. apply: function(value) { this.year = +value; }
  153. },
  154. 'yy': {
  155. regex: '\\d{2}',
  156. apply: function(value) { this.year = +value + 2000; }
  157. },
  158. 'y': {
  159. regex: '\\d{1,4}',
  160. apply: function(value) { this.year = +value; }
  161. },
  162. 'MMMM': {
  163. regex: $locale.DATETIME_FORMATS.MONTH.join('|'),
  164. apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); }
  165. },
  166. 'MMM': {
  167. regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'),
  168. apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); }
  169. },
  170. 'MM': {
  171. regex: '0[1-9]|1[0-2]',
  172. apply: function(value) { this.month = value - 1; }
  173. },
  174. 'M': {
  175. regex: '[1-9]|1[0-2]',
  176. apply: function(value) { this.month = value - 1; }
  177. },
  178. 'dd': {
  179. regex: '[0-2][0-9]{1}|3[0-1]{1}',
  180. apply: function(value) { this.date = +value; }
  181. },
  182. 'd': {
  183. regex: '[1-2]?[0-9]{1}|3[0-1]{1}',
  184. apply: function(value) { this.date = +value; }
  185. },
  186. 'EEEE': {
  187. regex: $locale.DATETIME_FORMATS.DAY.join('|')
  188. },
  189. 'EEE': {
  190. regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|')
  191. },
  192. 'HH': {
  193. regex: '(?:0|1)[0-9]|2[0-3]',
  194. apply: function(value) { this.hours = +value; }
  195. },
  196. 'hh': {
  197. regex: '0[0-9]|1[0-2]',
  198. apply: function(value) { this.hours = +value; }
  199. },
  200. 'H': {
  201. regex: '1?[0-9]|2[0-3]',
  202. apply: function(value) { this.hours = +value; }
  203. },
  204. 'h': {
  205. regex: '[0-9]|1[0-2]',
  206. apply: function(value) { this.hours = +value; }
  207. },
  208. 'mm': {
  209. regex: '[0-5][0-9]',
  210. apply: function(value) { this.minutes = +value; }
  211. },
  212. 'm': {
  213. regex: '[0-9]|[1-5][0-9]',
  214. apply: function(value) { this.minutes = +value; }
  215. },
  216. 'sss': {
  217. regex: '[0-9][0-9][0-9]',
  218. apply: function(value) { this.milliseconds = +value; }
  219. },
  220. 'ss': {
  221. regex: '[0-5][0-9]',
  222. apply: function(value) { this.seconds = +value; }
  223. },
  224. 's': {
  225. regex: '[0-9]|[1-5][0-9]',
  226. apply: function(value) { this.seconds = +value; }
  227. },
  228. 'a': {
  229. regex: $locale.DATETIME_FORMATS.AMPMS.join('|'),
  230. apply: function(value) {
  231. if (this.hours === 12) {
  232. this.hours = 0;
  233. }
  234. if (value === 'PM') {
  235. this.hours += 12;
  236. }
  237. }
  238. }
  239. };
  240. function createParser(format) {
  241. var map = [], regex = format.split('');
  242. angular.forEach(formatCodeToRegex, function(data, code) {
  243. var index = format.indexOf(code);
  244. if (index > -1) {
  245. format = format.split('');
  246. regex[index] = '(' + data.regex + ')';
  247. format[index] = '$'; // Custom symbol to define consumed part of format
  248. for (var i = index + 1, n = index + code.length; i < n; i++) {
  249. regex[i] = '';
  250. format[i] = '$';
  251. }
  252. format = format.join('');
  253. map.push({ index: index, apply: data.apply });
  254. }
  255. });
  256. return {
  257. regex: new RegExp('^' + regex.join('') + '$'),
  258. map: orderByFilter(map, 'index')
  259. };
  260. }
  261. this.parse = function(input, format, baseDate) {
  262. if (!angular.isString(input) || !format) {
  263. return input;
  264. }
  265. format = $locale.DATETIME_FORMATS[format] || format;
  266. format = format.replace(SPECIAL_CHARACTERS_REGEXP, '\\$&');
  267. if (!this.parsers[format]) {
  268. this.parsers[format] = createParser(format);
  269. }
  270. var parser = this.parsers[format],
  271. regex = parser.regex,
  272. map = parser.map,
  273. results = input.match(regex);
  274. if (results && results.length) {
  275. var fields, dt;
  276. if (angular.isDate(baseDate) && !isNaN(baseDate.getTime())) {
  277. fields = {
  278. year: baseDate.getFullYear(),
  279. month: baseDate.getMonth(),
  280. date: baseDate.getDate(),
  281. hours: baseDate.getHours(),
  282. minutes: baseDate.getMinutes(),
  283. seconds: baseDate.getSeconds(),
  284. milliseconds: baseDate.getMilliseconds()
  285. };
  286. } else {
  287. if (baseDate) {
  288. $log.warn('dateparser:', 'baseDate is not a valid date');
  289. }
  290. fields = { year: 1900, month: 0, date: 1, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 };
  291. }
  292. for (var i = 1, n = results.length; i < n; i++) {
  293. var mapper = map[i-1];
  294. if (mapper.apply) {
  295. mapper.apply.call(fields, results[i]);
  296. }
  297. }
  298. if (isValid(fields.year, fields.month, fields.date)) {
  299. dt = new Date(fields.year, fields.month, fields.date,
  300. fields.hours, fields.minutes, fields.seconds,
  301. fields.milliseconds || 0);
  302. }
  303. return dt;
  304. }
  305. };
  306. // Check if date is valid for specific month (and year for February).
  307. // Month: 0 = Jan, 1 = Feb, etc
  308. function isValid(year, month, date) {
  309. if (date < 1) {
  310. return false;
  311. }
  312. if (month === 1 && date > 28) {
  313. return date === 29 && ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0);
  314. }
  315. if (month === 3 || month === 5 || month === 8 || month === 10) {
  316. return date < 31;
  317. }
  318. return true;
  319. }
  320. }]);
  321. angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.position'])
  322. .value('$datepickerSuppressError', false)
  323. .constant('datepickerConfig', {
  324. formatDay: 'dd',
  325. formatMonth: 'MMMM',
  326. formatYear: 'yyyy',
  327. formatDayHeader: 'EEE',
  328. formatDayTitle: 'MMMM yyyy',
  329. formatMonthTitle: 'yyyy',
  330. datepickerMode: 'day',
  331. minMode: 'day',
  332. maxMode: 'year',
  333. showWeeks: true,
  334. startingDay: 0,
  335. yearRange: 20,
  336. minDate: null,
  337. maxDate: null,
  338. shortcutPropagation: false
  339. })
  340. .controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$log', 'dateFilter', 'datepickerConfig', '$datepickerSuppressError', function($scope, $attrs, $parse, $interpolate, $log, dateFilter, datepickerConfig, $datepickerSuppressError) {
  341. var self = this,
  342. ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl;
  343. // Modes chain
  344. this.modes = ['day', 'month', 'year'];
  345. // Configuration attributes
  346. angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle',
  347. 'showWeeks', 'startingDay', 'yearRange', 'shortcutPropagation'], function(key, index) {
  348. self[key] = angular.isDefined($attrs[key]) ? (index < 6 ? $interpolate($attrs[key])($scope.$parent) : $scope.$parent.$eval($attrs[key])) : datepickerConfig[key];
  349. });
  350. // Watchable date attributes
  351. angular.forEach(['minDate', 'maxDate'], function(key) {
  352. if ($attrs[key]) {
  353. $scope.$parent.$watch($parse($attrs[key]), function(value) {
  354. self[key] = value ? new Date(value) : null;
  355. self.refreshView();
  356. });
  357. } else {
  358. self[key] = datepickerConfig[key] ? new Date(datepickerConfig[key]) : null;
  359. }
  360. });
  361. angular.forEach(['minMode', 'maxMode'], function(key) {
  362. if ($attrs[key]) {
  363. $scope.$parent.$watch($parse($attrs[key]), function(value) {
  364. self[key] = angular.isDefined(value) ? value : $attrs[key];
  365. $scope[key] = self[key];
  366. if ((key == 'minMode' && self.modes.indexOf($scope.datepickerMode) < self.modes.indexOf(self[key])) || (key == 'maxMode' && self.modes.indexOf($scope.datepickerMode) > self.modes.indexOf(self[key]))) {
  367. $scope.datepickerMode = self[key];
  368. }
  369. });
  370. } else {
  371. self[key] = datepickerConfig[key] || null;
  372. $scope[key] = self[key];
  373. }
  374. });
  375. $scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode;
  376. $scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000);
  377. if (angular.isDefined($attrs.initDate)) {
  378. this.activeDate = $scope.$parent.$eval($attrs.initDate) || new Date();
  379. $scope.$parent.$watch($attrs.initDate, function(initDate) {
  380. if (initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)) {
  381. self.activeDate = initDate;
  382. self.refreshView();
  383. }
  384. });
  385. } else {
  386. this.activeDate = new Date();
  387. }
  388. $scope.isActive = function(dateObject) {
  389. if (self.compare(dateObject.date, self.activeDate) === 0) {
  390. $scope.activeDateId = dateObject.uid;
  391. return true;
  392. }
  393. return false;
  394. };
  395. this.init = function(ngModelCtrl_) {
  396. ngModelCtrl = ngModelCtrl_;
  397. ngModelCtrl.$render = function() {
  398. self.render();
  399. };
  400. };
  401. this.render = function() {
  402. if (ngModelCtrl.$viewValue) {
  403. var date = new Date(ngModelCtrl.$viewValue),
  404. isValid = !isNaN(date);
  405. if (isValid) {
  406. this.activeDate = date;
  407. } else if (!$datepickerSuppressError) {
  408. $log.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.');
  409. }
  410. }
  411. this.refreshView();
  412. };
  413. this.refreshView = function() {
  414. if (this.element) {
  415. this._refreshView();
  416. var date = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
  417. ngModelCtrl.$setValidity('dateDisabled', !date || (this.element && !this.isDisabled(date)));
  418. }
  419. };
  420. this.createDateObject = function(date, format) {
  421. var model = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
  422. return {
  423. date: date,
  424. label: dateFilter(date, format),
  425. selected: model && this.compare(date, model) === 0,
  426. disabled: this.isDisabled(date),
  427. current: this.compare(date, new Date()) === 0,
  428. customClass: this.customClass(date)
  429. };
  430. };
  431. this.isDisabled = function(date) {
  432. return ((this.minDate && this.compare(date, this.minDate) < 0) || (this.maxDate && this.compare(date, this.maxDate) > 0) || ($attrs.dateDisabled && $scope.dateDisabled({date: date, mode: $scope.datepickerMode})));
  433. };
  434. this.customClass = function(date) {
  435. return $scope.customClass({date: date, mode: $scope.datepickerMode});
  436. };
  437. // Split array into smaller arrays
  438. this.split = function(arr, size) {
  439. var arrays = [];
  440. while (arr.length > 0) {
  441. arrays.push(arr.splice(0, size));
  442. }
  443. return arrays;
  444. };
  445. // Fix a hard-reprodusible bug with timezones
  446. // The bug depends on OS, browser, current timezone and current date
  447. // i.e.
  448. // var date = new Date(2014, 0, 1);
  449. // console.log(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours());
  450. // can result in "2013 11 31 23" because of the bug.
  451. this.fixTimeZone = function(date) {
  452. var hours = date.getHours();
  453. date.setHours(hours === 23 ? hours + 2 : 0);
  454. };
  455. $scope.select = function(date) {
  456. if ($scope.datepickerMode === self.minMode) {
  457. var dt = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : new Date(0, 0, 0, 0, 0, 0, 0);
  458. dt.setFullYear(date.getFullYear(), date.getMonth(), date.getDate());
  459. ngModelCtrl.$setViewValue(dt);
  460. ngModelCtrl.$render();
  461. } else {
  462. self.activeDate = date;
  463. $scope.datepickerMode = self.modes[self.modes.indexOf($scope.datepickerMode) - 1];
  464. }
  465. };
  466. $scope.move = function(direction) {
  467. var year = self.activeDate.getFullYear() + direction * (self.step.years || 0),
  468. month = self.activeDate.getMonth() + direction * (self.step.months || 0);
  469. self.activeDate.setFullYear(year, month, 1);
  470. self.refreshView();
  471. };
  472. $scope.toggleMode = function(direction) {
  473. direction = direction || 1;
  474. if (($scope.datepickerMode === self.maxMode && direction === 1) || ($scope.datepickerMode === self.minMode && direction === -1)) {
  475. return;
  476. }
  477. $scope.datepickerMode = self.modes[self.modes.indexOf($scope.datepickerMode) + direction];
  478. };
  479. // Key event mapper
  480. $scope.keys = { 13: 'enter', 32: 'space', 33: 'pageup', 34: 'pagedown', 35: 'end', 36: 'home', 37: 'left', 38: 'up', 39: 'right', 40: 'down' };
  481. var focusElement = function() {
  482. self.element[0].focus();
  483. };
  484. // Listen for focus requests from popup directive
  485. $scope.$on('datepicker.focus', focusElement);
  486. $scope.keydown = function(evt) {
  487. var key = $scope.keys[evt.which];
  488. if (!key || evt.shiftKey || evt.altKey) {
  489. return;
  490. }
  491. evt.preventDefault();
  492. if (!self.shortcutPropagation) {
  493. evt.stopPropagation();
  494. }
  495. if (key === 'enter' || key === 'space') {
  496. if (self.isDisabled(self.activeDate)) {
  497. return; // do nothing
  498. }
  499. $scope.select(self.activeDate);
  500. focusElement();
  501. } else if (evt.ctrlKey && (key === 'up' || key === 'down')) {
  502. $scope.toggleMode(key === 'up' ? 1 : -1);
  503. focusElement();
  504. } else {
  505. self.handleKeyDown(key, evt);
  506. self.refreshView();
  507. }
  508. };
  509. }])
  510. .directive('datepicker', function() {
  511. return {
  512. restrict: 'EA',
  513. replace: true,
  514. templateUrl: function(element, attrs) {
  515. return attrs.templateUrl || 'app/partials/bootstrap/datepicker.html';
  516. },
  517. scope: {
  518. datepickerMode: '=?',
  519. dateDisabled: '&',
  520. customClass: '&',
  521. shortcutPropagation: '&?'
  522. },
  523. require: ['datepicker', '^ngModel'],
  524. controller: 'DatepickerController',
  525. controllerAs: 'datepicker',
  526. link: function(scope, element, attrs, ctrls) {
  527. var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1];
  528. datepickerCtrl.init(ngModelCtrl);
  529. }
  530. };
  531. })
  532. .directive('daypicker', ['dateFilter', function(dateFilter) {
  533. return {
  534. restrict: 'EA',
  535. replace: true,
  536. templateUrl: 'app/partials/bootstrap/day.html',
  537. require: '^datepicker',
  538. link: function(scope, element, attrs, ctrl) {
  539. scope.showWeeks = ctrl.showWeeks;
  540. ctrl.step = { months: 1 };
  541. ctrl.element = element;
  542. var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
  543. function getDaysInMonth(year, month) {
  544. return ((month === 1) && (year % 4 === 0) && ((year % 100 !== 0) || (year % 400 === 0))) ? 29 : DAYS_IN_MONTH[month];
  545. }
  546. function getDates(startDate, n) {
  547. var dates = new Array(n), current = new Date(startDate), i = 0, date;
  548. while (i < n) {
  549. date = new Date(current);
  550. ctrl.fixTimeZone(date);
  551. dates[i++] = date;
  552. current.setDate(current.getDate() + 1);
  553. }
  554. return dates;
  555. }
  556. ctrl._refreshView = function() {
  557. var year = ctrl.activeDate.getFullYear(),
  558. month = ctrl.activeDate.getMonth(),
  559. firstDayOfMonth = new Date(year, month, 1),
  560. difference = ctrl.startingDay - firstDayOfMonth.getDay(),
  561. numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference,
  562. firstDate = new Date(firstDayOfMonth);
  563. if (numDisplayedFromPreviousMonth > 0) {
  564. firstDate.setDate(-numDisplayedFromPreviousMonth + 1);
  565. }
  566. // 42 is the number of days on a six-month calendar
  567. var days = getDates(firstDate, 42);
  568. for (var i = 0; i < 42; i ++) {
  569. days[i] = angular.extend(ctrl.createDateObject(days[i], ctrl.formatDay), {
  570. secondary: days[i].getMonth() !== month,
  571. uid: scope.uniqueId + '-' + i
  572. });
  573. }
  574. scope.labels = new Array(7);
  575. for (var j = 0; j < 7; j++) {
  576. scope.labels[j] = {
  577. abbr: dateFilter(days[j].date, ctrl.formatDayHeader),
  578. full: dateFilter(days[j].date, 'EEEE')
  579. };
  580. }
  581. scope.title = dateFilter(ctrl.activeDate, ctrl.formatDayTitle);
  582. scope.rows = ctrl.split(days, 7);
  583. if (scope.showWeeks) {
  584. scope.weekNumbers = [];
  585. var thursdayIndex = (4 + 7 - ctrl.startingDay) % 7,
  586. numWeeks = scope.rows.length;
  587. for (var curWeek = 0; curWeek < numWeeks; curWeek++) {
  588. scope.weekNumbers.push(
  589. getISO8601WeekNumber(scope.rows[curWeek][thursdayIndex].date));
  590. }
  591. }
  592. };
  593. ctrl.compare = function(date1, date2) {
  594. return (new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()) - new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()));
  595. };
  596. function getISO8601WeekNumber(date) {
  597. var checkDate = new Date(date);
  598. checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday
  599. var time = checkDate.getTime();
  600. checkDate.setMonth(0); // Compare with Jan 1
  601. checkDate.setDate(1);
  602. return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1;
  603. }
  604. ctrl.handleKeyDown = function(key, evt) {
  605. var date = ctrl.activeDate.getDate();
  606. if (key === 'left') {
  607. date = date - 1; // up
  608. } else if (key === 'up') {
  609. date = date - 7; // down
  610. } else if (key === 'right') {
  611. date = date + 1; // down
  612. } else if (key === 'down') {
  613. date = date + 7;
  614. } else if (key === 'pageup' || key === 'pagedown') {
  615. var month = ctrl.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1);
  616. ctrl.activeDate.setMonth(month, 1);
  617. date = Math.min(getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()), date);
  618. } else if (key === 'home') {
  619. date = 1;
  620. } else if (key === 'end') {
  621. date = getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth());
  622. }
  623. ctrl.activeDate.setDate(date);
  624. };
  625. ctrl.refreshView();
  626. }
  627. };
  628. }])
  629. .directive('monthpicker', ['dateFilter', function(dateFilter) {
  630. return {
  631. restrict: 'EA',
  632. replace: true,
  633. templateUrl: 'app/partials/bootstrap/month.html',
  634. require: '^datepicker',
  635. link: function(scope, element, attrs, ctrl) {
  636. ctrl.step = { years: 1 };
  637. ctrl.element = element;
  638. ctrl._refreshView = function() {
  639. var months = new Array(12),
  640. year = ctrl.activeDate.getFullYear(),
  641. date;
  642. for (var i = 0; i < 12; i++) {
  643. date = new Date(year, i, 1);
  644. ctrl.fixTimeZone(date);
  645. months[i] = angular.extend(ctrl.createDateObject(date, ctrl.formatMonth), {
  646. uid: scope.uniqueId + '-' + i
  647. });
  648. }
  649. scope.title = dateFilter(ctrl.activeDate, ctrl.formatMonthTitle);
  650. scope.rows = ctrl.split(months, 3);
  651. };
  652. ctrl.compare = function(date1, date2) {
  653. return new Date(date1.getFullYear(), date1.getMonth()) - new Date(date2.getFullYear(), date2.getMonth());
  654. };
  655. ctrl.handleKeyDown = function(key, evt) {
  656. var date = ctrl.activeDate.getMonth();
  657. if (key === 'left') {
  658. date = date - 1; // up
  659. } else if (key === 'up') {
  660. date = date - 3; // down
  661. } else if (key === 'right') {
  662. date = date + 1; // down
  663. } else if (key === 'down') {
  664. date = date + 3;
  665. } else if (key === 'pageup' || key === 'pagedown') {
  666. var year = ctrl.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1);
  667. ctrl.activeDate.setFullYear(year);
  668. } else if (key === 'home') {
  669. date = 0;
  670. } else if (key === 'end') {
  671. date = 11;
  672. }
  673. ctrl.activeDate.setMonth(date);
  674. };
  675. ctrl.refreshView();
  676. }
  677. };
  678. }])
  679. .directive('yearpicker', ['dateFilter', function(dateFilter) {
  680. return {
  681. restrict: 'EA',
  682. replace: true,
  683. templateUrl: 'app/partials/bootstrap/year.html',
  684. require: '^datepicker',
  685. link: function(scope, element, attrs, ctrl) {
  686. var range = ctrl.yearRange;
  687. ctrl.step = { years: range };
  688. ctrl.element = element;
  689. function getStartingYear( year ) {
  690. return parseInt((year - 1) / range, 10) * range + 1;
  691. }
  692. ctrl._refreshView = function() {
  693. var years = new Array(range), date;
  694. for (var i = 0, start = getStartingYear(ctrl.activeDate.getFullYear()); i < range; i++) {
  695. date = new Date(start + i, 0, 1);
  696. ctrl.fixTimeZone(date);
  697. years[i] = angular.extend(ctrl.createDateObject(date, ctrl.formatYear), {
  698. uid: scope.uniqueId + '-' + i
  699. });
  700. }
  701. scope.title = [years[0].label, years[range - 1].label].join(' - ');
  702. scope.rows = ctrl.split(years, 5);
  703. };
  704. ctrl.compare = function(date1, date2) {
  705. return date1.getFullYear() - date2.getFullYear();
  706. };
  707. ctrl.handleKeyDown = function(key, evt) {
  708. var date = ctrl.activeDate.getFullYear();
  709. if (key === 'left') {
  710. date = date - 1; // up
  711. } else if (key === 'up') {
  712. date = date - 5; // down
  713. } else if (key === 'right') {
  714. date = date + 1; // down
  715. } else if (key === 'down') {
  716. date = date + 5;
  717. } else if (key === 'pageup' || key === 'pagedown') {
  718. date += (key === 'pageup' ? - 1 : 1) * ctrl.step.years;
  719. } else if (key === 'home') {
  720. date = getStartingYear(ctrl.activeDate.getFullYear());
  721. } else if (key === 'end') {
  722. date = getStartingYear(ctrl.activeDate.getFullYear()) + range - 1;
  723. }
  724. ctrl.activeDate.setFullYear(date);
  725. };
  726. ctrl.refreshView();
  727. }
  728. };
  729. }])
  730. .constant('datepickerPopupConfig', {
  731. datepickerPopup: 'yyyy-MM-dd',
  732. datepickerPopupTemplateUrl: 'app/partials/bootstrap/popup.html',
  733. datepickerTemplateUrl: 'app/partials/bootstrap/datepicker.html',
  734. html5Types: {
  735. date: 'yyyy-MM-dd',
  736. 'datetime-local': 'yyyy-MM-ddTHH:mm:ss.sss',
  737. 'month': 'yyyy-MM'
  738. },
  739. currentText: 'Today',
  740. clearText: 'Clear',
  741. closeText: 'Done',
  742. closeOnDateSelection: true,
  743. appendToBody: false,
  744. showButtonBar: true,
  745. onOpenFocus: true
  746. })
  747. .directive('datepickerPopup', ['$compile', '$parse', '$document', '$rootScope', '$position', 'dateFilter', 'dateParser', 'datepickerPopupConfig', '$timeout',
  748. function($compile, $parse, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout) {
  749. return {
  750. restrict: 'EA',
  751. require: 'ngModel',
  752. scope: {
  753. isOpen: '=?',
  754. currentText: '@',
  755. clearText: '@',
  756. closeText: '@',
  757. dateDisabled: '&',
  758. customClass: '&'
  759. },
  760. link: function(scope, element, attrs, ngModel) {
  761. var dateFormat,
  762. closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$parent.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection,
  763. appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? scope.$parent.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody,
  764. onOpenFocus = angular.isDefined(attrs.onOpenFocus) ? scope.$parent.$eval(attrs.onOpenFocus) : datepickerPopupConfig.onOpenFocus,
  765. datepickerPopupTemplateUrl = angular.isDefined(attrs.datepickerPopupTemplateUrl) ? attrs.datepickerPopupTemplateUrl : datepickerPopupConfig.datepickerPopupTemplateUrl,
  766. datepickerTemplateUrl = angular.isDefined(attrs.datepickerTemplateUrl) ? attrs.datepickerTemplateUrl : datepickerPopupConfig.datepickerTemplateUrl,
  767. cache = {};
  768. scope.showButtonBar = angular.isDefined(attrs.showButtonBar) ? scope.$parent.$eval(attrs.showButtonBar) : datepickerPopupConfig.showButtonBar;
  769. scope.getText = function(key) {
  770. return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text'];
  771. };
  772. scope.isDisabled = function(date) {
  773. if (date === 'today') {
  774. date = new Date();
  775. }
  776. return ((scope.watchData.minDate && scope.compare(date, cache.minDate) < 0) ||
  777. (scope.watchData.maxDate && scope.compare(date, cache.maxDate) > 0));
  778. };
  779. scope.compare = function(date1, date2) {
  780. return (new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()) - new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()));
  781. };
  782. var isHtml5DateInput = false;
  783. if (datepickerPopupConfig.html5Types[attrs.type]) {
  784. dateFormat = datepickerPopupConfig.html5Types[attrs.type];
  785. isHtml5DateInput = true;
  786. } else {
  787. dateFormat = attrs.datepickerPopup || datepickerPopupConfig.datepickerPopup;
  788. attrs.$observe('datepickerPopup', function(value, oldValue) {
  789. var newDateFormat = value || datepickerPopupConfig.datepickerPopup;
  790. // Invalidate the $modelValue to ensure that formatters re-run
  791. // FIXME: Refactor when PR is merged: https://github.com/angular/angular.js/pull/10764
  792. if (newDateFormat !== dateFormat) {
  793. dateFormat = newDateFormat;
  794. ngModel.$modelValue = null;
  795. if (!dateFormat) {
  796. throw new Error('datepickerPopup must have a date format specified.');
  797. }
  798. }
  799. });
  800. }
  801. if (!dateFormat) {
  802. throw new Error('datepickerPopup must have a date format specified.');
  803. }
  804. if (isHtml5DateInput && attrs.datepickerPopup) {
  805. throw new Error('HTML5 date input types do not support custom formats.');
  806. }
  807. // popup element used to display calendar
  808. var popupEl = angular.element('<div datepicker-popup-wrap><div datepicker></div></div>');
  809. popupEl.attr({
  810. 'ng-model': 'date',
  811. 'ng-change': 'dateSelection(date)',
  812. 'template-url': datepickerPopupTemplateUrl
  813. });
  814. function cameltoDash(string) {
  815. return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); });
  816. }
  817. // datepicker element
  818. var datepickerEl = angular.element(popupEl.children()[0]);
  819. datepickerEl.attr('template-url', datepickerTemplateUrl);
  820. if (isHtml5DateInput) {
  821. if (attrs.type === 'month') {
  822. datepickerEl.attr('datepicker-mode', '"month"');
  823. datepickerEl.attr('min-mode', 'month');
  824. }
  825. }
  826. if (attrs.datepickerOptions) {
  827. var options = scope.$parent.$eval(attrs.datepickerOptions);
  828. if (options && options.initDate) {
  829. scope.initDate = options.initDate;
  830. datepickerEl.attr('init-date', 'initDate');
  831. delete options.initDate;
  832. }
  833. angular.forEach(options, function(value, option) {
  834. datepickerEl.attr( cameltoDash(option), value );
  835. });
  836. }
  837. scope.watchData = {};
  838. angular.forEach(['minMode', 'maxMode', 'minDate', 'maxDate', 'datepickerMode', 'initDate', 'shortcutPropagation'], function(key) {
  839. if (attrs[key]) {
  840. var getAttribute = $parse(attrs[key]);
  841. scope.$parent.$watch(getAttribute, function(value) {
  842. scope.watchData[key] = value;
  843. if (key === 'minDate' || key === 'maxDate') {
  844. cache[key] = new Date(value);
  845. }
  846. });
  847. datepickerEl.attr(cameltoDash(key), 'watchData.' + key);
  848. // Propagate changes from datepicker to outside
  849. if (key === 'datepickerMode') {
  850. var setAttribute = getAttribute.assign;
  851. scope.$watch('watchData.' + key, function(value, oldvalue) {
  852. if (angular.isFunction(setAttribute) && value !== oldvalue) {
  853. setAttribute(scope.$parent, value);
  854. }
  855. });
  856. }
  857. }
  858. });
  859. if (attrs.dateDisabled) {
  860. datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })');
  861. }
  862. if (attrs.showWeeks) {
  863. datepickerEl.attr('show-weeks', attrs.showWeeks);
  864. }
  865. if (attrs.customClass) {
  866. datepickerEl.attr('custom-class', 'customClass({ date: date, mode: mode })');
  867. }
  868. function parseDate(viewValue) {
  869. if (angular.isNumber(viewValue)) {
  870. // presumably timestamp to date object
  871. viewValue = new Date(viewValue);
  872. }
  873. if (!viewValue) {
  874. return null;
  875. } else if (angular.isDate(viewValue) && !isNaN(viewValue)) {
  876. return viewValue;
  877. } else if (angular.isString(viewValue)) {
  878. var date = dateParser.parse(viewValue, dateFormat, scope.date);
  879. if (isNaN(date)) {
  880. return undefined;
  881. } else {
  882. return date;
  883. }
  884. } else {
  885. return undefined;
  886. }
  887. }
  888. function validator(modelValue, viewValue) {
  889. var value = modelValue || viewValue;
  890. if (!attrs.ngRequired && !value) {
  891. return true;
  892. }
  893. if (angular.isNumber(value)) {
  894. value = new Date(value);
  895. }
  896. if (!value) {
  897. return true;
  898. } else if (angular.isDate(value) && !isNaN(value)) {
  899. return true;
  900. } else if (angular.isString(value)) {
  901. var date = dateParser.parse(value, dateFormat);
  902. return !isNaN(date);
  903. } else {
  904. return false;
  905. }
  906. }
  907. if (!isHtml5DateInput) {
  908. // Internal API to maintain the correct ng-invalid-[key] class
  909. ngModel.$$parserName = 'date';
  910. ngModel.$validators.date = validator;
  911. ngModel.$parsers.unshift(parseDate);
  912. ngModel.$formatters.push(function(value) {
  913. scope.date = value;
  914. return ngModel.$isEmpty(value) ? value : dateFilter(value, dateFormat);
  915. });
  916. } else {
  917. ngModel.$formatters.push(function(value) {
  918. scope.date = value;
  919. return value;
  920. });
  921. }
  922. // Inner change
  923. scope.dateSelection = function(dt) {
  924. if (angular.isDefined(dt)) {
  925. scope.date = dt;
  926. }
  927. var date = scope.date ? dateFilter(scope.date, dateFormat) : null; // Setting to NULL is necessary for form validators to function
  928. element.val(date);
  929. ngModel.$setViewValue(date);
  930. if (closeOnDateSelection) {
  931. scope.isOpen = false;
  932. element[0].focus();
  933. }
  934. };
  935. // Detect changes in the view from the text box
  936. ngModel.$viewChangeListeners.push(function() {
  937. scope.date = dateParser.parse(ngModel.$viewValue, dateFormat, scope.date);
  938. });
  939. var documentClickBind = function(event) {
  940. if (scope.isOpen && !(element[0].contains(event.target) || popupEl[0].contains(event.target))) {
  941. scope.$apply(function() {
  942. scope.isOpen = false;
  943. });
  944. }
  945. };
  946. var inputKeydownBind = function(evt) {
  947. if (evt.which === 27 && scope.isOpen) {
  948. evt.preventDefault();
  949. evt.stopPropagation();
  950. scope.$apply(function() {
  951. scope.isOpen = false;
  952. });
  953. element[0].focus();
  954. } else if (evt.which === 40 && !scope.isOpen) {
  955. evt.preventDefault();
  956. evt.stopPropagation();
  957. scope.$apply(function() {
  958. scope.isOpen = true;
  959. });
  960. }
  961. };
  962. element.bind('keydown', inputKeydownBind);
  963. scope.keydown = function(evt) {
  964. if (evt.which === 27) {
  965. scope.isOpen = false;
  966. element[0].focus();
  967. }
  968. };
  969. scope.$watch('isOpen', function(value) {
  970. if (value) {
  971. scope.position = appendToBody ? $position.offset(element) : $position.position(element);
  972. scope.position.top = scope.position.top + element.prop('offsetHeight');
  973. $timeout(function() {
  974. if (onOpenFocus) {
  975. scope.$broadcast('datepicker.focus');
  976. }
  977. $document.bind('click', documentClickBind);
  978. }, 0, false);
  979. } else {
  980. $document.unbind('click', documentClickBind);
  981. }
  982. });
  983. scope.select = function(date) {
  984. if (date === 'today') {
  985. var today = new Date();
  986. if (angular.isDate(scope.date)) {
  987. date = new Date(scope.date);
  988. date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate());
  989. } else {
  990. date = new Date(today.setHours(0, 0, 0, 0));
  991. }
  992. }
  993. scope.dateSelection(date);
  994. };
  995. scope.close = function() {
  996. scope.isOpen = false;
  997. element[0].focus();
  998. };
  999. var $popup = $compile(popupEl)(scope);
  1000. // Prevent jQuery cache memory leak (template is now redundant after linking)
  1001. popupEl.remove();
  1002. if (appendToBody) {
  1003. $document.find('body').append($popup);
  1004. } else {
  1005. element.after($popup);
  1006. }
  1007. scope.$on('$destroy', function() {
  1008. if (scope.isOpen === true) {
  1009. if (!$rootScope.$$phase) {
  1010. scope.$apply(function() {
  1011. scope.isOpen = false;
  1012. });
  1013. }
  1014. }
  1015. $popup.remove();
  1016. element.unbind('keydown', inputKeydownBind);
  1017. $document.unbind('click', documentClickBind);
  1018. });
  1019. }
  1020. };
  1021. }])
  1022. .directive('datepickerPopupWrap', function() {
  1023. return {
  1024. restrict:'EA',
  1025. replace: true,
  1026. transclude: true,
  1027. templateUrl: function(element, attrs) {
  1028. return attrs.templateUrl || 'template/datepicker/popup.html';
  1029. }
  1030. };
  1031. });
  1032. /**
  1033. * @ngdoc overview
  1034. * @name ui.bootstrap.tabs
  1035. *
  1036. * @description
  1037. * AngularJS version of the tabs directive.
  1038. */
  1039. angular.module('ui.bootstrap.tabs', [])
  1040. .controller('TabsetController', ['$scope', function TabsetCtrl($scope) {
  1041. var ctrl = this,
  1042. tabs = ctrl.tabs = $scope.tabs = [];
  1043. ctrl.select = function(selectedTab) {
  1044. angular.forEach(tabs, function(tab) {
  1045. if (tab.active && tab !== selectedTab) {
  1046. tab.active = false;
  1047. tab.onDeselect();
  1048. selectedTab.selectCalled = false;
  1049. }
  1050. });
  1051. selectedTab.active = true;
  1052. // only call select if it has not already been called
  1053. if (!selectedTab.selectCalled) {
  1054. selectedTab.onSelect();
  1055. selectedTab.selectCalled = true;
  1056. }
  1057. };
  1058. ctrl.addTab = function addTab(tab) {
  1059. tabs.push(tab);
  1060. // we can't run the select function on the first tab
  1061. // since that would select it twice
  1062. if (tabs.length === 1 && tab.active !== false) {
  1063. tab.active = true;
  1064. } else if (tab.active) {
  1065. ctrl.select(tab);
  1066. } else {
  1067. tab.active = false;
  1068. }
  1069. };
  1070. ctrl.removeTab = function removeTab(tab) {
  1071. var index = tabs.indexOf(tab);
  1072. //Select a new tab if the tab to be removed is selected and not destroyed
  1073. if (tab.active && tabs.length > 1 && !destroyed) {
  1074. //If this is the last tab, select the previous tab. else, the next tab.
  1075. var newActiveIndex = index == tabs.length - 1 ? index - 1 : index + 1;
  1076. ctrl.select(tabs[newActiveIndex]);
  1077. }
  1078. tabs.splice(index, 1);
  1079. };
  1080. var destroyed;
  1081. $scope.$on('$destroy', function() {
  1082. destroyed = true;
  1083. });
  1084. }])
  1085. /**
  1086. * @ngdoc directive
  1087. * @name ui.bootstrap.tabs.directive:tabset
  1088. * @restrict EA
  1089. *
  1090. * @description
  1091. * Tabset is the outer container for the tabs directive
  1092. *
  1093. * @param {boolean=} vertical Whether or not to use vertical styling for the tabs.
  1094. * @param {boolean=} justified Whether or not to use justified styling for the tabs.
  1095. *
  1096. * @example
  1097. <example module="ui.bootstrap">
  1098. <file name="index.html">
  1099. <tabset>
  1100. <tab heading="Tab 1"><b>First</b> Content!</tab>
  1101. <tab heading="Tab 2"><i>Second</i> Content!</tab>
  1102. </tabset>
  1103. <hr />
  1104. <tabset vertical="true">
  1105. <tab heading="Vertical Tab 1"><b>First</b> Vertical Content!</tab>
  1106. <tab heading="Vertical Tab 2"><i>Second</i> Vertical Content!</tab>
  1107. </tabset>
  1108. <tabset justified="true">
  1109. <tab heading="Justified Tab 1"><b>First</b> Justified Content!</tab>
  1110. <tab heading="Justified Tab 2"><i>Second</i> Justified Content!</tab>
  1111. </tabset>
  1112. </file>
  1113. </example>
  1114. */
  1115. .directive('tabset', function() {
  1116. return {
  1117. restrict: 'EA',
  1118. transclude: true,
  1119. replace: true,
  1120. scope: {
  1121. type: '@'
  1122. },
  1123. controller: 'TabsetController',
  1124. templateUrl: 'app/partials/bootstrap/tabset.html',
  1125. link: function(scope, element, attrs) {
  1126. scope.vertical = angular.isDefined(attrs.vertical) ? scope.$parent.$eval(attrs.vertical) : false;
  1127. scope.justified = angular.isDefined(attrs.justified) ? scope.$parent.$eval(attrs.justified) : false;
  1128. }
  1129. };
  1130. })
  1131. /**
  1132. * @ngdoc directive
  1133. * @name ui.bootstrap.tabs.directive:tab
  1134. * @restrict EA
  1135. *
  1136. * @param {string=} heading The visible heading, or title, of the tab. Set HTML headings with {@link ui.bootstrap.tabs.directive:tabHeading tabHeading}.
  1137. * @param {string=} select An expression to evaluate when the tab is selected.
  1138. * @param {boolean=} active A binding, telling whether or not this tab is selected.
  1139. * @param {boolean=} disabled A binding, telling whether or not this tab is disabled.
  1140. *
  1141. * @description
  1142. * Creates a tab with a heading and content. Must be placed within a {@link ui.bootstrap.tabs.directive:tabset tabset}.
  1143. *
  1144. * @example
  1145. <example module="ui.bootstrap">
  1146. <file name="index.html">
  1147. <div ng-controller="TabsDemoCtrl">
  1148. <button class="btn btn-small" ng-click="items[0].active = true">
  1149. Select item 1, using active binding
  1150. </button>
  1151. <button class="btn btn-small" ng-click="items[1].disabled = !items[1].disabled">
  1152. Enable/disable item 2, using disabled binding
  1153. </button>
  1154. <br />
  1155. <tabset>
  1156. <tab heading="Tab 1">First Tab</tab>
  1157. <tab select="alertMe()">
  1158. <tab-heading><i class="icon-bell"></i> Alert me!</tab-heading>
  1159. Second Tab, with alert callback and html heading!
  1160. </tab>
  1161. <tab ng-repeat="item in items"
  1162. heading="{{item.title}}"
  1163. disabled="item.disabled"
  1164. active="item.active">
  1165. {{item.content}}
  1166. </tab>
  1167. </tabset>
  1168. </div>
  1169. </file>
  1170. <file name="script.js">
  1171. function TabsDemoCtrl($scope) {
  1172. $scope.items = [
  1173. { title:"Dynamic Title 1", content:"Dynamic Item 0" },
  1174. { title:"Dynamic Title 2", content:"Dynamic Item 1", disabled: true }
  1175. ];
  1176. $scope.alertMe = function() {
  1177. setTimeout(function() {
  1178. alert("You've selected the alert tab!");
  1179. });
  1180. };
  1181. };
  1182. </file>
  1183. </example>
  1184. */
  1185. /**
  1186. * @ngdoc directive
  1187. * @name ui.bootstrap.tabs.directive:tabHeading
  1188. * @restrict EA
  1189. *
  1190. * @description
  1191. * Creates an HTML heading for a {@link ui.bootstrap.tabs.directive:tab tab}. Must be placed as a child of a tab element.
  1192. *
  1193. * @example
  1194. <example module="ui.bootstrap">
  1195. <file name="index.html">
  1196. <tabset>
  1197. <tab>
  1198. <tab-heading><b>HTML</b> in my titles?!</tab-heading>
  1199. And some content, too!
  1200. </tab>
  1201. <tab>
  1202. <tab-heading><i class="icon-heart"></i> Icon heading?!?</tab-heading>
  1203. That's right.
  1204. </tab>
  1205. </tabset>
  1206. </file>
  1207. </example>
  1208. */
  1209. .directive('tab', ['$parse', '$log', function($parse, $log) {
  1210. return {
  1211. require: '^tabset',
  1212. restrict: 'EA',
  1213. replace: true,
  1214. templateUrl: 'app/partials/bootstrap/tab.html',
  1215. transclude: true,
  1216. scope: {
  1217. active: '=?',
  1218. heading: '@',
  1219. onSelect: '&select', //This callback is called in contentHeadingTransclude
  1220. //once it inserts the tab's content into the dom
  1221. onDeselect: '&deselect'
  1222. },
  1223. controller: function() {
  1224. //Empty controller so other directives can require being 'under' a tab
  1225. },
  1226. link: function(scope, elm, attrs, tabsetCtrl, transclude) {
  1227. scope.$watch('active', function(active) {
  1228. if (active) {
  1229. tabsetCtrl.select(scope);
  1230. }
  1231. });
  1232. scope.disabled = false;
  1233. if (attrs.disable) {
  1234. scope.$parent.$watch($parse(attrs.disable), function(value) {
  1235. scope.disabled = !! value;
  1236. });
  1237. }
  1238. // Deprecation support of "disabled" parameter
  1239. // fix(tab): IE9 disabled attr renders grey text on enabled tab #2677
  1240. // This code is duplicated from the lines above to make it easy to remove once
  1241. // the feature has been completely deprecated
  1242. if (attrs.disabled) {
  1243. $log.warn('Use of "disabled" attribute has been deprecated, please use "disable"');
  1244. scope.$parent.$watch($parse(attrs.disabled), function(value) {
  1245. scope.disabled = !! value;
  1246. });
  1247. }
  1248. scope.select = function() {
  1249. if (!scope.disabled) {
  1250. scope.active = true;
  1251. }
  1252. };
  1253. tabsetCtrl.addTab(scope);
  1254. scope.$on('$destroy', function() {
  1255. tabsetCtrl.removeTab(scope);
  1256. });
  1257. //We need to transclude later, once the content container is ready.
  1258. //when this link happens, we're inside a tab heading.
  1259. scope.$transcludeFn = transclude;
  1260. }
  1261. };
  1262. }])
  1263. .directive('tabHeadingTransclude', function() {
  1264. return {
  1265. restrict: 'A',
  1266. require: '^tab',
  1267. link: function(scope, elm, attrs, tabCtrl) {
  1268. scope.$watch('headingElement', function updateHeadingElement(heading) {
  1269. if (heading) {
  1270. elm.html('');
  1271. elm.append(heading);
  1272. }
  1273. });
  1274. }
  1275. };
  1276. })
  1277. .directive('tabContentTransclude', function() {
  1278. return {
  1279. restrict: 'A',
  1280. require: '^tabset',
  1281. link: function(scope, elm, attrs) {
  1282. var tab = scope.$eval(attrs.tabContentTransclude);
  1283. //Now our tab is ready to be transcluded: both the tab heading area
  1284. //and the tab content area are loaded. Transclude 'em both.
  1285. tab.$transcludeFn(tab.$parent, function(contents) {
  1286. angular.forEach(contents, function(node) {
  1287. if (isTabHeading(node)) {
  1288. //Let tabHeadingTransclude know.
  1289. tab.headingElement = node;
  1290. } else {
  1291. elm.append(node);
  1292. }
  1293. });
  1294. });
  1295. }
  1296. };
  1297. function isTabHeading(node) {
  1298. return node.tagName && (
  1299. node.hasAttribute('tab-heading') ||
  1300. node.hasAttribute('data-tab-heading') ||
  1301. node.hasAttribute('x-tab-heading') ||
  1302. node.tagName.toLowerCase() === 'tab-heading' ||
  1303. node.tagName.toLowerCase() === 'data-tab-heading' ||
  1304. node.tagName.toLowerCase() === 'x-tab-heading'
  1305. );
  1306. }
  1307. });