diff --git a/.gitignore b/.gitignore index 2b85d59..acccc72 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .DS_Store .project +.packages .idea .children *.dart.js @@ -8,8 +9,11 @@ *.iml packages packages/ -pubspec.lock .buildlog content_shell*.zip drt-* test.log +/.settings/ +node_modules +.pub +build \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..4df7637 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: node_js +node_js: + - "0.11" +before_install: + - export DISPLAY=:99.0 +script: + - ./scripts/travis/run_tests.sh \ No newline at end of file diff --git a/AUTHORS.md b/AUTHORS.md new file mode 100644 index 0000000..1c7675d --- /dev/null +++ b/AUTHORS.md @@ -0,0 +1,10 @@ +Contributors in the order of first contribution + +* [Sergey Akopkokyants](https://github.com/akserg) +* [Tõnis Pool](https://github.com/poolik) +* [Günter Zöchbauer](https://github.com/zoechi) +* [Francesco Cina](https://github.com/ufoscout) +* [Neeraj Mittal](https://github.com/neermitt) +* [Robert Schütte](https://github.com/Roba1993) +* [Jonathan Hughes](https://github.com/jonathanhughes) +* [Martynas Kazlauskas](martynas@firmfirm.co) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6306cbc..66d1321 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,23 +1,147 @@ -# Version 0.2.0, In Progress... +# Version 0.6.13 (2016/26/09) + +## Bug Fixes + +- Accordion collapsed after initialization with delay [#154](https://github.com/akserg/angular.dart.ui/issues/154) +- Typeahead highlight only works with the exact match need it to show highlight based on the words [#171](https://github.com/akserg/angular.dart.ui/issues/171) + +# Version 0.6.12 (2015/12/04) + +## Bug Fixes + +- Unit tests fail with issue: The null object does not have a method '[]' +- Added null check when backdrop is double clicked. + + +# Version 0.6.10-11 (2015/10/08) + +## Bug Fixes + +- Carousel transition canceled + +# Version 0.6.9, (2015/09/24) + +## Bug Fixes + +- Work around to tab heading issue. + +# Version 0.6.8, (2015/03/01) + +- Updated Copyright info. +- Improved documentation for Carousel component and general README file + +# Version 0.6.7, (2015/02/27) + +## Bug Fixes + +- Accordion: double click on title causes inconsistent state or bad rendering +- Accordion Collapse +- Accordion doesn't expand after creation if is-open equals true +- Accordion: fast clicks on title causes inconsistent state or bad rendering +- Combine slide and angular css files + +# Version 0.6.0, (2014/12/12) + +- Migration on Angular Dart 1.0, support Bootstrap 3.3.1 and bug fixes. + +# Version 0.5.5, (2014/06/25) + +## Bug Fixes + +- Modal Dialog with backdrop:'static' cannot be closed by click on button with data-dismiss='modal' + +# Version 0.5.4, (2014/06/21) + +## Bug Fixes + +- Error when using typeahead-on-select=ctrl.setSelectedItem($item, $model, $label) + +# Version 0.5.3, (2014/06/18) + +## Bug Fixes + +- Typeahead placement of suggestion popover offset by 200px +- Fix animation toggle and z-index calculation in ModalWindow + +# Version 0.5.2, (2014/06/17) ## Features -- **alert** -- **Collapse:** - - created ([7e8af32](https://github.com/akserg/angular.dart.ui/commit/7e8af32)) -- **DropdownToggle:** - - merged pull request ([b3753d3](https://github.com/akserg/angular.dart.ui/commit/b3753d3)) -- **alert** - - created ([6ab5094](https://github.com/akserg/angular.dart.ui/commit/6ab5094)) +- Project migrated to follow Dart SDK 1.4.3 + +## Bug Fixes + +- ModalWindow must call dismiss method of top ModalInstance instead of Modal.close. + +# Version 0.5.1, (2014/06/06) + +## Features + +- Project migrated to follow Angular Dart 0.12.0 + +# Version 0.5.0, (2014/06/05) + +## Features + +- All components have migrated away from Shadow DOM and applyAuthorStyles. + +## Bug Fixes + +- ng-click called multiple times +- DatePicker tests fail with UTC+1 + +# Version 0.4.0, (2014/05/16) + +## Features + +- Carousel +- Timepicker +- Pagination +- Tooltip +- Popover +- Typehead + +## Bug Fixes + +- Classes Popover, Tooltip, ModalWindow, DatePicker compiled to JavaScript don't work proper +- Error compiling Pagination component to JavaScript +- Checkbox component doesn't work proper in example + +# Version 0.3.0, (2014/04/19) + +## Features + +- Accordion +- Datepicker (partially implemented) +- Rating +- Tabs +- Drag and Drop support + +## Bug Fixes + +- Fixed selectors of all components +- Fixed unittest for all components + +# Version 0.2.0, (2014/02/13) + +## Features + +- Collapse +- DropdownToggle +- Alert +- Progressbar +- Modal +- Timeout + +## Bug Fixes + +- Transition and Collapse are not working as expected # Version 0.1.0, (2014/01/14) ## Features -- **Buttons:** - - created ([bcedb32](https://github.com/akserg/angular.dart.ui/commit/bcedb32)) -- **Position:** - - created ([bcedb32](https://github.com/akserg/angular.dart.ui/commit/bcedb32)) -- **Transition:** - - created ([3894ad9](https://github.com/akserg/angular.dart.ui/commit/3894ad9)) +- Buttons +- Position +- Transition diff --git a/LICENSE.md b/LICENSE.md index fb65421..10e365a 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright (C) 2013 - 2014 Sergey Akopkokhyants. All rights reserved. +Copyright (C) 2013 - 2016 Angular Dart UI authors. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 5e17e7a..d5fe880 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,70 @@ -angular.dart.ui [![Build Status](https://drone.io/github.com/akserg/angular.dart.ui/status.png)](https://drone.io/github.com/akserg/angular.dart.ui/latest) +Angular UI [![Build Status](https://travis-ci.org/akserg/angular.dart.ui.svg?branch=master)](https://travis-ci.org/akserg/angular.dart.ui) [![Stories in Ready](https://badge.waffle.io/akserg/angular.dart.ui.svg?label=ready)](http://waffle.io/akserg/angular.dart.ui?milestone=0.6) [![Coverage Status](https://coveralls.io/repos/akserg/angular.dart.ui/badge.svg)](https://coveralls.io/r/akserg/angular.dart.ui) =============== Port of Angular-UI to Dart. -###Bootstrap directives and components +Look at [Demo](http://akserg.github.io/angular.dart.ui.demo/index.html) page for this project. -- **Checkbox and RadioButton** -- **DropdownToggle** -- **Collapse** -- **Alert** +You may be interesting in check out [Material Design Theme](http://akserg.github.io/angular.dart.material.demo) for this project. + +## Quick-Start +Include the following code to your `index.html` +```html + + + + + + + + +``` + +Add the angular-ui module in your `main.dart` +```dart +import 'package:angular/angular.dart'; +import 'package:angular/application_factory.dart'; +import 'package:angular_ui/angular_ui.dart'; + +void main() { + applicationFactory() + .addModule(new AngularUIModule()) // The angular-ui module + .addModule(new MainModule()) // Your own module + .run(); +} +``` + +Use the angular-ui components as described below or in the [demo](http://akserg.github.io/angular.dart.ui.demo/index.html). + + +##Bootstrap directives and components + +- Checkbox and RadioButton +- DropdownToggle +- Collapse +- Alert +- ProgressBar +- Modal +- Accordion +- Rating +- Datepicker (partially implemented) +- Tabs +- Drag and Drop support +- Carousel +- Timepicker +- Pagination +- Tooltip +- Popover +- Typehead + +*Note: Drag and Drop support is experimental feature and API can be changed at any time in the future.* ##Credits -Big thanks for yours valuable input: -- **T�nis Pool** (https://github.com/poolik). -- **AngularDart project** (https://github.com/angular/angular.dart) \ No newline at end of file + +- [Sergey Akopkokhyants](https://github.com/akserg) +- [Tõnis Pool](https://github.com/poolik). +- [Günter Zöchbauer](https://github.com/zoechi) +- [Francesco Cina](https://github.com/ufoscout) +- [AngularDart project](https://github.com/angular/angular.dart) +- [Neeraj Mittal](https://github.com/neermitt) +- [Robert Schütte](https://github.com/Roba1993) \ No newline at end of file diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 0000000..c21bab4 --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,27 @@ +module.exports = function(config) { + config.set({ + basePath: '.', + frameworks: ['dart-unittest'], + + files: [ + {pattern: 'test/angular_ui_tests.dart', included: true}, + {pattern: '**/*.dart', included: false}, + {pattern: '**/*.html', included: false} + ], + + exclude: [ + ], + + autoWatch: true, + captureTimeout: 60000, + browserNoActivityTimeout: 300000, + + + plugins: [ + 'karma-dart', + 'karma-chrome-launcher' + ], + + browsers: ['Dartium'] + }); +}; diff --git a/lib/.settings/org.eclipse.core.resources.prefs b/lib/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..99f26c0 --- /dev/null +++ b/lib/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/lib/README.md b/lib/README.md new file mode 100644 index 0000000..b4d552e --- /dev/null +++ b/lib/README.md @@ -0,0 +1 @@ +**AngularUI** module. \ No newline at end of file diff --git a/lib/accordion/README.md b/lib/accordion/README.md new file mode 100644 index 0000000..e861814 --- /dev/null +++ b/lib/accordion/README.md @@ -0,0 +1,3 @@ +The **accordion directive** builds to provide a list of items, with collapsible bodies that are collapsed or expanded by clicking on the item's header. +We can control whether expanding an item will cause the other items to close, using the `close-others` attribute on accordion. +The body of each accordion group is transcluded in to the body of the collapsible element. \ No newline at end of file diff --git a/lib/accordion/accordion.dart b/lib/accordion/accordion.dart new file mode 100644 index 0000000..7f2cdb5 --- /dev/null +++ b/lib/accordion/accordion.dart @@ -0,0 +1,86 @@ +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +library angular.ui.accordion; + +import 'dart:html' as dom; +import "package:angular/angular.dart"; +import 'package:angular/utils.dart' as utils; +import 'package:logging/logging.dart' show Logger; +import 'package:angular_ui/utils/dbl_click_preventer.dart'; +import 'package:angular_ui/utils/timeout.dart'; + +part 'accordion_group.dart'; + +final _log = new Logger('angular.ui.accordion'); + +class AccordionModule extends Module { + AccordionModule() { + install(new DblClickPreventerModule()); + bind(AccordionComponent); + bind(AccordionHeadingComponent); + bind(AccordionGroupComponent); + bind(AccordionTransclude); + bind(AccordionConfig, toValue:new AccordionConfig()); + } +} + +@Injectable() +class AccordionConfig { + bool closeOthers = true; +} + +@Component( + selector: 'accordion', + //templateUrl: 'packages/angular_ui/accordion/accordion.html', + template: '
', + useShadowDom: false +) +//@Component( +// selector: '[accordion]', +// templateUrl: 'packages/angular_ui/accordion/accordion.html', +// useShadowDom: false +//) +class AccordionComponent implements ScopeAware { + + @NgTwoWay('close-others') + bool isCloseOthers; + + Scope scope; + final AccordionConfig _config; + + /* + * This array keeps track of the accordion groups + */ + List groups = []; + + AccordionComponent(this._config); + + /* + * Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to + */ + void closeOthers(AccordionGroupComponent openGroup) { + isCloseOthers = isCloseOthers != null ? isCloseOthers : _config.closeOthers; + if (isCloseOthers) { + groups.forEach((AccordionGroupComponent e) { + if(e != openGroup) { + e.isOpen = false; + } + }); + } + } + + /* + * This is called from the accordion-group directive to add itself to the accordion + */ + void addGroup(AccordionGroupComponent groupScope) { + groups.add(groupScope); + } + + /* + * This is called from the accordion-group directive when to remove itself + */ + void removeGroup(AccordionGroupComponent groupScope) { + groups.remove(groupScope); + } +} diff --git a/lib/accordion/accordion.html b/lib/accordion/accordion.html new file mode 100644 index 0000000..93287ec --- /dev/null +++ b/lib/accordion/accordion.html @@ -0,0 +1,7 @@ + + +
\ No newline at end of file diff --git a/lib/accordion/accordion_group.dart b/lib/accordion/accordion_group.dart new file mode 100644 index 0000000..e41ef99 --- /dev/null +++ b/lib/accordion/accordion_group.dart @@ -0,0 +1,118 @@ +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +part of angular.ui.accordion; + +@Component( + selector: 'accordion-group', +// templateUrl: 'packages/angular_ui/accordion/accordion_group.html', + template: r''' +
+
+

+ {{heading}} +

+
+
+
+
+
+''', + useShadowDom: false +) +class AccordionGroupComponent implements DetachAware, ScopeAware { + + @NgAttr('heading') + var heading; + + final AccordionComponent accordion; + final DblClickPreventer dblClickPreventer; + final Timeout timeout; + + AccordionGroupComponent(this.accordion, this.dblClickPreventer, this.timeout) { + accordion.addGroup(this); + } + + void set scope(Scope scope) { + // + timeout(() { + scope.watch('isOpen', (value, old){ + collapse = !value; + }); + }, delay:50); + } + + bool _isOpen = false; + @NgTwoWay('is-open') + set isOpen(value) { + _isOpen = utils.toBool(value); + if (_isOpen) { + accordion.closeOthers(this); + } + } + get isOpen => _isOpen; + + bool collapse = true; + + bool _isDisabled = false; + @NgTwoWay('is-disabled') + get isDisabled => _isDisabled; + set isDisabled(var newValue) { + _isDisabled = utils.toBool(newValue); + } + + toggleOpen() { + dblClickPreventer(() { + if ( !isDisabled ) { + isOpen = !isOpen; + } + }); + } + + @override + void detach() { + this.accordion.removeGroup(this); + } +} + +/* + * Use accordion-heading below an accordion-group to provide a heading containing HTML + * + * Heading containing HTML - + * + */ +@Decorator(selector: 'accordion-heading') +class AccordionHeadingComponent { + AccordionHeadingComponent(dom.Element elem, AccordionGroupComponent acc) { + elem.remove(); + acc.heading = elem; + } +} + +/** + * This decorator update heading depends on + * presence of [AccordionHeadingComponent] in [AccordionGroupComponent] + */ +@Decorator(selector: '[accordion-transclude]') +class AccordionTransclude implements ScopeAware { + dom.Element elem; + + AccordionTransclude(this.elem); + + void set scope(Scope scope) { + scope.watch("heading", (value, previousValue) { + if (value != null && value is dom.Element && value.tagName == 'ACCORDION-HEADING') { + // We adding text belogns to 'accordion-heading' element to span element + dom.SpanElement span = elem.querySelector('span') as dom.SpanElement; + if (span != null) { + span.children.clear(); + span.appendHtml(value.innerHtml.trim()); + // Other elements like icons must move separatelly into the element itself + while (value.children.length > 0) { + elem.append(value.children[0]); + } + } + } + }); + } +} \ No newline at end of file diff --git a/lib/accordion/accordion_group.html b/lib/accordion/accordion_group.html new file mode 100644 index 0000000..86a411b --- /dev/null +++ b/lib/accordion/accordion_group.html @@ -0,0 +1,16 @@ + + +
+
+

+ {{heading}} +

+
+
+
+
+
diff --git a/lib/alert.dart b/lib/alert.dart deleted file mode 100644 index d49fe01..0000000 --- a/lib/alert.dart +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2013 - 2014, akserg (Sergey Akopkokhyants) -// https://github.com/akserg/angular.dart.ui -// All rights reserved. Please see the LICENSE.md file. -library angular.ui.alert; - -import "package:angular/angular.dart"; - -/** - * Alert Module. - */ -class AlertModule extends Module { - AlertModule() { - type(Alert); - } -} - -/** - * Alert component. - */ -@NgComponent(selector: 'alert', publishAs: 't', applyAuthorStyles: true, - templateUrl: 'packages/angular_ui/alert.html') -class Alert { - @NgOneWay('type') - String type; - - @NgCallback('close') - var close; - - /** - * Flag helps show or hide close button depends on availability of [close] - * attribute. - */ - bool get showable => (close as BoundExpression).expression.isChain; - - /** - * Thst method calls [close] callback - */ - void closeHandler() { - close(); - } -} \ No newline at end of file diff --git a/lib/alert.html b/lib/alert.html deleted file mode 100644 index 8abc24f..0000000 --- a/lib/alert.html +++ /dev/null @@ -1,10 +0,0 @@ - - -
- - -
\ No newline at end of file diff --git a/lib/alert/README.md b/lib/alert/README.md new file mode 100644 index 0000000..e25e11f --- /dev/null +++ b/lib/alert/README.md @@ -0,0 +1,3 @@ +**Alert** is an Angular Dart version of bootstrap's alert. +This directive can be used to generate alerts from the dynamic model data (using the ng-repeat directive); +The presence of the "close" attribute determines if a close button is displayed. diff --git a/lib/alert/alert.dart b/lib/alert/alert.dart new file mode 100644 index 0000000..fab435c --- /dev/null +++ b/lib/alert/alert.dart @@ -0,0 +1,58 @@ +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +library angular.ui.alert; + +import "package:angular/angular.dart"; +import "package:angular/core_dom/module_internal.dart"; + +/** + * Alert Module. + */ +class AlertModule extends Module { + AlertModule() { + bind(Alert); + } +} + +/** + * Alert component. + */ +@Component(selector: 'alert', + useShadowDom: false, +// templateUrl: 'packages/angular_ui/alert/alert.html' + template: ''' +
+ + +
''' +) +//@Component(selector: '[alert]', +// useShadowDom: false, +// templateUrl: 'packages/angular_ui/alert/alert.html') +class Alert implements ScopeAware { + @NgOneWay('type') + String type; + + @NgCallback('close') + var close; + + Scope scope; + + /** + * Flag helps show or hide close button depends on availability of [close] + * attribute. + */ + var _closeable = false; + bool get closeable => _closeable; + + /** + * Calculate and return alert type as string depnds on type. If type is null + * methods returns 'warning' as default value. + */ + String get alertTypeAsString => "alert-${type != null ? type : 'warning'}"; + + Alert(NodeAttrs attr) { + _closeable = attr.containsKey('close'); + } +} \ No newline at end of file diff --git a/lib/alert/alert.html b/lib/alert/alert.html new file mode 100644 index 0000000..83d6ed7 --- /dev/null +++ b/lib/alert/alert.html @@ -0,0 +1,10 @@ + + +
+ + +
\ No newline at end of file diff --git a/lib/angular_ui.dart b/lib/angular_ui.dart index 91a0c9c..4716a97 100644 --- a/lib/angular_ui.dart +++ b/lib/angular_ui.dart @@ -1,26 +1,60 @@ -// Copyright (c) 2013 - 2014, akserg (Sergey Akopkokhyants) +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. // https://github.com/akserg/angular.dart.ui // All rights reserved. Please see the LICENSE.md file. library angular.ui; import "package:angular/angular.dart"; -import 'package:angular_ui/buttons.dart'; -import 'package:angular_ui/collapse.dart'; -import 'package:angular_ui/dropdown_toggle.dart'; -import 'package:angular_ui/alert.dart'; -import 'package:angular_ui/carousel.dart'; +import 'package:angular_ui/alert/alert.dart'; +import 'package:angular_ui/accordion/accordion.dart'; +import 'package:angular_ui/buttons/buttons.dart'; +import 'package:angular_ui/carousel/carousel.dart'; +import 'package:angular_ui/collapse/collapse.dart'; +import 'package:angular_ui/dropdown/dropdown_toggle.dart'; +import 'package:angular_ui/pagination/pagination.dart'; +import 'package:angular_ui/progressbar/progressbar.dart'; +import 'package:angular_ui/rating/rating.dart'; +import 'package:angular_ui/tabs/tabset.dart'; +import 'package:angular_ui/utils/content_append.dart'; +import 'package:angular_ui/utils/timeout.dart'; +import 'package:angular_ui/utils/transition.dart'; +import 'package:angular_ui/modal/modal.dart'; +import 'package:angular_ui/dragdrop/dragdrop.dart'; +import 'package:angular_ui/datepicker/datepicker.dart'; +import 'package:angular_ui/timepicker/timepicker.dart'; +import 'package:angular_ui/tooltip/tooltip.dart'; +import 'package:angular_ui/popover/popover.dart'; +import 'package:angular_ui/typeahead/module.dart'; +import 'package:angular_ui/utils/dbl_click_preventer.dart'; + +export 'package:angular_ui/modal/modal.dart'; /** * AngularUI Module */ class AngularUIModule extends Module { AngularUIModule() { + install(new AlertModule()); + install(new AccordionModule()); install(new ButtonModule()); + install(new CarouselModule()); install(new CollapseModule()); + install(new DragDropModule()); install(new DropdownToggleModule()); - install(new AlertModule()); - install(new CarouselModule()); + install(new PaginationModule()); + install(new ProgressbarModule()); + install(new RatingModule()); + install(new TabsModule()); + install(new TimeoutModule()); + install(new TransitionModule()); + install(new ModalModule()); + install(new DatepickerModule()); + install(new TimepickerModule()); + install(new TooltipModule()); + install(new PopoverModule()); + install(new TypeaheadModule()); + install(new ContentAppendModule()); + install(new DblClickPreventerModule()); } } diff --git a/lib/buttons/README.md b/lib/buttons/README.md new file mode 100644 index 0000000..73ef600 --- /dev/null +++ b/lib/buttons/README.md @@ -0,0 +1,2 @@ +There are 2 directives that can make a group of buttons to behave like a set of checkboxes or radio buttons. + diff --git a/lib/buttons.dart b/lib/buttons/buttons.dart similarity index 69% rename from lib/buttons.dart rename to lib/buttons/buttons.dart index 4fb20dd..b183567 100644 --- a/lib/buttons.dart +++ b/lib/buttons/buttons.dart @@ -1,4 +1,4 @@ -// Copyright (c) 2013 - 2014, akserg (Sergey Akopkokhyants) +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. // https://github.com/akserg/angular.dart.ui // All rights reserved. Please see the LICENSE.md file. library angular.ui.buttons; @@ -11,9 +11,9 @@ import "package:angular/angular.dart"; */ class ButtonModule extends Module { ButtonModule() { - value(ButtonConfig, new ButtonConfig(activeClass:'active', toggleEvent: 'click')); - type(BtnRadio); - type(BtnCheckbox); + bind(ButtonConfig, toValue:new ButtonConfig(activeClass:'active', toggleEvent: 'click')); + bind(BtnRadio); + bind(BtnCheckbox); } } @@ -24,13 +24,13 @@ class ButtonConfig { String activeClass; String toggleEvent; - ButtonConfig({this.activeClass, this.toggleEvent}); + ButtonConfig({this.activeClass:'active', this.toggleEvent:'click'}); } /** * Radio button directive. */ -@NgDirective(selector:'[btn-radio]') +@Decorator(selector:'[btn-radio]') class BtnRadio { @NgAttr("btn-radio") @@ -45,13 +45,15 @@ class BtnRadio { BtnRadio(this.element, this.ngModel, this.config, this.scope) { // model -> UI ngModel.render = (value) { - element.classes.toggle(config.activeClass, ngModel.modelValue == scope.$eval(btnRadioAttr)); + element.classes.toggle(config.activeClass, ngModel.modelValue == scope.eval(btnRadioAttr)); }; // ui -> model element.on[config.toggleEvent].listen((dom.Event event) { if (!element.classes.contains(config.activeClass)) { - ngModel.viewValue = scope.$eval(btnRadioAttr); + ngModel.markAsTouched(); + ngModel.viewValue = scope.eval(btnRadioAttr); + ngModel.render(ngModel.modelValue); } }); } @@ -60,7 +62,7 @@ class BtnRadio { /** * Checkbox button directive */ -@NgDirective(selector:'[btn-checkbox]') +@Decorator(selector:'[btn-checkbox]') class BtnCheckbox { ButtonConfig config; NgModel ngModel; @@ -79,7 +81,7 @@ class BtnCheckbox { dynamic get falseValue => getCheckboxValue(btnCheckboxFalse, false); dynamic getCheckboxValue(attributeValue, defaultValue) { - var val = scope.$eval(attributeValue); + var val = scope.eval(attributeValue); return val != null ? val : defaultValue; } @@ -91,7 +93,14 @@ class BtnCheckbox { // ui -> model element.on[config.toggleEvent].listen((dom.Event event) { + // We need remove focus out of the element because it doesn't change the state + element.blur(); + ngModel.markAsTouched(); ngModel.viewValue = element.classes.contains(config.activeClass) ? falseValue : trueValue; + ngModel.render(ngModel.modelValue); + }); + element.onBlur.listen((e) { + ngModel.markAsTouched(); }); } } \ No newline at end of file diff --git a/lib/carousel.dart b/lib/carousel.dart deleted file mode 100644 index 87b3b7a..0000000 --- a/lib/carousel.dart +++ /dev/null @@ -1,286 +0,0 @@ -// Copyright (c) 2013 - 2014, akserg (Sergey Akopkokhyants) -// https://github.com/akserg/angular.dart.ui -// All rights reserved. Please see the LICENSE.md file. -library angular.ui.carousel; - -import 'dart:html' as dom; -import 'dart:async' as async; -import "package:angular/angular.dart"; -import 'transition.dart'; -import 'timeout.dart'; - -/** - * Carousel Module. - */ -class CarouselModule extends Module { - CarouselModule() { - install(new TransitionModule()); - type(CarouselController); - type(Carousel); - type(Slide); - } -} - -class CarouselController { - List slides = []; - var currentIndex = -1; - var currentTimeout; - var isPlaying = false; - Slide currentSlide; - - var destroyed = false; - - Scope scope; - Transition transition; - Timeout timeout; - - CarouselController(this.scope, this.transition, this.timeout) { - scope.$on('destroy', () { - destroyed = true; - }); - - scope.next = () { - var newIndex = (currentIndex + 1) % slides.length; - - //Prevent this user-triggered transition from occurring if there is already one in progress - if (scope.currentTransition == null) { - return select(slides[newIndex], direction:'next'); - } - }; - - scope.prev = () { - var newIndex = currentIndex - 1 < 0 ? slides.length - 1 : currentIndex - 1; - - //Prevent this user-triggered transition from occurring if there is already one in progress - if (scope.currentTransition == null) { - return select(slides[newIndex], direction:'prev'); - } - }; - - scope.select = (slide) { - select(slide); - }; - - scope.isActive = (slide) { - return currentSlide == slide; - }; - - scope.slides = () { - return slides; - }; - - scope.$watch('interval', restartTimer); - scope.$on('destroy', resetTimer); - - scope.play = () { - if (!isPlaying) { - isPlaying = true; - restartTimer(); - } - }; - - scope.pause = () { - if (!scope.noPause) { - isPlaying = false; - resetTimer(); - } - }; - } - - void select(Slide nextSlide, {String direction:null}) { - var nextIndex = slides.indexOf(nextSlide); - // Decide direction if it's not given - if (direction == null) { - direction = nextIndex > currentIndex ? 'next' : 'prev'; - } - if (nextSlide != null && nextSlide != currentSlide) { - var goNext = () { - // Scope has been destroyed, stop here. - if (destroyed) { return; } - // - var transitionDone = (Slide next, Slide current) { - next.direction = ''; - next.entering = false; - next.leaving = false; - next.active = true; - // - if (current != null) { - current.direction = ''; - current.entering = false; - current.leaving = false; - current.active = false; - } - scope.currentTransition = null; - }; - // If we have a slide to transition from and we have a transition type and we're allowed, go - if (currentSlide != null && !scope.noTransition && nextSlide.element != null) { - // We shouldn't do class manip in here, but it's the same weird thing bootstrap does. need to fix sometime - nextSlide.element.classes.add(direction); - var reflow = nextSlide.element.offsetWidth; //force reflow - - //Set all other slides to stop doing their stuff for the new transition - slides.forEach((slide) { - //angular.extend(slide, {direction: '', entering: false, leaving: false, active: false}); - slide.direction = ''; - slide.entering = false; - slide.leaving = false; - slide.active = false; - }); - - nextSlide.direction = direction; - nextSlide.entering = true; - nextSlide.active = true; - - if (currentSlide != null) { - currentSlide.direction = direction; - currentSlide.leaving = true; - } - - scope.currentTransition = transition(nextSlide.element, {}); - - // We have to create new pointers inside a closure since next & current will change - var closure = (next, current) { - (scope.currentTransition as async.Completer).future.then((onValue) { - transitionDone(next, current); - }, onError:(e) { - transitionDone(next, current); - }); - }; - // - closure(nextSlide, currentSlide); - } else { - transitionDone(nextSlide, currentSlide); - } - currentSlide = nextSlide; - currentIndex = nextIndex; - //every time you change slides, reset the timer - restartTimer(); - }; - - if (scope.currentTransition != null) { - (scope.currentTransition as async.Completer).completeError('Transition cancelled'); - //Timeout so ng-class in template has time to fix classes for finished slide - timeout(goNext); - } else { - goNext(); - } - } - } - - /* Allow outside people to call indexOf on slides array */ - int indexOfSlide (slide) { - return slides.indexOf(slide); - } - - void restartTimer() { - resetTimer(); - var interval = scope.interval; - if (interval != null && interval >= 0) { - currentTimeout = timeout(timerFn, delay:interval); - } - } - - void resetTimer() { - if (currentTimeout != null) { - timeout.cancel(currentTimeout); - currentTimeout = null; - } - } - - void timerFn() { - if (isPlaying) { - scope.next(); - restartTimer(); - } else { - scope.pause(); - } - } - - void addSlide(slide, element) { - slide.element = element; - slides.add(slide); - //if this is the first slide or the slide is set to active, select it - if(slides.length == 1 || slide.active) { - select(slides[slides.length-1]); - if (slides.length == 1) { - scope.play(); - } - } else { - slide.active = false; - } - } - - void removeSlide(slide) { - //get the index of the slide inside the carousel - var index = slides.indexOf(slide); - slides.removeAt(index); - if (slides.length > 0 && slide.active) { - if (index >= slides.length) { - select(slides[index-1]); - } else { - select(slides[index]); - } - } else if (currentIndex > index) { - currentIndex--; - } - } -} - -/** - * Carousel component. - */ -@NgComponent(selector: 'carousel', publishAs: 'c', applyAuthorStyles: true, - template: ''' -
-
    1\'> -
  1. -
-
- 1\'> - 1\'> -
''') -class Carousel { - -} - -/** - * Slide component. - * Creates a slide inside a [Carousel] component. - * Must be placed as a child of a Carousel element. - */ -@NgComponent(selector: 'slide', publishAs: 's', applyAuthorStyles: true, - template:''' -
-''') -class Slide { - @NgTwoWay('active') - bool active = false; - - var direction = ''; - var entering = false; - var leaving = false; - - var scope; - dom.Element element; - var carouselCtrl; - - Slide(Scope this.scope, dom.Element this.element, this.carouselCtrl) { - carouselCtrl.addSlide(scope, element); - //when the scope is destroyed then remove the slide from the current slides array - scope.$on('destroy', () { - carouselCtrl.removeSlide(scope); - }); - - scope.$watch('active', (active) { - if (active) { - carouselCtrl.select(scope); - } - }); - } -} \ No newline at end of file diff --git a/lib/carousel/README.md b/lib/carousel/README.md new file mode 100644 index 0000000..100e1e3 --- /dev/null +++ b/lib/carousel/README.md @@ -0,0 +1,55 @@ +**Carousel** creates a carousel similar to bootstrap's image carousel. +Use a `` element with `` elements inside it. It will automatically cycle through the slides at a given rate, and a current-index variable will be kept in sync with the currently visible slide. + +## Demo +Use the `` element in your html +```html +
+ + + + + + +
+``` + +Generate the slide content in dart +```dart +@Component( + selector: 'carousel-demo', + templateUrl: 'your_html.html', + useShadowDom: false) +class CarouselDemo { + List> slides = []; + + /// Generate 4 start slides + CarouselDemo() { + for (int i = 0; i < 4; i++) { + addSlide(); + } + } + + /// Function to generate random cat picture slides + void addSlide() { + int newWidth = 600 + slides.length; + slides.add({ + 'image': 'http://placekitten.com/g/${newWidth}/300', + 'text': ['More','Extra','Lots of','Surplus'][slides.length % 4] + ' ' + + ['Cats', 'Kittys', 'Felines', 'Cutes'][slides.length % 4] + }); + } +} +``` + +Add full URL address to `your_html.html` into the list of angular trasformers in `pubspec.yaml` file: + +``` +transformers: +- angular: + html_files: + - .../your_html.html +``` \ No newline at end of file diff --git a/lib/carousel/carousel.dart b/lib/carousel/carousel.dart new file mode 100644 index 0000000..1d13c21 --- /dev/null +++ b/lib/carousel/carousel.dart @@ -0,0 +1,352 @@ +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +library angular.ui.carousel; + +import 'dart:html' as dom; + +import 'dart:async' as async; +import 'package:angular/angular.dart'; + +import 'package:angular_ui/utils/transition.dart'; +import 'package:angular_ui/utils/timeout.dart'; + +import 'package:logging/logging.dart' show Logger; +final _log = new Logger('angular.ui.carousel'); + +/** + * Carousel Module. + */ +class CarouselModule extends Module { + CarouselModule() { + install(new TransitionModule()); + bind(Carousel); + bind(Slide); + } +} + + +/** + * Carousel component. + */ +@Component( + selector: 'carousel', + useShadowDom: false, + visibility: Directive.CHILDREN_VISIBILITY, + template: ''' +''' + //templateUrl: 'packages/angular_ui/carousel/carousel.html' +) +//@Component( +// selector: '[carousel]', +// publishAs: 'c', +// useShadowDom: false, +// visibility: Directive.CHILDREN_VISIBILITY, +// templateUrl: 'packages/angular_ui/carousel/carousel.html') +class Carousel implements DetachAware, ScopeAware { + + @NgOneWay('no-transition') + bool noTransition = false; + + int _interval; + @NgOneWay('interval') + set interval(int interval) { + _interval = interval; + restartTimer(); + } + + @NgOneWay('no-pause') + bool noPause = false; + + List slides = []; + int _currentIndex = -1; + async.Completer _currentTimeout; + bool _isPlaying = false; + Slide _currentSlide; + + bool _destroyed = false; + + Transition _transition; + async.Completer _currentTransition; + Timeout _timeout; + Scope scope; + + Carousel(this._transition, this._timeout) { + _log.fine('CarouselComponent'); + } + + void next() { + if (slides.length > 0) { + var newIndex = (_currentIndex + 1) % slides.length; + //Prevent this user-triggered transition from occurring if there is already one in progress + if (_currentTransition == null) { + select(slides[newIndex], direction:'next'); + } + } + } + + void prev() { + if (slides.length > 0) { + var newIndex = _currentIndex - 1 < 0 ? slides.length - 1 : _currentIndex - 1; + + //Prevent this user-triggered transition from occurring if there is already one in progress + if (_currentTransition == null) { + select(slides[newIndex], direction:'prev'); + } + } + } + + bool isActive(Slide slide) { + return _currentSlide == slide; + } + + void play() { + if (!_isPlaying) { + _isPlaying = true; + restartTimer(); + } + } + + void pause() { + if (!noPause) { + _isPlaying = false; + resetTimer(); + } + } + + void select(Slide nextSlide, {String direction: null}) { + if (slides.length > 0) { + int nextIndex = slides.indexOf(nextSlide); + // Decide direction if it's not given + if (direction == null) { + direction = nextIndex > _currentIndex ? 'next' : 'prev'; + } + if (nextSlide != null && nextSlide != _currentSlide) { + if (_currentTransition != null && !_currentTransition.isCompleted) { + _currentTransition.completeError('Transition cancelled'); + //Timeout so ng-class in template has time to fix classes for finished slide + _timeout(() => _goNext(direction, nextIndex)); + } else { + _goNext(direction, nextIndex); + } + } + } + } + + void _goNext(String direction, int nextIndex) { + // Scope has been destroyed, stop here. + if (_destroyed) { + return; + } + if (nextIndex >= slides.length) { + // Something goes wrong - stop transition. + return; + } + Slide nextSlide = slides[nextIndex]; + // If we have a slide to transition from and we have a transition type and we're allowed, go + if (_currentSlide != null && direction != null && direction.isNotEmpty && !noTransition && nextSlide.element != null) { + // We shouldn't do class manip in here, but it's the same weird thing bootstrap does. need to fix sometime + nextSlide.element.classes.add(direction); + + var reflow = nextSlide.element.children[0].offsetWidth; //force reflow + + //Set all other slides to stop doing their stuff for the new transition + slides.forEach((Slide slide) { + slide.direction = ''; + slide.entering = false; + slide.leaving = false; + slide.active = false; + slide.next = false; + }); + + nextSlide.direction = direction; + nextSlide.entering = true; + nextSlide._active = true; + nextSlide.next = true; + + if (_currentSlide != null) { + _currentSlide.direction = direction; + _currentSlide.leaving = true; + } + + _currentTransition = _transition(nextSlide.element, {}); + + // We have to create new pointers inside a closure since next & current will change + var closure = (next, current) { + _currentTransition.future.then((onValue) { + _transitionDone(next, current); + }, onError:(e) { + _transitionDone(next, current); + }); + }; + // + closure(nextSlide, _currentSlide); + } else { + _transitionDone(nextSlide, _currentSlide); + } + _currentSlide = nextSlide; + _currentIndex = nextIndex; + //every time you change slides, reset the timer + restartTimer(); + } + + void _transitionDone(Slide next, Slide current) { + next.direction = ''; + next.entering = false; + next.leaving = false; + next._active = true; + // + if (current != null) { + current.direction = ''; + current.entering = false; + current.leaving = false; + current.active = false; + } + _currentTransition = null; + } + + + /* Allow outside people to call indexOf on slides array */ + int indexOfSlide (slide) { + return slides.indexOf(slide); + } + + void restartTimer() { + resetTimer(); + if (_interval != null && _interval >= 0) { + _currentTimeout = _timeout(() { + timerFn(); + }, delay: _interval); + } + } + + void resetTimer() { + if (_currentTimeout != null) { + _timeout.cancel(_currentTimeout); + _currentTimeout = null; + } + } + + void timerFn() { + new async.Future(() { + if (_isPlaying) { + next(); + restartTimer(); + } else { + pause(); + } + }); + } + + void addSlide(Slide slide, dom.Element element) { + slide.element = element; + slides.add(slide); + //if this is the first slide or the slide is set to active, select it + if(slides.length == 1 || slide.active) { + select(slides[slides.length - 1]); + if (slides.length == 1) { + play(); + } + } else { + slide.active = false; + } + } + + void removeSlide(Slide slide) { + //get the index of the slide inside the carousel + var index = slides.indexOf(slide); + slides.removeAt(index); + if (slides.length > 0 && slide.active) { + if (index >= slides.length) { + select(slides[index - 1]); + } else { + select(slides[index]); + } + } else if (_currentIndex > index) { + _currentIndex--; + } + } + + void detach() { + _destroyed = true; + resetTimer(); + } +} + + +@Component( + selector: 'slide', + publishAs: 's', + useShadowDom: false, + template: ''' +
+
''' + //templateUrl: 'packages/angular_ui/carousel/slide.html' +) +//@Component( +// selector: '[slide]', +// publishAs: 's', +// useShadowDom: false, +// templateUrl: 'packages/angular_ui/carousel/slide.html') +class Slide implements ShadowRootAware, DetachAware, ScopeAware { + Scope scope; + bool _active = false; + @NgTwoWay('active') + set active(bool value) { + if(value == null) { + return; + } + if(value != _active) { + _active = value; + if(_active) { + _carouselCtrl.select(this); + } + } + } + bool get active => _active; + + String _direction = ''; + + @NgTwoWay('direction') String get direction => _direction; + set direction(String val) { + _direction = val; + } + + @NgTwoWay('entering') bool entering = false; + @NgTwoWay('leaving') bool leaving = false; + @NgTwoWay('next') bool next = false; + + //Scope scope; + dom.Element element; + Carousel _carouselCtrl; + + Slide(this.element, this._carouselCtrl) { + _log.fine('SlideComponent'); + } + + @override + void detach() { + //when the scope is destroyed then remove the slide from the current slides array + _carouselCtrl.removeSlide(this); + } + + + @override + void onShadowRoot(shadowRoot) { + //_carouselCtrl.addSlide(this, shadowRoot.querySelector('div')); + _carouselCtrl.addSlide(this, element.querySelector('div')); + } +} \ No newline at end of file diff --git a/lib/carousel/carousel.html b/lib/carousel/carousel.html new file mode 100644 index 0000000..b516756 --- /dev/null +++ b/lib/carousel/carousel.html @@ -0,0 +1,14 @@ + + + \ No newline at end of file diff --git a/lib/carousel/slide.html b/lib/carousel/slide.html new file mode 100644 index 0000000..2e90ca8 --- /dev/null +++ b/lib/carousel/slide.html @@ -0,0 +1,14 @@ + + +
+
\ No newline at end of file diff --git a/lib/collapse/README.md b/lib/collapse/README.md new file mode 100644 index 0000000..33879f4 --- /dev/null +++ b/lib/collapse/README.md @@ -0,0 +1,2 @@ +Angular Dart version of Bootstrap's **collapse** plugin. +Provides a simple way to hide and show an element with a css transition. diff --git a/lib/collapse.dart b/lib/collapse/collapse.dart similarity index 57% rename from lib/collapse.dart rename to lib/collapse/collapse.dart index a803071..443f185 100644 --- a/lib/collapse.dart +++ b/lib/collapse/collapse.dart @@ -1,13 +1,16 @@ -// Copyright (c) 2013 - 2014, akserg (Sergey Akopkokhyants) +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. // https://github.com/akserg/angular.dart.ui // All rights reserved. Please see the LICENSE.md file. library angular.ui.collapse; import 'dart:html' as dom; import 'dart:async' as async; -import "package:angular/angular.dart"; -import "package:angular/utils.dart"; -import 'transition.dart'; +import 'package:angular/angular.dart'; +import 'package:angular/utils.dart'; +import 'package:angular_ui/utils/transition.dart'; + +import 'package:logging/logging.dart' show Logger; +final _log = new Logger('angular.ui.collapse'); /** * Collapse Module. @@ -15,33 +18,33 @@ import 'transition.dart'; class CollapseModule extends Module { CollapseModule() { install(new TransitionModule()); - type(Collapse); + bind(Collapse); } } /** - * Collapse directive. + * The collapsible directive indicates a block of html that will expand and collapse. */ -@NgDirective(selector:'[collapse]') -class Collapse { - +@Component(selector:'[collapse]', useShadowDom: false) +class Collapse implements ScopeAware { + @NgOneWay("collapse") - void set collapseAttr(bool value) { + void set isCollapsed(value) { if (toBool(value)) { collapse(); } else { expand(); } } - + dom.Element element; Transition transition; Scope scope; - + var initialAnimSkip = true; async.Completer currentTransition; - - Collapse(this.element, this.transition, this.scope); + + Collapse(this.element, this.transition); async.Future doTransition(change) { async.Completer newTransition = transition(element, change); @@ -54,50 +57,60 @@ class Collapse { }; if (currentTransition != null && !currentTransition.isCompleted) { - currentTransition.completeError('Canceled'); + currentTransition.complete('Canceled'); } + currentTransition = newTransition; - newTransition.future.then((value)=> newTransitionDone(), onError:(e)=>newTransitionDone()); + + newTransition.future.whenComplete(newTransitionDone); + return newTransition.future; } - + void expand() { if (initialAnimSkip) { initialAnimSkip = false; expandDone(); } else { - element.classes.remove('collapse'); - element.classes.add('collapsing'); - doTransition({ 'height': '${element.scrollHeight}px' }).then((value)=>expandDone()); + element.classes + ..remove('collapse') + ..add('collapsing'); + + doTransition({ 'height': '${element.scrollHeight}px' }).whenComplete(expandDone); } } - + void expandDone() { - element.classes.remove('collapsing'); - element.classes.add('collapse-in'); + element.classes + ..remove('collapsing') + ..add('in') + ..add('collapse'); element.style.height = 'auto'; } - + void collapse() { if (initialAnimSkip) { initialAnimSkip = false; collapseDone(); - element.style.height = "0px"; + element.style.height = "0"; } else { // CSS transitions don't work with height: auto, so we have to manually change the height to a specific value element.style.height = '${element.scrollHeight}px'; //trigger reflow so a browser realizes that height was updated from auto to a specific value var x = element.offsetWidth; - element.classes.remove('collapse-in'); - element.classes.add('collapsing'); + element.classes + ..remove('collapse') + ..remove('in') + ..add('collapsing'); - doTransition({ 'height': '0' }).then((value)=>collapseDone()); + doTransition({ 'height': '0' }).whenComplete(collapseDone); } } - + void collapseDone() { - element.classes.remove('collapsing'); - element.classes.add('collapse'); + element.classes + ..remove('collapsing') + ..add('collapse'); } } \ No newline at end of file diff --git a/lib/css/angular.css b/lib/css/angular.css new file mode 100644 index 0000000..fce0fef --- /dev/null +++ b/lib/css/angular.css @@ -0,0 +1,68 @@ +/* +Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. +https://github.com/akserg/angular.dart.ui +All rights reserved. Please see the LICENSE.md file. +*/ +@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; +} + +.panel-group accordion-group .panel { + margin-top: 5px; +} + +.item { + position: relative; + display: none; + -webkit-transition: .6s ease-in-out left; + transition: .6s ease-in-out left; +} +img, +a > img { + display: block; + max-width: 100%; + height: auto; + line-height: 1; +} +.active, +.next, +.prev { + display: block; +} +.active { + left: 0; +} +.next, +.prev { + position: absolute; + top: 0; + width: 100%; +} +.next { + left: 100%; +} +.prev { + left: -100%; +} +.next.left, +.prev.right { + left: 0; +} +.active.left { + left: -100%; +} +.active.right { + left: 100%; +} + +.nav, .pagination, .carousel, .panel-title a { + cursor: pointer; +} \ No newline at end of file diff --git a/lib/datepicker/README.md b/lib/datepicker/README.md new file mode 100644 index 0000000..c7632a3 --- /dev/null +++ b/lib/datepicker/README.md @@ -0,0 +1,100 @@ +A clean, flexible, and fully customizable **date picker**. + +User can navigate through months and years. +The **datepicker** shows dates that come from other than the main month being displayed. These other dates are also selectable. + +Everything is formatted using the [date filter](http://docs.angularjs.org/api/ng.filter:date) and thus is also localized. + +### Datepicker Settings ### + +All settings can be provided as attributes in the `` or globally configured through the `DatepickerConfig`. + + * `ng-model` + : + The date object. + + * `show-weeks` + _(Defaults: true)_ : + Whether to display week numbers. + + * `starting-day` + _(Defaults: 0)_ : + Starting day of the week from 0-6 (0=Sunday, ..., 6=Saturday). + + * `min` + _(Default: null)_ : + Defines the minimum available date. + + * `max` + _(Default: null)_ : + Defines the maximum available date. + + * `date-disabled (date, mode)` + _(Default: null)_ : + An optional expression to disable visible options based on passing date and current mode _(day|month|year)_. + + * `day-format` + _(Default: 'dd')_ : + Format of day in month. + + * `month-format` + _(Default: 'MMMM')_ : + Format of month in year. + + * `year-format` + _(Default: 'yyyy')_ : + Format of year in year range. + + * `year-range` + _(Default: 20)_ : + Number of years displayed in year selection. + + * `day-header-format` + _(Default: 'EEE')_ : + Format of day in week header. + + * `day-title-format` + _(Default: 'MMMM yyyy')_ : + Format of title when selecting day. + + * `month-title-format` + _(Default: 'yyyy')_ : + Format of title when selecting month. + + +### Popup Settings ### + +Options for datepicker can be passed as JSON using the `datepicker-options` attribute. +Specific settings for the `datepicker-popup`, that can globally configured through the `DatepickerPopupConfig`, are: + + * `datepicker-popup` + _(Default: 'yyyy-MM-dd')_ : + The format for displayed dates. + + * `show-button-bar` + _(Default: true)_ : + Whether to display a button bar underneath the datepicker. + + * `current-text` + _(Default: 'Today')_ : + The text to display for the current day button. + + * `toggle-weeks-text` + _(Default: 'Weeks')_ : + The text to display for the toggling week numbers button. + + * `clear-text` + _(Default: 'Clear')_ : + The text to display for the clear button. + + * `close-text` + _(Default: 'Done')_ : + The text to display for the close button. + + * `close-on-date-selection` + _(Default: true)_ : + Whether to close calendar when a date is chosen. + + * `datepicker-append-to-body` + _(Default: false)_: + Append the datepicker popup element to `body`, rather than inserting after `datepicker-popup`. For global configuration, use `DatepickerPopupConfig.appendToBody`. diff --git a/lib/datepicker/datepicker.dart b/lib/datepicker/datepicker.dart new file mode 100644 index 0000000..de30c4c --- /dev/null +++ b/lib/datepicker/datepicker.dart @@ -0,0 +1,610 @@ +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +library angular.ui.datepicker; + +import 'dart:html' as dom; +import "package:angular/angular.dart"; +import "package:angular/core_dom/module_internal.dart"; +import "package:angular_ui/utils/position.dart"; +import 'package:angular_ui/utils/utils.dart'; + +part 'popup.dart'; + +/** + * Datepicker Module. + */ +class DatepickerModule extends Module { + DatepickerModule() { + install(new PositionModule()); + bind(DatepickerConfig, toValue:new DatepickerConfig()); + bind(DatepickerPopupConfig, toValue:new DatepickerPopupConfig()); + bind(Datepicker); + bind(DatepickerPopupWrap); + bind(DatepickerPopup); + bind(WeekNumberFilter); + } +} + +typedef VisibleDates _GetVisibleDates(DateTime date, DateTime selected); + +/** + * Datepicker configuration. + */ +@Injectable() +class DatepickerConfig { + String dayFormat = 'dd'; + String monthFormat = 'MMMM'; + String yearFormat = 'yyyy'; + String dayHeaderFormat = 'EEE'; + String dayTitleFormat = 'MMMM yyyy'; + String monthTitleFormat = 'yyyy'; + bool showWeeks = true; + int startingDay = 0; + int yearRange = 20; + String minDate = null; + String maxDate = null; +} + +/** + * List of visible dates. + */ +@Injectable() +class VisibleDates { + List objects = []; + String title = ''; + List labels = []; +} + +/** + * Date format to show. + */ +@Injectable() +class Format { + String day; + String month; + String year; + String dayHeader; + String dayTitle; + String monthTitle; +} + +/** + * Date Value Object + */ +@Injectable() +class DateVO { + DateTime date; + String label = ''; + bool selected = false; + bool secondary = false; + bool disabled = false; +} + +/** + * Datepicker mode. + */ +@Injectable() +class Mode { + String name; + _GetVisibleDates getVisibleDates; + int split; + Function compare; + Map step; +} + +/** + * Datepicker. + */ +@Component( + selector: 'datepicker[ng-model]', + useShadowDom: false, + template: ''' + + + + + + + + + + + + +
''' + //templateUrl: 'packages/angular_ui/datepicker/datepicker.html' +) +//@Component(selector: '[datepicker][ng-model]', publishAs: 'd', +// useShadowDom: false, +// templateUrl: 'packages/angular_ui/datepicker/datepicker.html') +class Datepicker implements ShadowRootAware, ScopeAware { + + int mode = 0; + DateTime selected = new DateTime.now(); + bool showWeekNumbers = false; + + var _lastModelValue; + bool _modelValueChanged = false; + bool get modelValueChanged { + var res = _modelValueChanged; + _modelValueChanged = false; + return res; + } + + Mode get currentMode { + return modes[mode]; + } + + var format; + List modes; + + @NgOneWay('day-format') + void set dayFormat(String value) { + format.day = value != null ? value : _datepickerConfig.dayFormat; + } + + @NgOneWay('month-format') + void set monthFormat(String value) { + format.month = value != null ? value : _datepickerConfig.monthFormat; + } + + @NgOneWay('year-format') + void set yearFormat(String value) { + format.year = value != null ? value : _datepickerConfig.yearFormat; + } + + @NgOneWay('day-header-format') + void set dayHeaderFormat(String value) { + format.dayHeader = value != null ? value : _datepickerConfig.dayHeaderFormat; + } + + @NgOneWay('day-title-format') + void set dayTitleFormat(String value) { + format.dayTitle = value != null ? value : _datepickerConfig.dayTitleFormat; + } + + @NgOneWay('month-title-format') + void set monthTitleFormat(String value) { + format.monthTitle = value != null ? value : _datepickerConfig.monthTitleFormat; + } + + int _startingDay; + @NgOneWay('starting-day') + void set startingDay(int value) { + _startingDay = value != null ? value : _datepickerConfig.startingDay; + } + int get startingDay => _startingDay; + + int _yearRange; + @NgOneWay('year-range') + void set yearRange(int value) { + _yearRange = value != null ? value : _datepickerConfig.yearRange; + } + int get yearRange => _yearRange; + + var _dateDisabled = null; + @NgCallback('date-disabled') + void set dateDisabled(value) { + _dateDisabled = null; + } + get dateDisabled => _dateDisabled; + + bool _showWeeks = false; + @NgOneWay('show-weeks') + set showWeeks(bool value) { + if (value == null) { + value = false; + } + _showWeeks = value; + updateShowWeekNumbers(); + } + bool get showWeeks => _showWeeks; + + DateTime minDate; + @NgOneWay('min') + void set min(value) { + minDate = parseDate(value); + refill(); + } + + DateTime maxDate; + @NgOneWay('max') + void set max(value) { + maxDate = parseDate(value); + refill(); + } + + dom.Element _element; + DatepickerConfig _datepickerConfig; + NodeAttrs _attrs; + NgModel _ngModel; + Scope _scope; + Date _dateFilter; + + List rows; + List labels; + + Datepicker(this._element, this._datepickerConfig, this._attrs, this._ngModel, this._dateFilter); + + void set scope(Scope scope) { + this._scope = scope; + init(); + + showWeeks = _datepickerConfig.showWeeks; + + _ngModel.render = (value) { + refill(true); + }; + } + + /** + * Redraw component first time because ShadowRoot are available now. + */ + @override + void onShadowRoot(shadowRoot) { + refill(true); + } + + Datepicker.forTests(this._element, this._datepickerConfig, this._attrs, this._scope, this._dateFilter) { + init(); + } + + + void updateShowWeekNumbers() { + showWeekNumbers = mode == 0 && showWeeks; + showWeekNumbersEls(); + } + + void refill([bool updateSelected = false]) { + bool valid = true; + DateTime date = parseDate(_ngModel.modelValue); + if (date == null) { + valid = false; + } else if (updateSelected) { + selected = date; + } + + var currentMode = modes[mode]; + VisibleDates data = currentMode.getVisibleDates(selected, date); + data.objects.forEach((DateVO obj) { + obj.disabled = isDisabled(obj.date, mode); + }); + + rows = split(data.objects, currentMode.split); + labels = data.labels; + String title = data.title; + + // DOM render + + dom.TableCellElement titleEl = _element.querySelector("#title"); + if (titleEl != null) { + if (rows.length > 0) { + titleEl.colSpan = rows[0].length - 2 + (showWeekNumbers ? 1 : 0); + } + (titleEl.firstChild as dom.ButtonElement).setInnerHtml('$title'); + } + + dom.TableRowElement labelsEl = _element.querySelector("#labels"); + if (labelsEl != null) { + labelsEl.children.clear(); + + dom.TableCellElement showWeekNumbersEl = new dom.TableCellElement(); + showWeekNumbersEl.classes.add("text-center"); + if (showWeekNumbers) { + showWeekNumbersEl.classes.remove("ng-hide"); + } else { + showWeekNumbersEl.classes.add("ng-hide"); + } + showWeekNumbersEl.text = '#'; + labelsEl.append(showWeekNumbersEl); + + labels.forEach((String label) { + dom.TableCellElement labelEl = new dom.TableCellElement(); + labelEl.classes.add("text-center"); + labelEl.text = label; + labelsEl.append(labelEl); + }); + } + + dom.TableSectionElement rowsEl = _element.querySelector("#rows"); + if (rowsEl != null) { + rowsEl.children.clear(); + + rows.forEach((List row){ + dom.TableRowElement rowEl = new dom.TableRowElement(); + rowsEl.append(rowEl); + + dom.TableCellElement rowWeekNumbersEl = new dom.TableCellElement(); + rowWeekNumbersEl.classes.add("text-center"); + if (showWeekNumbers) { + rowWeekNumbersEl.classes.remove("ng-hide"); + } else { + rowWeekNumbersEl.classes.add("ng-hide"); + } + rowWeekNumbersEl.setInnerHtml('${getWeekNumber(row)}'); + rowEl.append(rowWeekNumbersEl); + + row.forEach((DateVO dt) { + dom.TableCellElement dtEl = new dom.TableCellElement(); + dtEl.classes.add("text-center"); + rowEl.append(dtEl); + + dom.ButtonElement btnEl = new dom.ButtonElement() + ..type = 'button' + ..style.width = '100%' + ..classes.add('btn') + ..classes.add('btn-default') + ..classes.add('btn-sm') + ..onClick.listen((dom.MouseEvent evt){ + select(dt.date); + }); + if (dt.selected) { + btnEl.classes.add('btn-info'); + } + if (dt.disabled) { + btnEl.disabled = true; + } + dtEl.append(btnEl); + + dom.SpanElement labelSpan = new dom.SpanElement() + ..text = dt.label; + if (dt.secondary) { + labelSpan.classes.add('text-muted'); + } + btnEl.append(labelSpan); + }); + }); + } + } + + void showWeekNumbersEls() { + dom.TableCellElement titleEl = _element.querySelector("#title"); + if (titleEl != null && rows != null && rows.length > 0) { + titleEl.colSpan = rows[0].length - 2 + (showWeekNumbers ? 1 : 0); + } + // + List showWeekNumbersEls = _element.querySelectorAll("#labels > td") as List; + if (showWeekNumbersEls != null && showWeekNumbersEls.length > 0) { + dom.TableCellElement showWeekNumbersEl = showWeekNumbersEls.first; + if (showWeekNumbers) { + showWeekNumbersEl.classes.remove("ng-hide"); + } else { + showWeekNumbersEl.classes.add("ng-hide"); + } + } + // + dom.TableSectionElement rowsEl = _element.querySelector("#rows"); + if (rowsEl != null) { + rowsEl.children.forEach((dom.TableRowElement rowEl) { + dom.TableCellElement rowWeekNumbersEl = rowEl.firstChild; + if (showWeekNumbers) { + rowWeekNumbersEl.classes.remove("ng-hide"); + } else { + rowWeekNumbersEl.classes.add("ng-hide"); + } + }); + } + } + + void setMode(value) { + mode = value; + updateShowWeekNumbers(); + refill(); + } + + void select(DateTime date) { + if (mode == 0) { + DateTime dt; + + if (_ngModel.modelValue == null) { + dt = new DateTime(0); + } else { + if (_ngModel.modelValue is String) { + try { + dt = DateTime.parse(_ngModel.modelValue); + } on FormatException catch(ex) { + dt = new DateTime(0); + } + } else if (_ngModel.modelValue is int) { + dt = new DateTime.fromMillisecondsSinceEpoch(_ngModel.modelValue); + } else { + dt = _ngModel.modelValue as DateTime; + } + } + + _ngModel.viewValue = new DateTime(date.year, date.month, date.day, dt.hour, dt.minute, dt.second, dt.millisecond); + refill(true); + } else { + selected = date; + setMode(mode - 1); + } + } + + void move(int direction) { + var step = modes[mode].step; + int month = selected.month + direction * (step.containsKey('months') ? step['months'] : 0); + int year = selected.year + direction * (step.containsKey('years') ? step['years'] : 0); + selected = new DateTime(year, month, selected.day, selected.hour, selected.minute, selected.second, selected.millisecond); + refill(); + } + + void toggleMode() { + setMode((mode + 1) % modes.length); + } + + int getWeekNumber(List row) { + return (mode == 0 && showWeekNumbers && row.length == 7) ? + getISO8601WeekNumber(row[0].date) : null; + } + + int getISO8601WeekNumber(DateTime date) { + var checkDate = new DateTime(date.year, date.month, date.day + 4 - date.weekday % 7); + var time = new DateTime(checkDate.year, 1, 1); + // Compare with Jan 1 the same year + return ((((checkDate.millisecondsSinceEpoch - time.millisecondsSinceEpoch) / 86400000).round() / 7) + 1).floor(); + } + + void init() { + format = new Format() + ..day = eval(_scope, _attrs['day-format'], _datepickerConfig.dayFormat) + ..month = eval(_scope, _attrs['month-format'], _datepickerConfig.monthFormat) + ..year = eval(_scope, _attrs['year-format'], _datepickerConfig.yearFormat) + ..dayHeader = eval(_scope, _attrs['day-header-format'], _datepickerConfig.dayHeaderFormat) + ..dayTitle = eval(_scope, _attrs['day-title-format'], _datepickerConfig.dayTitleFormat) + ..monthTitle = eval(_scope, _attrs['month-title-format'], _datepickerConfig.monthTitleFormat); + + startingDay = eval(_scope, _attrs['starting-day'], _datepickerConfig.startingDay); + yearRange = eval(_scope, _attrs['year-rRange'], _datepickerConfig.yearRange); + + minDate = _datepickerConfig.minDate != null ? DateTime.parse(_datepickerConfig.minDate) : null; + maxDate = _datepickerConfig.maxDate != null ? DateTime.parse(_datepickerConfig.maxDate) : null; + + modes = [ + new Mode() + ..name = 'day' + ..getVisibleDates = (DateTime date, DateTime selected) { + var year = date.year, + month = date.month, + firstDayOfMonth = new DateTime(year, month, 1); + var difference = startingDay - firstDayOfMonth.weekday, + numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : -difference, + firstDate = new DateTime.fromMillisecondsSinceEpoch(firstDayOfMonth.millisecondsSinceEpoch), + numDates = 0; + + if (numDisplayedFromPreviousMonth > 0) { + firstDate = firstDate.add(new Duration(days: -numDisplayedFromPreviousMonth + 1)); + numDates += numDisplayedFromPreviousMonth; // Previous + } + numDates += getDaysInMonth(year, month + 1); // Current + numDates += (7 - numDates % 7) % 7; // Next + + var days = getDates(firstDate, numDates), + labels = new List(); // !!! 7 + for (var i = 0; i < numDates; i++) { + DateTime dt = days[i]; + days[i] = makeDate(dt, format.day, selected != null && + selected.day == dt.day && selected.month == dt.month && + selected.year == dt.year, dt.month != month); + } + for (var j = 0; j < 7; j++) { + labels.add(_dateFilter(days[j].date, format.dayHeader)); + } + return new VisibleDates() + ..objects = days + ..title = _dateFilter(date, format.dayTitle) + ..labels = labels; + } + ..compare = (DateTime date1, DateTime date2) { + return new DateTime(date1.year, date1.month, date1.day).compareTo( + new DateTime(date2.year, date2.month, date2.day)); + } + ..split = 7 + ..step = { + 'months': 1 + }, + new Mode() + ..name = 'month' + ..getVisibleDates = (DateTime date, DateTime selected) { + var months = new List(), + year = date.year; + for (var i = 1; i <= 12; i++) { + var dt = new DateTime(year, i, 1); + months.add(makeDate(dt, format.month, + (selected != null && selected.month == i && selected.year == year))); + } + return new VisibleDates() + ..objects = months + ..title = _dateFilter(date, format.monthTitle); + } + ..compare = (DateTime date1, DateTime date2) { + return new DateTime(date1.year, date1.month).compareTo(new DateTime( + date2.year, date2.month)); + } + ..split = 3 + ..step = { + 'years': 1 + }, + new Mode() + ..name = 'year' + ..getVisibleDates = (DateTime date, DateTime selected) { + var years = new List(), + year = date.year, + startYear = ((year - 1) ~/ yearRange) * yearRange + 1; + for (var i = 0; i < yearRange; i++) { + var dt = new DateTime(startYear + i, 1, 1); + years.add(makeDate(dt, format.year, + (selected != null && selected.year == dt.year))); + } + return new VisibleDates() + ..objects = years + ..title = [years[0].label, years[yearRange - 1].label].join(' - '); + } + ..compare = (DateTime date1, DateTime date2) { + return date1.year - date2.year; + } + ..split = 5 + ..step = { + 'years': yearRange + }]; + } + + + int getDaysInMonth(year, month) { + return new DateTime(year, month, 0).day; + } + + List getDates(DateTime startDate, int n) { + var dates = new List(); + var i = 0; + // Prevent repeated dates because of timezone bug + var current = new DateTime(startDate.year, startDate.month, startDate.day, 12, 0, 0); + while (i++ < n) { + dates.add(new DateTime.fromMillisecondsSinceEpoch(current.millisecondsSinceEpoch)); + current = current.add(new Duration(days: 1)); + } + return dates; + } + + DateVO makeDate(DateTime date, String format, bool isSelected, [bool + isSecondary = false]) { + return new DateVO() + ..date = date + ..label = _dateFilter(date, format) + ..selected = !!isSelected + ..secondary = !!isSecondary; + } + + + bool isDisabled(DateTime date, [int mode = 0]) { + var currentMode = modes[mode]; + return ((minDate != null && currentMode.compare(date, minDate) < 0) || + (maxDate != null && currentMode.compare(date, this.maxDate) > 0) || + (dateDisabled != null && dateDisabled({'date':date, 'mode':currentMode.name}))); + } + +// bool isDisabled(DateTime date, [int mode = 0]) { +// var currentMode = modes[mode]; +// if (minDate != null) { +// return currentMode.compare(date, minDate) < 0; +// } else if (maxDate != null) { +// return currentMode.compare(date, this.maxDate) > 0; +// } else if (dateDisabled != null ){ +// return dateDisabled({'date': date, 'mode': currentMode.name}); +// } +// return false; +// } +} + +/** + * Filter to show week number + */ +@Formatter(name:'weekNumber') +class WeekNumberFilter { + call(valueToFilter, datepicker) { + if (valueToFilter != null && valueToFilter is List) { + return datepicker.getWeekNumber(valueToFilter.toList()); + } + return null; + } +} diff --git a/lib/datepicker/datepicker.html b/lib/datepicker/datepicker.html new file mode 100644 index 0000000..3b4da14 --- /dev/null +++ b/lib/datepicker/datepicker.html @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + +
diff --git a/lib/datepicker/popup.dart b/lib/datepicker/popup.dart new file mode 100644 index 0000000..59a0d13 --- /dev/null +++ b/lib/datepicker/popup.dart @@ -0,0 +1,289 @@ +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +part of angular.ui.datepicker; + +/** + * Datepicker popup configuration. + */ +class DatepickerPopupConfig { + String dateFormat = 'yyyy-MM-dd'; + String currentText = 'Today'; + String toggleWeeksText = 'Weeks'; + String clearText = 'Clear'; + String closeText = 'Done'; + bool closeOnDateSelection = true; + bool appendToBody = false; + bool showButtonBar = true; +} + +/** + * Datapicker Popup directive. + */ + +@Decorator(selector: 'datepicker-popup[ng-model]') +@Decorator(selector: '[datepicker-popup][ng-model]') +class DatepickerPopup implements ScopeAware { + + String _dateFormat; + bool _closeOnDateSelection = false; + bool _appendToBody = false; + + var getIsOpen, setIsOpen; + var popupEl, datepickerEl; + + Scope _originalScope; + Scope _scope; + dom.Element _element; + Date _dateFilter; + NodeAttrs _attrs; + NgModel _ngModel; + Parser _parser; + Compiler _compiler; + DirectiveMap _directiveMap; + Injector _injector; + DatepickerConfig _datepickerConfig; + DatepickerPopupConfig _datepickerPopupConfig; + Position _position; + + DatepickerPopup(Scope _originalScope, this._element, this._dateFilter, this._attrs, this._ngModel, this._position, + this._parser, this._compiler, this._directiveMap, this._injector, this._datepickerConfig, this._datepickerPopupConfig); + + void set scope(Scope originalScope) { + this._originalScope = originalScope; + // create a child scope so we are not polluting original one + _scope = _originalScope.createChild(new PrototypeMap(_originalScope.context)); + + _closeOnDateSelection = _attrs.containsKey('close-on-date-selection') ? _originalScope.eval(_attrs['close-on-date-selection']) : _datepickerPopupConfig.closeOnDateSelection; + _appendToBody = _attrs.containsKey('datepicker-append-to-body') ? _originalScope.eval(_attrs['datepicker-append-to-body']) : _datepickerPopupConfig.appendToBody; + + _attrs.observe('datepicker-popup', (value) { + _dateFormat = value != null ? value : _datepickerPopupConfig.dateFormat; + _ngModel.render(null); + }); + + _scope.context['showButtonBar'] = _attrs.containsKey('show-button-bar') ? _originalScope.eval(_attrs['show-button-bar']) : _datepickerPopupConfig.showButtonBar; + + _originalScope.on(r'$destroy').listen((evt) { + popupEl.remove(); + _scope.destroy(); + }); + + _attrs.observe('current-text', (text) { + _scope.context['currentText'] = text != null ? text : _datepickerPopupConfig.currentText; + }); + + _attrs.observe('toggle-wWeeks-text', (text) { + _scope.context['toggleWeeksText'] = text != null ? text : _datepickerPopupConfig.toggleWeeksText; + }); + + _attrs.observe('clear-text', (text) { + _scope.context['clearText'] = text != null ? text : _datepickerPopupConfig.clearText; + }); + + _attrs.observe('close-text', (text) { + _scope.context['closeText'] = text != null ? text : _datepickerPopupConfig.closeText; + }); + + if (_attrs.containsKey('is-open')) { + getIsOpen = _parser(_attrs['is-open']); + setIsOpen = getIsOpen.assign; + + _originalScope.watch(getIsOpen, (value, prev) { + _scope.context['isOpen'] = !! value; + }); + } + _scope.context['isOpen'] = getIsOpen != null ? getIsOpen(_originalScope) : false; // Initial state + + /////////////// + String html = '
'; + // Convert to html + + popupEl = compile(html, _injector, _compiler, scope:_scope, directives: _directiveMap); + datepickerEl = popupEl.querySelector('[datepicker]'); + // + popupEl.attributes['ng-model'] = 'date'; + popupEl.attributes['ng-change'] = 'dateSelection()'; + datepickerEl.attributes['ng-model'] = 'date'; + // + Map datepickerOptions = {}; + if (_attrs.containsKey('datepicker-options')) { + datepickerOptions = _originalScope.eval(_attrs['datepicker-options']); + datepickerOptions.forEach((key, value) { + datepickerEl.setAttribute(key, value.toString()); + }); + } + + // Inner change + _scope.context['dateSelection'] = (dt) { + if (dt != null) { + _scope.context['date'] = dt; + } + _ngModel.viewValue = _parseDate(_scope.context['date']); + _ngModel.render(null); + + if (_closeOnDateSelection != null) { + setOpen(false); + } + }; + + _element.onInput.listen(_inputChanged); + _element.onChange.listen(_inputChanged); + _element.onKeyUp.listen(_inputChanged); + + // Outter change + _ngModel.render = (value) { + var date = _ngModel.viewValue != null ? _dateFilter(_ngModel.viewValue, _dateFormat) : ''; + (_element as dynamic).value = date; + _scope.context['date'] = _ngModel.modelValue; + }; + + addWatchableAttribute(_attrs['min'], 'min'); + addWatchableAttribute(_attrs['max'], 'max'); + + if (_attrs.containsKey('show-weeks')) { + addWatchableAttribute(_attrs['show-weeks'], 'showWeeks', 'show-weeks'); + } else { + _scope.context['showWeeks'] = datepickerOptions.containsKey('show-weeks') ? datepickerOptions['show-weeks'] : _datepickerConfig.showWeeks; + datepickerEl.attributes['show-weeks'] = 'showWeeks'; + } + + if (_attrs.containsKey('date-disabled')) { + datepickerEl.attributes['date-disabled'] = _attrs['date-disabled']; + } + + var documentBindingInitialized = false, elementFocusInitialized = false; + _scope.watch('isOpen', (value, prev) { + if (value != null) { + updatePosition(); + dom.document.addEventListener('click', _documentClickBind); + if(elementFocusInitialized) { + _element.removeEventListener('focus', _elementFocusBind); + } + _element.focus(); // element[0] + documentBindingInitialized = true; + } else { + if(documentBindingInitialized) { + dom.document.removeEventListener('click', _documentClickBind); + } + _element.addEventListener('focus', _elementFocusBind); + elementFocusInitialized = true; + } + + if (setIsOpen != null) { + setIsOpen(_originalScope, value); + } + }); + + _scope.context['today'] = () { + _scope.context['dateSelection'](new DateTime.now()); + }; + _scope.context['clear'] = () { + _scope.context['dateSelection'](null); + }; + + if (_appendToBody) { + dom.document.body.append(popupEl); + } else { + _element.parent.append(popupEl); // after + } + } + + void setOpen( value ) { + if (setIsOpen) { + setIsOpen(_originalScope, !!value); + } else { + _scope.context['isOpen'] = !!value; + } + } + + void _documentClickBind(dom.Event event) { + if (_scope.context['isOpen'] && event.target != _element) { + _scope.apply(() { + setOpen(false); + }); + } + } + + void _elementFocusBind(dom.Event evt) { + _scope.apply(() { + setOpen(true); + }); + } + + DateTime _parseDate(viewValue) { + if (viewValue == null) { +// _ngModel.setValidity('date', true); + return null; + } else if (viewValue is DateTime) { +// _ngModel.setValidity('date', true); + return viewValue; + } else if (viewValue is String) { + var date = DateTime.parse(viewValue); + if (date == null) { +// _ngModel.setValidity('date', false); + return null; + } else { +// _ngModel.setValidity('date', true); + return date; + } + } else { +// _ngModel.setValidity('date', false); + return null; + } + } + + void _inputChanged(dom.Event event) { + _scope.apply(() => _scope.context['date'] = _ngModel.modelValue); + } + + void addWatchableAttribute(attribute, scopeProperty, [datepickerAttribute = null]) { + if (attribute != null) { + _originalScope.watch(attribute, (value, prev) { + _scope.context[scopeProperty] = value; + }); + datepickerEl.attributes[datepickerAttribute != null ? datepickerAttribute : scopeProperty] = scopeProperty; + } + } + + void updatePosition() { + _scope.context['position'] = _appendToBody ? _position.offset(_element) : _position.position(_element); + _scope.context['position'].top = _scope.context['position'].top + _element.offsetHeight; + } +} + +@Component( + selector: 'datepicker-popup-wrap', + useShadowDom: false, + template: ''' +''' + //templateUrl: 'packages/angular_ui/datepicker/popup.html' +) +//@Component(selector: '[datepicker-popup-wrap]', publishAs: 'd', +// useShadowDom: false, +// templateUrl: 'packages/angular_ui/datepicker/popup.html') +class DatepickerPopupWrap implements ScopeAware { + + Scope scope; + + String get display { + return scope.context['isOpen'] != null && scope.context['isOpen'] ? 'block' : 'none'; + } + + DatepickerPopupWrap(dom.Element element) { + element.onClick.listen((dom.Event event) { + event.preventDefault(); + event.stopPropagation(); + }); + } +} \ No newline at end of file diff --git a/lib/datepicker/popup.html b/lib/datepicker/popup.html new file mode 100644 index 0000000..4820387 --- /dev/null +++ b/lib/datepicker/popup.html @@ -0,0 +1,17 @@ + + + diff --git a/lib/dragdrop/README.md b/lib/dragdrop/README.md new file mode 100644 index 0000000..9372a13 --- /dev/null +++ b/lib/dragdrop/README.md @@ -0,0 +1,17 @@ +Angular Dart **Drag&Drop** directives. + +The **ui-draggable** directive identifies the draggable object. +Available settings: + - `allowed-drop-zones`: String or array of Strings. Specify the drop-zones to which this component can drop. + - `draggable-enabled`: bool value. whether the object is draggable. Default is true. + - `draggable-data`: the data that has to be dragged. It can be any JS object. + - `ui-draggable`: an instance of DragDropConfig class. It permits to configure how the drag&drop look&feel (cursor, drag image, custom classes to add on drag/drop events) + - `on-drag-success`: callback function called when the drag action ends with a valid drop action. It is activated after the `on-drop-success` callback + +The **ui-droppable** directive identifies the drop target object. +Available settings: + + - `drop-zones`: String or array of Strings. It permits to specify the drop zones associated with this component. By default, if the `drop-zones` attribute is not specified, the droppable component accepts drop operations by all the draggable components that do not specify the `allowed-drop-zones` + - `on-drop-success`: callback function called when the drop action completes correctly. It is activated before the `on-drag-success` callback. + - `ui-droppable`: an instance of DragDropConfig class. It permits to configure how the drag&drop look&feel (cursor, drag image, custom classes to add on drag/drop events) + diff --git a/lib/dragdrop/common.dart b/lib/dragdrop/common.dart new file mode 100644 index 0000000..fa2d5a3 --- /dev/null +++ b/lib/dragdrop/common.dart @@ -0,0 +1,185 @@ +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +part of angular.ui.dragdrop; + +@Injectable() +class DragDropZonesService { + List allowedDropZones = []; +} + +class DragImage { + html.Element imageElement; + int x_offset; + int y_offset; + + DragImage(this.imageElement, {this.x_offset: 0, this.y_offset: 0}) {} + +} + +class BaseDDConfig { + DragImage dragImage; + DataTransferEffect dragEffect = DataTransferEffect.MOVE; + DataTransferEffect dropEffect = DataTransferEffect.MOVE; + String dragCursor = "move"; +} + +class DataTransferEffect { + + static const COPY = const DataTransferEffect('copy'); + static const LINK = const DataTransferEffect('link'); + static const MOVE = const DataTransferEffect('move'); + static const NONE = const DataTransferEffect('none'); + static const values = const [COPY, LINK, MOVE, NONE]; + + final String name; + const DataTransferEffect(this.name); +} + +abstract class AbstractDraggableDroppableComponent { + + DraggableElementHandler _draggableHandler; + List _dropZoneNames = [new math.Random().nextDouble().toString()]; + + final html.Element elem; + final DragDropZonesService ddZonesService; + + BaseDDConfig _config; + bool _dragEnabled = false; + bool dropEnabled = false; + + List get dropZoneNames => _dropZoneNames; + set dropZoneNames(var names) { + if (names!=null && (names is String)) { + this._dropZoneNames = [names]; + } else if (names is List) { + this._dropZoneNames = names; + } + } + + BaseDDConfig get config => _config; + set config(BaseDDConfig config) { + this._config = config; + _draggableHandler.refresh(); + } + + bool get dragEnabled => _dragEnabled; + set dragEnabled(bool enabled) { + _dragEnabled = enabled; + _draggableHandler.refresh(); + } + + AbstractDraggableDroppableComponent(this.elem, this.ddZonesService, BaseDDConfig config) { + _draggableHandler = new DraggableElementHandler(this); + this.config = config; + + //drop events + { + elem.onDragEnter.listen(_onDragEnter); + elem.onDragOver.listen((html.MouseEvent event) { + _onDragOver(event); + //workaround to avoid NullPointerException during unit testing + if (event.dataTransfer!=null) { + event.dataTransfer.dropEffect = config.dropEffect.name; + } + }); + elem.onDragLeave.listen(_onDragLeave); + elem.onTouchEnter.listen(_onDragEnter); + elem.onTouchLeave.listen(_onDragLeave); + elem.onDrop.listen(_onDrop); + } + + //drag events + { + elem.onDragStart.listen((html.MouseEvent event) { + _onDragStart(event); + //workaround to avoid NullPointerException during unit testing + if (event.dataTransfer!=null) { + event.dataTransfer.effectAllowed = this.config.dragEffect.name; + event.dataTransfer.setData('text/html', ''); + + if (this.config.dragImage!=null) { + DragImage dragImage = this.config.dragImage; + event.dataTransfer.setDragImage(dragImage.imageElement, dragImage.x_offset, dragImage.y_offset); + } + + } + }); + elem.onDragEnd.listen(_onDragEnd) ; + + elem.onTouchStart.listen(_onDragStart) ; + elem.onTouchEnd.listen(_onDragEnd) ; + } + } + + void _onDragEnter(html.Event event) { + if(!dropEnabled || !isDropAllowed()) { + return; + } + _log.finer("'dragEnter' event"); + // This is necessary to allow us to drop. + event.preventDefault(); + onDragEnterCallback(event); + } + + void _onDragOver(html.Event event) { + if(!dropEnabled || !isDropAllowed()) { + return; + } + _log.finest("'dragOver' event"); + // This is necessary to allow us to drop. + event.preventDefault(); + onDragOverCallback(event); + } + + void _onDragLeave(html.Event event) { + if(!dropEnabled || !isDropAllowed()) { + return; + } + _log.finer("'dragLeave' event"); + onDragLeaveCallback(event); + } + + void _onDrop(html.Event event) { + if(!dropEnabled || !isDropAllowed()) { + return; + } + _log.finer("'drop' event"); + onDropCallback(event); + } + + bool isDropAllowed() { + if (_dropZoneNames.isEmpty && ddZonesService.allowedDropZones.isEmpty) { + return true; + } + for (String dragZone in ddZonesService.allowedDropZones) { + if (_dropZoneNames.contains(dragZone)) { + return true; + } + } + return false; + } + + void _onDragStart(html.Event event) { + if(!_dragEnabled) { + return; + } + _log.finer("'dragStart' event"); + ddZonesService.allowedDropZones = _dropZoneNames; + onDragStartCallback(event); + } + + void _onDragEnd(html.Event event) { + _log.finer("'dragEnd' event"); + ddZonesService.allowedDropZones = []; + onDragEndCallback(event); + } + + void onDragEnterCallback(html.Event event) {} + void onDragOverCallback(html.Event event) {} + void onDragLeaveCallback(html.Event event) {} + void onDropCallback(html.Event event) {} + void onDragStartCallback(html.Event event) {} + void onDragEndCallback(html.Event event) {} + +} diff --git a/lib/dragdrop/dragdrop.dart b/lib/dragdrop/dragdrop.dart new file mode 100644 index 0000000..24d450f --- /dev/null +++ b/lib/dragdrop/dragdrop.dart @@ -0,0 +1,29 @@ +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +library angular.ui.dragdrop; + +import 'package:angular/angular.dart'; +import 'package:logging/logging.dart' show Logger; +import 'dart:html' as html; +import 'dart:math' as math; + +part 'common.dart'; +part 'draggable.dart'; +part 'droppable.dart'; +part 'sortable.dart'; + +final _log = new Logger('angular.ui.dragdrop'); + +class DragDropModule extends Module { + DragDropModule() { + bind(DragDropZonesService); + bind(DragDropDataService); + bind(DragDropConfigService); + bind(DraggableComponent); + bind(DroppableComponent); + bind(SortableComponent); + bind(SortableItemComponent); + bind(DragDropSortableDataService); + } +} diff --git a/lib/dragdrop/draggable.dart b/lib/dragdrop/draggable.dart new file mode 100644 index 0000000..3210662 --- /dev/null +++ b/lib/dragdrop/draggable.dart @@ -0,0 +1,95 @@ +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +part of angular.ui.dragdrop; + +class DragDropConfig extends BaseDDConfig { + String onDragStartClass = "ui-drag-start"; + String onDragEnterClass = "ui-drag-enter"; + String onDragOverClass = "ui-drag-over"; +} + +@Injectable() +class DragDropDataService { + Function onDragSuccessCallback; + var draggableData; +} + +@Injectable() +class DragDropConfigService { + DragDropConfig dragDropConfig = new DragDropConfig(); + SortableConfig sortableConfig = new SortableConfig(); +} + +@Decorator(selector: '[ui-draggable]', + visibility: Directive.CHILDREN_VISIBILITY) +class DraggableComponent extends AbstractDraggableDroppableComponent { + + @NgOneWay("draggable-enabled") + set draggable(bool value) { + if(value!=null) { + dragEnabled = value; + } + } + @NgOneWay("draggable-data") + var draggableData; + + DragDropConfig ddConfig; + + @NgOneWay("ui-draggable") + set dragdropConfig(var config) { + if (!(config is DragDropConfig)) { + return; + } + this.config = ddConfig = config as DragDropConfig; + } + @NgCallback("on-drag-success") + Function onDragSuccessCallback; + + @NgOneWay("allowed-drop-zones") + set dropZones (var dropZones) { + this.dropZoneNames = dropZones; + } + + DragDropDataService dragDropService; + + DraggableComponent(html.Element elem, DragDropZonesService ddZonesService, this.dragDropService, DragDropConfigService dragDropConfigService) + : super(elem, ddZonesService, dragDropConfigService.dragDropConfig) { + dragdropConfig = dragDropConfigService.dragDropConfig; + this.dragEnabled = true; + } + + + @override + void onDragEndCallback(html.Event event) { + dragDropService.draggableData = null; + dragDropService.onDragSuccessCallback = null; + html.Element dragTarget = event.target; + dragTarget.classes.remove(ddConfig.onDragStartClass); + } + + @override + void onDragStartCallback(html.Event event) { + dragDropService.draggableData = draggableData; + dragDropService.onDragSuccessCallback = onDragSuccessCallback; + html.Element dragTarget = event.target; + dragTarget.classes.add(ddConfig.onDragStartClass); + } + +} + +class DraggableElementHandler { + + String defaultCursor; + AbstractDraggableDroppableComponent draggableComponent; + DraggableElementHandler(this.draggableComponent ) { + defaultCursor = draggableComponent.elem.style.cursor; + } + + void refresh() { + draggableComponent.elem.draggable = draggableComponent._dragEnabled; + if (draggableComponent.config.dragCursor!=null) { + draggableComponent.elem.style.cursor = draggableComponent._dragEnabled ? draggableComponent.config.dragCursor : defaultCursor; + } + } +} \ No newline at end of file diff --git a/lib/dragdrop/droppable.dart b/lib/dragdrop/droppable.dart new file mode 100644 index 0000000..a85e4d9 --- /dev/null +++ b/lib/dragdrop/droppable.dart @@ -0,0 +1,64 @@ +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +part of angular.ui.dragdrop; + + +@Decorator(selector: '[ui-droppable]') +class DroppableComponent extends AbstractDraggableDroppableComponent { + + @NgCallback("on-drop-success") + Function onDropSuccessCallback; + + DragDropConfig ddConfig; + + @NgOneWay("ui-droppable") + set dragdropConfig(var config) { + if (!(config is DragDropConfig)) { + return; + } + this.config = ddConfig = config as DragDropConfig; + } + + @NgOneWay("drop-zones") + set dropZones (var dropZoneNames) { + this.dropZoneNames = dropZoneNames; + } + + DragDropDataService dragDropService; + + DroppableComponent(html.Element elem, DragDropZonesService ddZonesService, this.dragDropService, DragDropConfigService dragDropConfigService) + : super(elem, ddZonesService, dragDropConfigService.dragDropConfig) { + dragdropConfig = dragDropConfigService.dragDropConfig; + this.dropEnabled = true; + } + + @override + void onDragEnterCallback(html.Event event) { + elem.classes.add(ddConfig.onDragEnterClass); + } + + @override + void onDragLeaveCallback(html.Event event) { + elem.classes.remove(ddConfig.onDragOverClass); + elem.classes.remove(ddConfig.onDragEnterClass); + } + + @override + void onDragOverCallback(html.Event event) { + elem.classes.add(ddConfig.onDragOverClass); + } + + @override + void onDropCallback(html.Event event) { + if (onDropSuccessCallback!=null) { + onDropSuccessCallback({'data':dragDropService.draggableData}); + } + if(dragDropService.onDragSuccessCallback!=null){ + dragDropService.onDragSuccessCallback(); + } + elem.classes.remove(ddConfig.onDragOverClass); + elem.classes.remove(ddConfig.onDragEnterClass); + } + +} diff --git a/lib/dragdrop/sortable.dart b/lib/dragdrop/sortable.dart new file mode 100644 index 0000000..7ebf2b8 --- /dev/null +++ b/lib/dragdrop/sortable.dart @@ -0,0 +1,139 @@ +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +part of angular.ui.dragdrop; + +class SortableConfig extends BaseDDConfig { + String onSortableDragClass = "ui-sortable-drag"; +} + +@Injectable() +class DragDropSortableDataService { + int index; + List sortableData; + + html.Element _elem; + SortableConfig _config; + void element(html.Element elem, SortableConfig config) { + if (_elem != null) { + _elem.classes.remove(_config.onSortableDragClass); + } + if (elem != null) { + _elem = elem; + _config = config; + _elem.classes.add(_config.onSortableDragClass); + } + } +} + +@Decorator(selector: '[ui-sortable]', + visibility: Directive.CHILDREN_VISIBILITY) +class SortableComponent extends AbstractDraggableDroppableComponent { + + DragDropSortableDataService sortableDataService; + SortableConfig _sortableConfig; + List _sortableData = []; + + @NgTwoWay('ui-sortable-data') + List get sortableData => _sortableData; + set sortableData (var sortableData) { + if (sortableData is List) { + _sortableData = sortableData as List; + onSortableDataChange(); + } + } + + @NgOneWay("ui-sortable") + set sortableConfig(var config) { + if (!(config is SortableConfig)) { + return; + } + this.config = _sortableConfig = config as SortableConfig; + } + + @NgOneWay('ui-sortable-zones') + set sortableZones (var dropZones) { + this.dropZoneNames = dropZones; + } + List get sortableZones => this._dropZoneNames; + + SortableComponent(html.Element elem, DragDropZonesService ddZonesService, DragDropConfigService dragDropConfigService, + this.sortableDataService, Scope scope) + : super(elem, ddZonesService, new BaseDDConfig()) { + this.sortableConfig = dragDropConfigService.sortableConfig; + + scope.watch(elem.attributes['ui-sortable-data'], (oldValue, newValue) { + onSortableDataChange(); + }, collection: true); + + } + + @override + void onDragEnterCallback(html.Event event) { + + _log.finer('drag node [' + sortableDataService.index.toString() + '] over parent node'); + _sortableData.add(sortableDataService.sortableData.removeAt(sortableDataService.index)); + sortableDataService.sortableData = _sortableData; + sortableDataService.index = 0; + + } + + void onSortableDataChange() { + this.dropEnabled = _sortableData.isEmpty; + _log.finer("collection is changed, drop enabled: " + this.dropEnabled.toString()); + } +} + +@Decorator(selector: '[ui-sortable-item]') +class SortableItemComponent extends AbstractDraggableDroppableComponent { + + final SortableComponent sortableComponent; + final DragDropSortableDataService sortableDataService; + + @NgOneWay('ui-sortable-item') + int index; + + SortableItemComponent(this.sortableComponent, this.sortableDataService, html.Element elem, DragDropZonesService ddZonesService, DragDropConfigService ddcService) + : super(elem, ddZonesService, ddcService.sortableConfig) { + this.dropZoneNames = this.sortableComponent.dropZoneNames; + this.dragEnabled = true; + this.dropEnabled = true; + } + + @override + void onDragStartCallback(html.Event event) { + _log.finer('dragging elem with index ' + index.toString()); + sortableDataService.sortableData = sortableComponent._sortableData; + sortableDataService.index = index; + sortableDataService.element(elem, sortableComponent._sortableConfig); + } + + @override + void onDragOverCallback(html.Event event) { + //This is needed to make it working on Firefox. Probably the order the events are triggered is not the same in FF + //and Chrome. + if (elem != sortableDataService._elem) { + sortableDataService.sortableData = sortableComponent._sortableData; + sortableDataService.index = index; + sortableDataService.element(elem, sortableComponent._sortableConfig); + } + } + + @override + void onDragEndCallback(html.Event event) { + sortableDataService.sortableData = null; + sortableDataService.index = null; + sortableDataService.element(null, sortableComponent._sortableConfig); + } + + @override + void onDragEnterCallback(html.Event event) { + sortableDataService.element(elem, sortableComponent._sortableConfig); + if ((index != sortableDataService.index) || (sortableDataService.sortableData != sortableComponent._sortableData)) { + _log.finer('drag node [' + index.toString() + '] over node [' + sortableDataService.index.toString() + ']'); + sortableComponent._sortableData.insert(index, sortableDataService.sortableData.removeAt(sortableDataService.index)); + sortableDataService.sortableData = sortableComponent._sortableData; + sortableDataService.index = index; + } + } +} diff --git a/lib/dropdown/README.md b/lib/dropdown/README.md new file mode 100644 index 0000000..7ebf951 --- /dev/null +++ b/lib/dropdown/README.md @@ -0,0 +1,2 @@ +**DropdownToggle** is a simple directive which will toggle a dropdown link on click. +Simply put it on the `` tag of the toggler-element, and it will find the nearest dropdown menu and toggle it when the `` is clicked. diff --git a/lib/dropdown_toggle.dart b/lib/dropdown/dropdown_toggle.dart similarity index 78% rename from lib/dropdown_toggle.dart rename to lib/dropdown/dropdown_toggle.dart index 27c10aa..1054925 100644 --- a/lib/dropdown_toggle.dart +++ b/lib/dropdown/dropdown_toggle.dart @@ -1,35 +1,35 @@ -// Copyright (c) 2013 - 2014, akserg (Sergey Akopkokhyants) +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. // https://github.com/akserg/angular.dart.ui // All rights reserved. Please see the LICENSE.md file. -// -// Credits: Tonis Pool who wrote and donate that code. -// -library angular.ui.dropdownToggle; +library angular.ui.dropdown_toggle; import 'dart:html' as dom; import "package:angular/angular.dart"; -import "package:angular/utils.dart"; +import "package:angular_ui/utils/utils.dart"; /** * DropdownToggle Module. */ class DropdownToggleModule extends Module { DropdownToggleModule() { - type(DropdownToggle); + bind(DropdownToggle); } } -@NgDirective( +@Decorator( selector: '[dropdown-toggle]' ) -class DropdownToggle { +@Decorator( + selector: '.dropdown-toggle' +) +class DropdownToggle implements ScopeAware { static dom.Element _openElement; static var _closeMenu = (dom.MouseEvent evt) => {}; dom.Element element; Scope scope; - DropdownToggle(this.element, this.scope) { + DropdownToggle(this.element) { this.element.parent.onClick.listen((dom.MouseEvent evt) => _closeMenu(evt)); this.element.onClick.listen(_toggleDropDown); } @@ -49,7 +49,7 @@ class DropdownToggle { _openElement = element; _closeMenu = (dom.MouseEvent event) { if (event != null) { - event.preventDefault(); + //event.preventDefault(); event.stopPropagation(); } element.parent.classes.remove('open'); diff --git a/lib/modal/README.md b/lib/modal/README.md new file mode 100644 index 0000000..5034300 --- /dev/null +++ b/lib/modal/README.md @@ -0,0 +1,20 @@ +**Modal** is a service to quickly create Angular Dart powered modal windows. +Creating custom modals is straightforward: create a partial view, its controller and reference them when using the service. + +The **Modal** service has only one method: `open(options, scope)` where available options are like follows: + +* `templateUrl` - a path to a template representing modal's content +* `template` - inline template representing the modal's content +* `backdrop` - controls presence of a backdrop. Allowed values: true (default), false (no backdrop), `'static'` - backdrop is present but modal window is not closed when clicking outside of the modal window. +* `keyboard` - indicates whether the dialog should be closable by hitting the ESC key, defaults to true +* `windowClass` - additional CSS class(es) to be added to a modal window template +* `size` - optional size of modal window. Allowed values: `'sm'` (small) or `'lg'` (large). Requires Bootstrap 3.1.0 or later + +* `scope` - a scope instance to be used for the modal's content + +The `open` method returns a modal instance, an object with the following properties: + +* `close(result)` - a method that can be used to close a modal, passing a result +* `dismiss(reason)` - a method that can be used to dismiss a modal, passing a reason +* `result` - a promise that is resolved when a modal is closed and rejected when a modal is dismissed +* `opened` - a promise that is resolved when a modal gets opened after downloading content's template and resolving all variables diff --git a/lib/modal/modal.dart b/lib/modal/modal.dart new file mode 100644 index 0000000..5944fc2 --- /dev/null +++ b/lib/modal/modal.dart @@ -0,0 +1,410 @@ +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +library angular.ui.modal; + +import 'dart:async' as async; +import 'dart:html' as dom; +import "package:angular/angular.dart"; +import 'package:angular_ui/utils/timeout.dart'; +import 'package:angular_ui/utils/utils.dart'; + +/** + * Modal Module. + */ +class ModalModule extends Module { + ModalModule() { + install(new TimeoutModule()); + bind(ModalWindow); + bind(Modal); + } +} + +/** + * Modal Window component. + */ +@Component( + selector: 'modal-window', + useShadowDom: false, + //templateUrl: 'packages/angular_ui/modal/window.html' + template: r''' +''' +) +class ModalWindow implements AttachAware { + + @NgAttr('windowClass') + String windowClass = ''; + + @NgOneWay('preventAnimation') + void set visible(bool value) { + if (_modal._top != null) { + _modal._top._visible = value; + } + } + + bool get visible => _modal._top == null ? false : _modal._top._visible; + + @NgOneWay('keyboard') + bool keyboard = true; + + int _index = 0; + @NgOneWay('index') + void set index(int value) { + _index = value; + } + + int get index => _index; + + @NgAttr('backdrop') + void set backdropAsString(String value) { + if (value != null) { + if (value == "static") { + _backdrop = true; + _staticBackdrop = true; + } else { + _backdrop = value == 'true' ? true : false; + _staticBackdrop = false; + } + } + } + + bool _backdrop = true; + + /** If false, clicking the backdrop closes the dialog. */ + bool _staticBackdrop = false; + + String _sizeClass = ''; + @NgAttr('size') + void set size(String value) { + if (value == 'lg' || value == 'sm') { + _sizeClass = 'modal-$value'; + } + } + + String get sizeClass => _sizeClass; + + dom.Element _element; + Modal _modal; + Timeout _timeout; + + ModalWindow(this._element, this._modal, this._timeout); + + void attach() { + if (_element != null) { + // wait 50ms such that .in is added after .fade + _timeout.call(() { + // trigger CSS transitions + visible = true; + // focus a freshly-opened modal + _element.focus(); + }, delay:50); + } + } + + void close(dom.MouseEvent event) { + if(!event.defaultPrevented) { + if (_backdrop && !_staticBackdrop && event.currentTarget == event.target) { + event.preventDefault(); + event.stopPropagation(); + if(_modal._top != null) { + _modal._top.dismiss('backdrop click'); + } + } + if ((event.target as dom.Element).dataset['dismiss'] == 'modal') { + event.preventDefault(); + event.stopPropagation(); + if(_modal._top != null) { + _modal._top.dismiss('dismiss click'); + } + } + } + } +} + +/** + * Modal Options. + */ +class ModalOptions { + String windowClass; + String size; + bool preventAnimation; + bool keyboard; + String backdrop; + String template; + String templateUrl; + + ModalOptions({this.windowClass:'', this.size, this.preventAnimation:false, + this.keyboard:true, this.backdrop:'true', this.template, this.templateUrl}); +} + +/** + * Type definition of close function. + */ +typedef void CloseHandler(result); +/** + * Type definition of dismiss function. + */ +typedef void DismissHandler(String reason); + +/** + * Instance of modal window to manage [result]s and [close] and [dismiss] functions. + */ +class ModalInstance { + dom.Element _backDropElement; + dom.Element _element; + + async.Completer _resultCompleter; + async.Completer _openCompleter; + + async.Future get result => _resultCompleter.future; + async.Future get opened => _openCompleter.future; + + dom.Element get _modalElement => _element.querySelector('.modal'); + + bool get _visible => _modalElement == null ? false : _modalElement.classes.contains('in'); + void set _visible(bool value) { + if (_modalElement != null) { + _modalElement.classes.toggle('in', value); + } + } + + CloseHandler close; + DismissHandler dismiss; +} + +/** + * Modal service. + */ +@Injectable() +class Modal { + static const _backdropClass = 'modal-backdrop'; + static List openedWindows = []; + + Timeout _timeout; + TemplateCache _templateCache; + Http _http; + Compiler _compiler; + DirectiveInjector _injector; + DirectiveMap _directiveMap; + + /** + * Create new instance of Modal service. + */ + Modal(this._compiler, this._timeout, this._templateCache, this._http, this._injector, this._directiveMap); + + /** + * Open new modal window with [options] and optional [scope] and + * returns the [ModalInstance]. + */ + ModalInstance open(ModalOptions options, Scope scope) { + assert(options != null); + + async.Completer resultCompleter = new async.Completer(); + async.Completer openCompleter = new async.Completer(); + + ModalInstance instance = new ModalInstance() + .._resultCompleter = resultCompleter + .._openCompleter = openCompleter + ..close = (result) { close(result); } + ..dismiss = (String reason) { dismiss(reason); }; + + async.Future contentFuture = _getContent(template:options.template, templateUrl:options.templateUrl); + + contentFuture + ..then((String content){ + // Check is content valid from modal-window perspective + if (content.contains('modal-window')) { + throw new Exception("It is not allowing to have modal-window inside othermodal-window" ); + } + // Add ModalWindow wrapper + String html = " rootElements = toNodeList(html); + + instance._element = rootElements.firstWhere((el) { + return el is dom.Element && el.tagName.toLowerCase() == "modal-window"; + }); + // + _compiler(rootElements, _directiveMap)(scope, _injector, rootElements); + // + _show(instance, options); + // + dom.document.body.append(instance._element); + // + openCompleter.complete(true); + }) + ..catchError((error) { + openCompleter.completeError(error); + resultCompleter.completeError(error); + }); + + // Cath exception in safe manner + resultCompleter.future.catchError((error){}); + openCompleter.future.catchError((error){}); + + return instance; + } + + /** + * Prepare [ModalInstance] to show with [options]. + */ + void _show(ModalInstance modalInstance, ModalOptions options) { + modalInstance._backDropElement = _createBackdrop(modalInstance._element.ownerDocument, options.backdrop); + + modalInstance._element.attributes["index"] = "${openedWindows.length}"; + + dom.document.onKeyDown.listen((dom.KeyboardEvent event) { + if(!event.defaultPrevented) { + if (event.keyCode == 27 && modalInstance != null && options.keyboard) { + event.preventDefault(); + event.stopPropagation(); + dismiss("by escape"); + } + } + }); + + if(modalInstance._backDropElement != null) { + // Go backdrop to opaque + modalInstance._backDropElement.classes + ..remove("in") + ..add("fade"); + + _timeout.call((){ + // Add transparancy to backdrop + // Start animation + modalInstance._backDropElement.classes + ..add("in"); + _timeout.call((){ + // Animation is done + modalInstance._backDropElement.classes + ..remove("fade"); + }, delay:250); + }, delay:1); + + modalInstance._backDropElement.onClick.listen((dom.MouseEvent e) { + // Call only backdrop on top element + String backdrop = _top._element.attributes['backdrop']; + if (backdrop != null && backdrop == 'true') { + hide(); + } + }); + } + // + openedWindows.add(modalInstance); + } + + /** + * Hide modal window. Developers must use [close] or [dismiss] methods to + * manage process of closing modal window. + */ + void hide() { + ModalInstance modalInstance = _top; + + if (modalInstance != null) { + // I commented out the statement below because somtimes modalInstance doesn't contain 'in' class. +// if (modalInstance._visible) { + modalInstance._visible = false; + modalInstance._element.attributes.remove("index"); + + if(modalInstance._backDropElement != null) { + modalInstance._backDropElement.classes + ..add('fade') + ..remove('in'); + _timeout.call(() { + modalInstance._backDropElement.remove(); + }, delay:250); + } +// } + openedWindows.remove(modalInstance); + _timeout.call(() { + modalInstance._element.remove(); + modalInstance = null; + }, delay:250); + } + } + + /** + * Close top modal window with [result]. + */ + void close(result) { + ModalInstance modalInstance = _top; + + if (modalInstance != null) { + if (modalInstance._visible) { + modalInstance._resultCompleter.complete(result); + } + } + hide(); + } + + /** + * Dismiss top modal window with optional [reason]. + */ + void dismiss([reason = '']) { + ModalInstance modalInstance = _top; + + if (modalInstance != null) { + if (modalInstance._visible) { + modalInstance._resultCompleter.completeError(reason); + } + } + hide(); + } + + /*****************/ + + /** + * Return topmost [ModalInstance] or null. + */ + ModalInstance get _top => openedWindows.length > 0 ? openedWindows[openedWindows.length - 1] : null; + + /** + * That method creates backdrop on html page depends on value in [addIfMissing] + * which can be equals "true", "false" and "static". + */ + dom.Element _createBackdrop(dom.HtmlDocument parentDocument, String addIfMissing) { + assert(parentDocument != null); + + dom.Element element; + if(addIfMissing == 'static' || addIfMissing == 'true') { + element = new dom.DivElement() + ..style.zIndex = '${1040 + openedWindows.length*10}' + ..classes.add(_backdropClass); + // Temporary workaround https://github.com/akserg/angular.dart.ui/issues/139 + // regards https://github.com/angular-ui/bootstrap/issues/2970 + element.style.height = '${dom.document.body.scrollHeight}px'; + parentDocument.body.append(element); + } + return element; + } + + /*****************/ + + /** + * Get content in future from [template] or loaded from [templateUrl]. + */ + async.Future _getContent({String template:null, String templateUrl:null}) { + if (template == null && templateUrl == null) { + throw new Exception('One of template or templateUrl options is required.'); + } + if (template != null) { + async.Completer def = new async.Completer()..complete(template); + return def.future; + } else { + return _http.get(templateUrl, cache: _templateCache).then((result) => result.data); + } + } +} diff --git a/lib/modal/window.html b/lib/modal/window.html new file mode 100644 index 0000000..f4ef19e --- /dev/null +++ b/lib/modal/window.html @@ -0,0 +1,14 @@ + + + \ No newline at end of file diff --git a/lib/pagination/pager.html b/lib/pagination/pager.html new file mode 100644 index 0000000..f400edd --- /dev/null +++ b/lib/pagination/pager.html @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/lib/pagination/pagination.dart b/lib/pagination/pagination.dart new file mode 100644 index 0000000..6ddc7e0 --- /dev/null +++ b/lib/pagination/pagination.dart @@ -0,0 +1,383 @@ +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +library angular.ui.pagination; + +import 'dart:math' as Math; + +import 'package:angular/angular.dart'; + +import "package:angular_ui/utils/utils.dart"; + +class PaginationModule extends Module { + + PaginationModule() { + bind(PagerConfig, toValue:new PagerConfig(10, '« Previous', 'Next »', true)); + bind(PagerComponent); + bind(PaginationConfig, toValue:new PaginationConfig(10, false, true, 'First', 'Previous', 'Next', 'Last', true)); + bind(PaginationComponent); + bind(PaginationGenerator, toValue: new BasicPaginationGenerator()); + } +} + +@Injectable() +class PagerConfig { + int itemsPerPage; + String previousText; + String nextText; + bool align; + + PagerConfig(this.itemsPerPage, this.previousText, this.nextText, this.align); +} + +@Component( + selector: 'pager[page][total-items]', + //templateUrl: 'packages/angular_ui/pagination/pager.html', + template: ''' +''', + useShadowDom: false, + map: const { + 'page': '<=>currentPage', + 'total-items' : '=>totalItems', + 'items-per-page' : '=>itemsPerPage', + 'num-pages': '&setNumPagesListener', + 'on-select-page': '&onSelectChangeExtEventHandler', + 'align': '@align', + 'previous-text': '@previousText', + 'next-text': '@nextText' + } +) +//@Component( +// selector: '[pager][page][total-items]', +// //templateUrl: 'packages/angular_ui/pagination/pager.html', +// template: ''' +//''', +// useShadowDom: false, +// map: const { +// 'page': '<=>currentPage', +// 'total-items' : '=>totalItems', +// 'items-per-page' : '=>itemsPerPage', +// 'num-pages': '&setNumPagesListener', +// 'on-select-page': '&onSelectChangeExtEventHandler', +// 'align': '@align', +// 'previous-text': '@previousText', +// 'next-text': '@nextText' +// } +//) +class PagerComponent implements ScopeAware { + // Paging always starts with 1st page. + static const int DEFAULT_FIRST_PAGE = 1; + + Scope scope; + final PagerConfig pagerConfig; + + // Bound attributes + //!!! BoundExpression onSelectChangeExtEventHandler; + var onSelectChangeExtEventHandler; + + // Bound attributes store fields + int _currentPage; + int _itemsPerPage; + //!!! BoundExpression _setNumPagesListener; + var _setNumPagesListener; + int _totalItems; + bool _align; + String _previousText; + String _nextText; + + // Computed Fields + int _totalPages; + + + PagerComponent(this.pagerConfig) { + // By default there is one page + _currentPage = DEFAULT_FIRST_PAGE; + _totalPages = DEFAULT_FIRST_PAGE; + + // load default config + _itemsPerPage = pagerConfig.itemsPerPage; + _align = pagerConfig.align; + _previousText = pagerConfig.previousText; + _nextText = pagerConfig.nextText; + } + + int get currentPage => _currentPage; + + set currentPage(value) { + var newIntValue; + try { + newIntValue = toInt(value); + } catch(FormatException){ + newIntValue = 1; + } + if(_currentPage != newIntValue) { + _currentPage = newIntValue; + generatePages(_currentPage, _totalPages); + } + } + + set totalItems(int value) { + var newIntValue = value == null? 0 : value; + if(_totalItems != newIntValue) { + _totalItems = newIntValue; + _reevaluateTotalPages(); + } + } + + set itemsPerPage(int value) { + if(_itemsPerPage != value) { + _itemsPerPage = value; + _reevaluateTotalPages(); + } + } + + set setNumPagesListener(value) { + _setNumPagesListener = value; + + _invokeNumPagesListener(_totalPages); + } + + bool get noPrevious => currentPage <= DEFAULT_FIRST_PAGE; + bool get noNext => currentPage >= _totalPages; + + String get previousText => _previousText; + set previousText(String value) => _previousText = (value == null? pagerConfig.previousText : value); + + String get nextText => _nextText; + set nextText(String value) => _nextText = (value == null? pagerConfig.nextText : value); + + bool get align => _align; + set align(value) => _align = (value == null? pagerConfig.align : toBool(value)); + + int get totalPages => _totalPages; + + void selectPage(int newPage) { + if((newPage >= DEFAULT_FIRST_PAGE) &&(newPage <= _totalPages)) { + scope.apply(() => currentPage = newPage); + + if (onSelectChangeExtEventHandler != null) { + onSelectChangeExtEventHandler(); + } + } + } + + // Do nothing on currentPageChange + generatePages(int currentPage, int totalPages) => null; + + int _calculateTotalPages(int totalItems, int itemsPerPage) { + var totalPages = (totalItems / itemsPerPage).ceil(); + return Math.max(totalPages, 1); + } + + void _reevaluateTotalPages() { + _totalPages = _calculateTotalPages(_totalItems, _itemsPerPage); + _invokeNumPagesListener(_totalPages); + _validateCurrentPage(); + generatePages(currentPage, _totalPages); + } + + void _validateCurrentPage() { + if(currentPage > _totalPages) { + currentPage = _totalPages; + } + } + + void _invokeNumPagesListener(int totalPages) { + if(_setNumPagesListener != null && _setNumPagesListener.expression.isAssignable) { + _setNumPagesListener.assign(totalPages); + } + } +} + +@Injectable() +class PaginationConfig extends PagerConfig { + bool boundaryLinks; + bool directionLinks; + String firstText; + String lastText; + + int maxSize; + + PaginationConfig(int itemsPerPage, this.boundaryLinks, this.directionLinks, this.firstText, String previousText, String nextText, this.lastText, bool align) :super(itemsPerPage, previousText, nextText, align); +} + +@Component( + selector: 'pagination[page][total-items]', + //templateUrl: 'packages/angular_ui/pagination/pagination.html', + template: ''' +''', + useShadowDom: false, + map: const { + 'page': '<=>currentPage', + 'total-items': '=>totalItems', + 'items-per-page': '=>itemsPerPage', + 'max-size': '=>maxSize', + 'rotate': '=>rotate', + 'num-pages': '&setNumPagesListener', + 'on-select-page': '&onSelectChangeExtEventHandler', + 'boundary-links': '=>boundaryLinks', + 'direction-links': '=>directionLinks', + 'align': '@align', + 'previous-text': '@previousText', + 'next-text': '@nextText', + 'first-text': '@firstText', + 'last-text': '@lastText' + } +) +//@Component( +// selector: '[pagination][page][total-items]', +// templateUrl: 'packages/angular_ui/pagination/pagination.html', +// useShadowDom: false, +// map: const { +// 'page': '<=>currentPage', +// 'total-items': '=>totalItems', +// 'items-per-page': '=>itemsPerPage', +// 'max-size': '=>maxSize', +// 'rotate': '=>rotate', +// 'num-pages': '&setNumPagesListener', +// 'on-select-page': '&onSelectChangeExtEventHandler', +// 'boundary-links': '=>boundaryLinks', +// 'direction-links': '=>directionLinks', +// 'align': '@align', +// 'previous-text': '@previousText', +// 'next-text': '@nextText', +// 'first-text': '@firstText', +// 'last-text': '@lastText' +// } +//) +class PaginationComponent extends PagerComponent { + + PaginationConfig paginationConfig; + PaginationGenerator paginationGenerator; + + // Bound attributes + bool boundaryLinks; + bool directionLinks; + List pages; + String _firstText; + String _lastText; + + // Bound attributes store fields + int _maxSize; + bool _rotate; + + + PaginationComponent(PaginationConfig paginationConfig, this.paginationGenerator): super(paginationConfig) { + _rotate = true; + this.paginationConfig = paginationConfig; + + // config + boundaryLinks = paginationConfig.boundaryLinks; + directionLinks = paginationConfig.directionLinks; + _firstText = paginationConfig.firstText; + _lastText = paginationConfig.nextText; + _maxSize = paginationConfig.maxSize; + } + + int get maxSize => _maxSize; + set maxSize(int value) { + _maxSize = value; + _generatePages(currentPage, totalPages, maxSize, rotate); + } + + bool get rotate => _rotate; + set rotate(bool value) { + _rotate = value; + _generatePages(currentPage, totalPages, maxSize, rotate); + } + + String get firstText => _firstText; + set firstText(String value) => _firstText = (value == null? paginationConfig.firstText : value); + + String get lastText => _lastText; + set lastText(String value) => _lastText = (value == null? paginationConfig.lastText : value); + + generatePages(int currentPage, int totalPages) => _generatePages(currentPage, totalPages, maxSize, rotate); + + _generatePages(int currentPage, int totalPages, int maxSize, bool rotate) => pages = paginationGenerator.getPages(currentPage, totalPages, maxSize, rotate); + +} + +@Injectable() +class PageInfo { + int number; + String text; + bool isActive; + + PageInfo(this.number, this.text, this.isActive); +} + + +abstract class PaginationGenerator { + List getPages(int currentPage, int totalPages, int maxSize, bool rotate); + + factory PaginationGenerator() { + return new BasicPaginationGenerator(); + } +} + +@Injectable() +class BasicPaginationGenerator implements PaginationGenerator { + + List getPages(int currentPage, int totalPages, int maxSize, bool rotate) { + var pages = new List(); + + // Default page limits + int startPage = 1, endPage = totalPages; + bool isMaxSized = ( (maxSize != null) && maxSize < totalPages ); + + // recompute if maxSize + if ( isMaxSized ) { + if ( rotate ) { + // Current page is displayed in the middle of the visible ones + startPage = Math.max(currentPage - ((maxSize/2).floor()), 1); + endPage = startPage + maxSize - 1; + + // Adjust if limit is exceeded + if (endPage > totalPages) { + endPage = totalPages; + startPage = endPage - maxSize + 1; + } + } else { + // Visible pages are paginated with maxSize + startPage = maxSize == 0? 0 : ((((currentPage / maxSize).ceil() - 1) * maxSize) + 1); + + // Adjust last page if limit is exceeded + endPage = Math.min(startPage + maxSize - 1, totalPages); + } + } + + // Add page number links + for (var number = startPage; number <= endPage; number++) { + var page = new PageInfo(number, '$number', number == currentPage); + pages.add(page); + } + + // Add links to move between page sets + if ( isMaxSized && ! rotate ) { + if ( startPage > 1 ) { + var previousPageSet = new PageInfo(startPage - 1, '...', false); + pages.insert(0, previousPageSet); + } + + if ( (endPage > 1) && (endPage < totalPages) ) { + var nextPageSet = new PageInfo(endPage + 1, '...', false); + pages.add(nextPageSet); + } + } + + return pages; + } +} \ No newline at end of file diff --git a/lib/pagination/pagination.html b/lib/pagination/pagination.html new file mode 100644 index 0000000..2152363 --- /dev/null +++ b/lib/pagination/pagination.html @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/lib/popover/popover.dart b/lib/popover/popover.dart new file mode 100644 index 0000000..e551337 --- /dev/null +++ b/lib/popover/popover.dart @@ -0,0 +1,81 @@ +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +library angular.ui.popover; + +import 'dart:html' as dom; +import "package:angular/angular.dart"; +import "package:angular/core_dom/module_internal.dart"; +import 'package:angular_ui/tooltip/tooltip.dart'; +import 'package:angular_ui/utils/timeout.dart'; +import 'package:angular_ui/utils/position.dart'; +import 'package:angular_ui/utils/utils.dart'; + +/** + * Popover Module. + */ +class PopoverModule extends Module { + PopoverModule() { + install(new TooltipModule()); + bind(Popover); + } +} + +@Decorator(selector:'[popover]') +//@Component(selector:'popover', useShadowDom: false) +class Popover extends TooltipBase { + + var template = '

'; + + dom.Element _popoverTitle; + dom.DivElement _popoverContent; + + Popover(dom.Element element, NodeAttrs attrs, Timeout timeout, TooltipConfig config, Interpolate interpolate, Position position, Injector injector, Compiler compiler) : + super("popover", 'popover', 'click', element, attrs, timeout, config, interpolate, position, injector, compiler); + + @override + void createTooltip() { + super.createTooltip(); + // + _popoverTitle = tooltip.querySelector(".popover-title"); + _popoverContent = tooltip.querySelector(".popover-content"); + } + + void render() { + tooltip.classes.clear(); + tooltip.classes.add('popover'); + // + var _placement = tt_placement; + if (_placement != null && _placement.length > 0) { + tooltip.classes.add(_placement); + } + // + var _in = toBool(eval(scope, tt_isOpen)); + if (_in) { + tooltip.classes.add('in'); + } + // + var _fade = toBool(eval(scope, tt_animation)); + if (_fade) { + tooltip.classes.add('fade'); + } + // + String txt; + if (_popoverTitle != null) { + txt = tt_title; + if (txt != null && txt.trim().length > 0) { + _popoverTitle.setInnerHtml(txt); + _popoverTitle.classes.remove('ng-hide'); + } else { + _popoverTitle.classes.add('ng-hide'); + } + } else { + _popoverTitle.classes.add('ng-hide'); + } + if (_popoverContent != null) { + txt = tt_content; + _popoverContent.setInnerHtml(txt == null ? '' : txt); + } + } +} + diff --git a/lib/popover/readme.md b/lib/popover/readme.md new file mode 100644 index 0000000..cbac5f3 --- /dev/null +++ b/lib/popover/readme.md @@ -0,0 +1,25 @@ +A lightweight, extensible directive for fancy popover creation. The popover +directive supports multiple placements, optional transition animation, and more. + +Like the Bootstrap jQuery plugin, the popover **requires** the tooltip +module. + +The popover directives provides several optional attributes to control how it +will display: + +- `popover-title`: A string to display as a fancy title. +- `popover-placement`: Where to place it? Defaults to "top", but also accepts + "bottom", "left", "right". +- `popover-animation`: Should it fade in and out? Defaults to "true". +- `popover-popup-delay`: For how long should the user have to have the mouse + over the element before the popover shows (in milliseconds)? Defaults to 0. +- `popover-trigger`: What should trigger the show of the popover? See the + `tooltip` directive for supported values. +- `popover-append-to-body`: Should the tooltip be appended to `$body` instead of + the parent element? + +The popover directives require the `$position` service. + +The popover directive also supports various default configurations through the +$tooltipProvider. See the [tooltip](#tooltip) section for more information. + diff --git a/lib/progressbar/bar.html b/lib/progressbar/bar.html new file mode 100644 index 0000000..e43b1e0 --- /dev/null +++ b/lib/progressbar/bar.html @@ -0,0 +1,9 @@ + + +
+ +
\ No newline at end of file diff --git a/lib/progressbar/progressbar.dart b/lib/progressbar/progressbar.dart new file mode 100644 index 0000000..22dada5 --- /dev/null +++ b/lib/progressbar/progressbar.dart @@ -0,0 +1,229 @@ +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +library angular.ui.progressbar; + +import 'dart:html' as dom; +import 'dart:async' as async; +import "package:angular/angular.dart"; +import "package:angular/core_dom/module_internal.dart"; +import 'package:angular_ui/utils/utils.dart'; +import 'package:angular_ui/utils/transition.dart'; + +/** + * Progressbar Module. + */ +class ProgressbarModule extends Module { + ProgressbarModule() { + install(new TransitionModule()); + bind(ProgressbarConfig, toValue:new ProgressbarConfig(animate:true, max: 100)); + bind(ProgressBar); + bind(Progress); + bind(Bar); + bind(NgPseudo); + } +} + +/** + * Progressbar configuration. + */ +class ProgressbarConfig { + bool animate; + int max; + + ProgressbarConfig({this.animate, this.max}); +} + +@Component( + selector: 'progressbar', + //templateUrl: 'packages/angular_ui/progressbar/progressbar.html', + template: ''' +
+
+ +
+
''', + useShadowDom: false, + map: const { + 'value': '=>value', + 'type': '@type' + }) +//@Component( +// selector: '[progressbar]', +// templateUrl: 'packages/angular_ui/progressbar/progressbar.html', +// publishAs: 'ctrl', +// useShadowDom: false, +// map: const { +// 'value': '=>value', +// 'type': '@type' +// }) +class ProgressBar extends _ProgressbarBase { + ProgressbarConfig _config; + + @NgOneWay("max") + int max; + + @NgOneWay("animate") + bool animate; + + String get type => _type; + set type(val) { _type = val; } + String get classes => _classes; + + set value(int val) { + super.value = val; + } + + ProgressBar(this._config, Transition transistion, dom.Element element) : super(transistion, element); + + evalMaxOrDefault(Scope scope) { + max = (max == null) ? _config.max : toInt(scope.parentScope.eval(max.toString())); + } + + evalAnimateOrDefault(Scope scope) { + animate = (animate == null) ? _config.animate : toBool(scope.parentScope.eval(animate.toString())); + } + + int get computedMax => max; + bool get isAnimate => animate; +} + +@Component( + selector: 'stackedProgress', + useShadowDom: false, +// templateUrl: 'packages/angular_ui/progressbar/stackedProgress.html' + template: ''' +
+ +

''' +) +//@Component( +// selector: '[stackedProgress]', +// useShadowDom: false, +// templateUrl: 'packages/angular_ui/progressbar/stackedProgress.html') +class Progress implements AttachAware, ScopeAware { + Scope scope; + + dom.Element _element; + String classes; + + Progress(this._element); + + void attach() { + this.classes = _element.classes.toString(); + } +} + +@Component( + selector: 'bar', + //templateUrl: 'packages/angular_ui/progressbar/bar.html', + template: ''' +
+ +
''', + useShadowDom: false, + map: const { + 'value': '=>value', + 'type': '@type' + }) +//@Component( +// selector: '[bar]', +// templateUrl: 'packages/angular_ui/progressbar/bar.html', +// publishAs: 'ctrl', +// useShadowDom: false, +// map: const { +// 'value': '=>value', +// 'type': '@type' +// }) +class Bar extends _ProgressbarBase { + ProgressbarConfig _config; + NodeAttrs _parentAttrs; + + int _max; + bool _animate; + + String get type => _type; + set type(val) { _type = val; } + String get classes => _classes; + + Bar(this._config, Transition transistion, dom.Element element) : super(transistion, element); + + evalMaxOrDefault(Scope scope) { + if (_element.parent.attributes.containsKey("max")) { + _max = scope.parentScope.eval(_element.parent.attributes["max"]); + } else { + _max = _config.max; + } + } + evalAnimateOrDefault(Scope scope) { + if (_element.parent.attributes.containsKey("animate")) { + _animate = scope.parentScope.eval(_element.parent.attributes["animate"]); + } else { + _animate = _config.animate; + } + } + + int get computedMax => _max; + bool get isAnimate => _animate; +} + +abstract class _ProgressbarBase implements AttachAware, ScopeAware { + + Scope scope; + dom.Element _element; + Transition _transistion; + + int _value = 0; + int _oldValue = 0; + + String _type; + String _classes; + + _ProgressbarBase(this._transistion, this._element); + + set value(int currenValue) { + _value = currenValue; + if (computedMax != null) { + _update(getProgressBarElement()); + } + } + + int get computedMax; + bool get isAnimate; + + dom.Element getProgressBarElement() => _element.querySelector(".progress-bar"); + + evalMaxOrDefault(Scope scope); + evalAnimateOrDefault(Scope scope); + + void attach() { + _classes = _element.classes.toString(); + new async.Future.delayed(Duration.ZERO, () { + // We need call update on next event-loop iteration when element + // will be really attached. + evalMaxOrDefault(scope); + evalAnimateOrDefault(scope); + _update(getProgressBarElement()); + }); + } + + void _update(dom.Element shadowElement) { + if (_value == null) { + throw new StateError('attribute value is required, add value="{{initialValue}}" to your element: \'${_element.innerHtml}\'!'); + } + + int percent = _getPercentage(_value); + if (isAnimate) { + shadowElement.style.width = _getPercentage(_oldValue).toString() + '%'; + _transistion(shadowElement, {'width': (percent.toString() + '%')}); + } else { + shadowElement.style.width = (percent.toString() + '%'); + shadowElement.style.transition = 'none'; + } + _oldValue = _value; + } + + int _getPercentage(int value) { + return (100 * value / computedMax).round(); + } +} \ No newline at end of file diff --git a/lib/progressbar/progressbar.html b/lib/progressbar/progressbar.html new file mode 100644 index 0000000..408d508 --- /dev/null +++ b/lib/progressbar/progressbar.html @@ -0,0 +1,11 @@ + + +
+
+ +
+
\ No newline at end of file diff --git a/lib/progressbar/readme.md b/lib/progressbar/readme.md new file mode 100644 index 0000000..291f942 --- /dev/null +++ b/lib/progressbar/readme.md @@ -0,0 +1,107 @@ +# Changes in implementations + +Because of the differences between angularjs and angulardart, it is impossible to support +AngularUI progressbar directive the same way in dart. + +## Element +To create stacked progressbars the `` element has been changed to `` + +## CSS +To quickly see the changes needed to bootstrap.css view this [commit](https://github.com/akserg/angular.dart.ui/commit/c0cdbba67286e6e33b5431a7d671d51d24f392ea) + +Because angulardart uses shadow DOM it's impossible to use css selectors for elements from different +shadow DOM-s (ie. `.progress-striped .progress-bar` where the `.progress-striped` element resides in one +shadow DOM and `.progress-bar` in another) + +To compensate this pseudo elements can be used to select elements from shadow dom. This is what we've +done in angular.dart.ui and because of that the standard bootstrap css file progressbar related declarations +need to be changes. For more information about this please see https://github.com/angular/angular.dart/issues/367 + +So to properly use progressbars replace this section in bootstrap.css: + +```css +.progress-striped .progress-bar { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-size: 40px 40px; +} +.progress.active .progress-bar { + -webkit-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} +.progress-bar-success { + background-color: #5cb85c; +} +.progress-striped .progress-bar-success { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.progress-bar-info { + background-color: #5bc0de; +} +.progress-striped .progress-bar-info { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.progress-bar-warning { + background-color: #f0ad4e; +} +.progress-striped .progress-bar-warning { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.progress-bar-danger { + background-color: #d9534f; +} +.progress-striped .progress-bar-danger { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +``` + +with: +```css +.progress-striped .progress-bar, +.progress-striped bar::x-bar.progress-bar { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-size: 40px 40px; +} +.progress.active .progress-bar, +stackedProgress.active bar::x-bar.progress-bar { + -webkit-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} +.progress-bar-success { + background-color: #5cb85c; +} +.progress-striped .progress-bar-success, +.progress-striped bar::x-bar.progress-bar-success { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.progress-bar-info { + background-color: #5bc0de; +} +.progress-striped .progress-bar-info, +.progress-striped bar::x-bar.progress-bar-info { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.progress-bar-warning { + background-color: #f0ad4e; +} +.progress-striped .progress-bar-warning, +.progress-striped bar::x-bar.progress-bar-warning { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.progress-bar-danger { + background-color: #d9534f; +} +.progress-striped .progress-bar-danger, +.progress-striped bar::x-bar.progress-bar-danger { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +``` diff --git a/lib/progressbar/stackedProgress.html b/lib/progressbar/stackedProgress.html new file mode 100644 index 0000000..a32a90c --- /dev/null +++ b/lib/progressbar/stackedProgress.html @@ -0,0 +1,9 @@ + + +
+ +

\ No newline at end of file diff --git a/lib/rating/README.md b/lib/rating/README.md new file mode 100644 index 0000000..6373317 --- /dev/null +++ b/lib/rating/README.md @@ -0,0 +1,37 @@ +**Rating** directive that will take care of visualising a star rating bar. + +### Settings ### + +#### `` #### + + * `ng-model` + : + The current rate. + + * `max` + _(Defaults: 5)_ : + Changes the number of icons. + + * `readonly` + _(Defaults: false)_ : + Prevent user's interaction. + + * `on-hover(value)` + : + An optional expression called when user's mouse is over a particular icon. + + * `on-leave()` + : + An optional expression called when user's mouse leaves the control altogether. + + * `state-on` + _(Defaults: null)_ : + A variable used in template to specify the state (class, src, etc) for selected icons. + + * `state-off` + _(Defaults: null)_ : + A variable used in template to specify the state for unselected icons. + + * `rating-states` + _(Defaults: null)_ : + An array of objects defining properties for all icons. In default template, `stateOn` & `stateOff` property is used to specify the icon's class. diff --git a/lib/rating/rating.dart b/lib/rating/rating.dart new file mode 100644 index 0000000..beff16a --- /dev/null +++ b/lib/rating/rating.dart @@ -0,0 +1,125 @@ +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +library angular.ui.rating; + +import 'dart:html' as dom; +import 'package:angular/angular.dart'; +import "package:angular/core_dom/module_internal.dart"; + +import 'package:angular_ui/utils/extend.dart'; +import 'package:angular_ui/utils/injectable_service.dart'; + +import 'package:logging/logging.dart' show Logger; +final _log = new Logger('angular.ui.rating'); + +class RatingModule extends Module { + RatingModule() { + bind(RatingComponent); + bind(RatingConfig, toValue:new RatingConfig()); + } +} + +@InjectableService() +class RatingConfig { + int max = 5; + String stateOn = null; + String stateOff = null; +} + +@Component( + selector: 'rating[ng-model]', + //templateUrl: 'packages/angular_ui/rating/rating.html', + template: r''' + + +''', + useShadowDom: false +) +//@Component( +// selector: '[rating][ng-model]', +// publishAs: 'ctrl', +// templateUrl: 'packages/angular_ui/rating/rating.html', +// useShadowDom: false +//) +class RatingComponent implements ScopeAware { + + int maxRange = 0; + String stateOn; + String stateOff; + List> range; + int val = 0; + + @NgOneWay('readonly') + bool readonly = false; + + @NgCallback('on-hover') + var onHover; + + @NgCallback('on-leave') + var onLeave; + + dom.Element _element; + NodeAttrs _attrs; + NgModel _ngModel; + RatingConfig _ratingConfig; + + void set scope(Scope scope) { + maxRange = _attrs.containsKey('max') ? scope.parentScope.eval(_attrs['max']) : _ratingConfig.max; + stateOn = _attrs.containsKey('state-on') ? scope.parentScope.eval(_attrs['state-on']) : _ratingConfig.stateOn; + stateOff = _attrs.containsKey('state-off') ? scope.parentScope.eval(_attrs['state-off']) : _ratingConfig.stateOff; + + range = _buildTemplateObjects(_attrs.containsKey('rating-states') ? scope.parentScope.eval(_attrs['rating-states']) : new List(maxRange)); + } + + RatingComponent(this._element, this._attrs, this._ngModel, this._ratingConfig) { + _ngModel.render = (value) { + val = _ngModel.viewValue; + }; + } + + List _buildTemplateObjects(List>states) { + Map defaultOptions = + { + 'stateOn': this.stateOn, + 'stateOff': this.stateOff + }; + + if(states != null) { + for (int i = 0, n = states.length; i < n; i++) { + while(states.length <= i) { + states.add({}); + } + states[i] = extend({ 'index': i }, [states[i], defaultOptions]); + } + } + return states; + } + + void rate(value) { + if (!readonly ) { + _ngModel.viewValue = value; + _ngModel.render(_ngModel.modelValue); + } + } + + void enter(value) { + if (!readonly) { + val = value; + } + onHover({'value': value}); + } + + void reset() { + val = _ngModel.viewValue; + onLeave(); + } + + String stateClass(index, Map r) { + if (index < val) { + return r.containsKey('stateOn') && r['stateOn'] != null ? r['stateOn'] : 'glyphicon-star'; + } else { + return r.containsKey('stateOff') && r['stateOff'] != null ? r['stateOff'] : 'glyphicon-star-empty'; + } + } +} \ No newline at end of file diff --git a/lib/rating/rating.html b/lib/rating/rating.html new file mode 100644 index 0000000..5d9a053 --- /dev/null +++ b/lib/rating/rating.html @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/lib/tabs/README.md b/lib/tabs/README.md new file mode 100644 index 0000000..0392913 --- /dev/null +++ b/lib/tabs/README.md @@ -0,0 +1,39 @@ +The tabs directive. + +### Settings ### + +#### `` #### + + * `vertical` + _(Defaults: false)_ : + Whether tabs appear vertically stacked. + + * `justified` + _(Defaults: false)_ : + Whether tabs fill the container and have a consistent width. + + * `type` + _(Defaults: 'tabs')_ : + Navigation type. Possible values are 'tabs' and 'pills'. + +#### `` #### + + * `heading` or `` + : + Heading text or HTML markup. + + * `active` + _(Defaults: false)_ : + Whether tab is currently selected. + + * `disabled` + _(Defaults: false)_ : + Whether tab is clickable and can be activated. + + * `select()` + _(Defaults: null)_ : + An optional expression called when tab is activated. + + * `deselect()` + _(Defaults: null)_ : + An optional expression called when tab is deactivated. \ No newline at end of file diff --git a/lib/tabs/tab.dart b/lib/tabs/tab.dart new file mode 100644 index 0000000..5aeae72 --- /dev/null +++ b/lib/tabs/tab.dart @@ -0,0 +1,76 @@ +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +part of angular.ui.tabs; + +@Component( + selector: 'tab', +// templateUrl: 'packages/angular_ui/tabs/tab.html', + template: ''' +
+ +
''', + useShadowDom: false +) +class TabComponent implements DetachAware, ScopeAware { + + Scope scope; + + final TabsetComponent tabset; + final Element element; + + //Called in contentHeadingTransclude once it inserts the tab's content into the dom + @NgCallback('select') + var onSelectCallback; + + @NgCallback('deselect') + var onDeselectCallback; + + bool _active = false; + + @NgAttr('heading') + var heading; + + @NgTwoWay('disabled') + bool disabled = false; + + TabComponent(this.element, this.tabset) { + _log.fine('TabComponent'); + this.tabset.addTab(this); + } + + @NgTwoWay('active') + get active => _active; + set active(var newValue) { + _active = newValue; + if (newValue != null && newValue == true) { + tabset.select(this); + } + } + + set select(bool newValue) { + if (newValue && !_active && onSelectCallback != null) { + onSelectCallback(); + } else if(!newValue && _active && onDeselectCallback!=null) { + onDeselectCallback(); + } + _active = newValue; + _refreshTabHeading(); + } + + // Work around for dynamic tab heading issue. It seems to have something to do with the digest cycle + // on tab and the ng-if adding the element back to dom. + void _refreshTabHeading() { + if(this._active && this.heading is Node) { + var clone = this.heading.clone(true); + (this.heading as Node).nodes.clear(); + this.heading = clone; + } + } + + @override + void detach() { + this.tabset.removeTab(this); + } +} + diff --git a/lib/tabs/tab.html b/lib/tabs/tab.html new file mode 100644 index 0000000..a8e2cc6 --- /dev/null +++ b/lib/tabs/tab.html @@ -0,0 +1,9 @@ + + +
+ +
diff --git a/lib/tabs/tab_heading.dart b/lib/tabs/tab_heading.dart new file mode 100644 index 0000000..f7b9502 --- /dev/null +++ b/lib/tabs/tab_heading.dart @@ -0,0 +1,12 @@ +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +part of angular.ui.tabs; + +@Decorator(selector: 'tab-heading') +class TabHeading { + TabHeading(Element elem, TabComponent tab) { + elem.remove(); + tab.heading = elem; + } +} diff --git a/lib/tabs/tabset.dart b/lib/tabs/tabset.dart new file mode 100644 index 0000000..479a239 --- /dev/null +++ b/lib/tabs/tabset.dart @@ -0,0 +1,83 @@ +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +library angular.ui.tabs; + +import 'package:angular/angular.dart'; +import 'dart:html'; +import 'package:logging/logging.dart'; +import 'package:angular_ui/utils/content_append.dart'; + +part 'tab.dart'; +part 'tab_heading.dart'; + +final _log = new Logger('angular.ui.tabs'); + +class TabsModule extends Module { + TabsModule() { + install(new ContentAppendModule()); + bind(TabsetComponent); + bind(TabComponent); + bind(TabHeading); + } +} + +@Component( + selector: 'tabset', +// templateUrl: 'packages/angular_ui/tabs/tabset.html', + template: ''' +
+ +
+ +
+
''', + useShadowDom: false +) +class TabsetComponent implements ScopeAware { + + Scope scope; + + @NgOneWay('justified') + bool justified = false; + + @NgOneWay('vertical') + bool vertical = false; + + @NgOneWay('type') + String type = "tabs"; + + List tabs = []; + + void select(TabComponent tab) { + if (!tab.disabled) { + tabs.forEach((tab) { + tab.select = false; + }); + tab.select = true; + } + } + + void addTab(TabComponent tab) { + tabs.add(tab); + if (tabs.length == 1 || tab.active) { + select(tab); + } + } + + void removeTab(TabComponent tab) { + int index = tabs.indexOf(tab); + //Select a new tab if the tab to be removed is selected + if (tab.active && tabs.length > 1) { + //If this is the last tab, select the previous tab. else, the next tab. + int newActiveIndex = index == tabs.length - 1 ? index - 1 : index + 1; + select(tabs[newActiveIndex]); + } + tabs.remove(tab); + } + +} diff --git a/lib/tabs/tabset.html b/lib/tabs/tabset.html new file mode 100644 index 0000000..c839e8f --- /dev/null +++ b/lib/tabs/tabset.html @@ -0,0 +1,16 @@ + + +
+ +
+ +
+
\ No newline at end of file diff --git a/lib/timepicker/timepicker.dart b/lib/timepicker/timepicker.dart new file mode 100644 index 0000000..ac0a343 --- /dev/null +++ b/lib/timepicker/timepicker.dart @@ -0,0 +1,305 @@ +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +library angular.ui.timepicker; + +import 'dart:html' as dom; +import "package:angular/angular.dart"; +import "package:angular/core_dom/module_internal.dart"; +import 'package:angular_ui/utils/utils.dart'; + +/** + * Timepicker Module. + */ +class TimepickerModule extends Module { + TimepickerModule() { + bind(TimepickerConfig, toValue:new TimepickerConfig()); + bind(Timepicker); + } +} + +class TimepickerConfig { + int hourStep = 1; + int minuteStep = 1; + bool showMeridian = true; + List meridians = null; + bool readonlyInput = false; + bool mousewheel = true; +} + +const List AMPMS = const ['AM','PM']; + +/** + * Timepicker. + */ +@Component( + selector: 'timepicker[ng-model]', +// templateUrl: 'packages/angular_ui/timepicker/timepicker.html', + template: ''' + + + + + + + + + + + + + + + + + + + + + +
 
+ + + : + + +
 
''', + useShadowDom: false +) +//@Component(selector: '[timepicker][ng-model]', publishAs: 't', +// useShadowDom: false, +// templateUrl: 'packages/angular_ui/timepicker/timepicker.html') +class Timepicker implements ShadowRootAware, ScopeAware { + + DateTime selected; + List meridians; + + dom.Element _element; + TimepickerConfig _timepickerConfig; + NodeAttrs _attrs; + NgModel _ngModel; + + Scope scope; + + var hourStep = 0, minuteStep = 0, showMeridian = false, readonlyInput = false, mousewheel = false; + + var invalidHours, invalidMinutes, updateHours, validHours, hours, updateMinutes, minutes, meridian; + + incrementHours () => addMinutes( hourStep * 60 ); + decrementHours () => addMinutes( - hourStep * 60 ); + incrementMinutes () => addMinutes( minuteStep ); + decrementMinutes () => addMinutes( - minuteStep ); + toggleMeridian () => addMinutes(12*60*((selected.hour < 12) ? 1 : -1)); + + Timepicker(this._element, this._timepickerConfig, this._attrs, this._ngModel) { + selected = new DateTime.now(); + } + + void onShadowRoot(dom.ShadowRoot shadowRoot) { + meridians = _attrs.containsKey('meridians') ? scope.parentScope.eval(_attrs['meridians']) : _timepickerConfig.meridians != null ? _timepickerConfig.meridians : AMPMS; + + hourStep = _timepickerConfig.hourStep; + if (_attrs.containsKey('hour-step')) { + scope.parentScope.watch(_attrs['hour-step'], (value, oldValue) { + hourStep = toInt(value); + }); + } + + minuteStep = _timepickerConfig.minuteStep; + if (_attrs.containsKey('minute-step')) { + scope.parentScope.watch(_attrs['minute-step'], (value, oldValue) { + minuteStep = toInt(value); + }); + } + + // 12H / 24H mode + showMeridian = _timepickerConfig.showMeridian; + if (_attrs.containsKey('show-meridian')) { + scope.parentScope.watch(_attrs['show-meridian'], (value, oldValue) { + showMeridian = !!value; + + if (_ngModel.errorStates['time'] != null) { + // Evaluate from template + var hours = getHoursFromTemplate(), + minutes = getMinutesFromTemplate(); + if (hours != null && minutes != null) { + selected = new DateTime(selected.year, selected.month, selected.day, hours, selected.minute, selected.second, selected.millisecond); + refresh(); + } + } else { + _updateTemplate(); + } + }); + } + + // Input elements + List inputs = ngQuery(_element, 'input'); + dom.Element hoursInputEl = inputs[0]; + dom.Element minutesInputEl = inputs[1]; + + // Respond on mousewheel spin + mousewheel = _attrs.containsKey('mousewheel') ? scope.eval(_attrs['mousewheel']) : _timepickerConfig.mousewheel; + if (mousewheel != null) { + + var isScrollingUp = (dom.WheelEvent e) { + return e.deltaX > 0 || e.deltaY < 0; + }; + + hoursInputEl.onMouseWheel.listen((e) { + scope.apply((isScrollingUp(e)) ? incrementHours() : decrementHours()); + e.preventDefault(); + }); + + minutesInputEl.onMouseWheel.listen((e) { + scope.apply((isScrollingUp(e)) ? incrementMinutes() : decrementMinutes()); + e.preventDefault(); + }); + } + + readonlyInput = _attrs.containsKey('readonly-input') ? scope.eval(_attrs['readonly-input']) : _timepickerConfig.readonlyInput; + if (!readonlyInput) { + updateHours = () { + var hours = getHoursFromTemplate(); + + if (hours != null ) { + selected = new DateTime(selected.year, selected.month, selected.day, hours, selected.minute, selected.second, selected.millisecond); + refresh('h'); + } else { + _invalidate(hours: true); + } + }; + + hoursInputEl.addEventListener('blur', (e) { + if (!validHours && hours < 10) { + scope.apply(() { + hours = _pad(hours); + }); + } + }); + + updateMinutes = () { + var minutes = getMinutesFromTemplate(); + + if (minutes != null) { + selected = new DateTime(selected.year, selected.month, selected.day, selected.hour, minutes, selected.second, selected.millisecond); + refresh( 'm' ); + } else { + _invalidate(minutes: true); + } + }; + + minutesInputEl.addEventListener('blur', (e) { + if (!invalidMinutes && minutes < 10 ) { + scope.apply(() { + minutes = _pad(minutes); + }); + } + }); + } else { + updateHours = null; + updateMinutes = null; + } + + _ngModel.render = (value) { + DateTime date = parseDate(_ngModel.modelValue); + if (date == null) { +// _ngModel.setValidity('time', false); +// $log.error('Timepicker 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.'); + } else { + selected = date; + _makeValid(); + _updateTemplate(); + } + }; + } + + _invalidate({bool hours:false, bool minutes:false}) { + _ngModel.viewValue = null; +// _ngModel.setValidity('time', false); + } + + // Get _scope.hours in 24H mode if valid + int getHoursFromTemplate ( ) { + var hours_ = int.parse(hours); + var valid = showMeridian ? (hours_ > 0 && hours_ < 13) : (hours_ >= 0 && hours_ < 24); + if (!valid) { + return null; + } + + if (showMeridian) { + if (hours_ == 12 ) { + hours_ = 0; + } + if (meridian == meridians[1] ) { + hours_ = hours_ + 12; + } + } + return hours_; + } + + int getMinutesFromTemplate() { + var minutes_ = int.parse(minutes); + return (minutes_ >= 0 && minutes_ < 60) ? minutes_ : null; + } + + String _pad(value) { + if (value != null) { + String val = value.toString(); + return val.length < 2 ? '0${value}' : val; + } + return null; + } + + DateTime _parseDate(value) { + if (value != null) { + if (value is DateTime) { + return value; + } else if (value is String) { + return DateTime.parse(value); + } else if (value is int) { + return new DateTime.fromMillisecondsSinceEpoch(value); + } + } + return null; + } + + // Call internally when we know that model is valid. + void refresh([keyboardChange = null]) { + _makeValid(); + _ngModel.viewValue = _parseDate(selected); + _updateTemplate(keyboardChange); + } + + void _makeValid() { +// _ngModel.setValidity('time', true); + invalidHours = false; + invalidMinutes = false; + } + + void _updateTemplate([keyboardChange = null]) { + var hours = selected.hour; + var minutes = selected.minute; + + if (toBool(showMeridian)) { + hours = (hours == 0 || hours == 12) ? 12 : hours % 12; // Convert 24 to 12 hour system + } + hours = keyboardChange == 'h' ? hours : _pad(hours); + minutes = keyboardChange == 'm' ? minutes : _pad(minutes); + meridian = selected.hour < 12 ? meridians[0] : meridians[1]; + + dom.InputElement hoursEl = ngQuery(_element, "#hours").first; + if (hoursEl != null) { + hoursEl.value = hours; + } + dom.InputElement minutesEl = ngQuery(_element, "#minutes").first; + if (minutesEl != null) { + minutesEl.value = minutes; + } + } + + void addMinutes( minutes ) { + var dt = _parseDate(selected.millisecondsSinceEpoch + minutes * 60000); + selected = new DateTime(selected.year, selected.month, selected.day, dt.hour, dt.minute, selected.second, selected.millisecond); + refresh(); + } +} \ No newline at end of file diff --git a/lib/timepicker/timepicker.html b/lib/timepicker/timepicker.html new file mode 100644 index 0000000..03a7d71 --- /dev/null +++ b/lib/timepicker/timepicker.html @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + +
 
+ + + : + + +
 
diff --git a/lib/tooltip/readme.md b/lib/tooltip/readme.md new file mode 100644 index 0000000..e7e494f --- /dev/null +++ b/lib/tooltip/readme.md @@ -0,0 +1,54 @@ +A lightweight, extensible directive for fancy tooltip creation. The tooltip +directive supports multiple placements, optional transition animation, and more. + +There are two versions of the tooltip: `tooltip` and `tooltip-html-unsafe`. The +former takes text only and will escape any HTML provided. The latter takes +whatever HTML is provided and displays it in a tooltip; it called "unsafe" +because the HTML is not sanitized. *The user is responsible for ensuring the +content is safe to put into the DOM!* + +The tooltip directives provide several optional attributes to control how they +will display: + +- `tooltip-placement`: Where to place it? Defaults to "top", but also accepts + "bottom", "left", "right". +- `tooltip-animation`: Should it fade in and out? Defaults to "true". +- `tooltip-popup-delay`: For how long should the user have to have the mouse + over the element before the tooltip shows (in milliseconds)? Defaults to 0. +- `tooltip-trigger`: What should trigger a show of the tooltip? +- `tooltip-append-to-body`: Should the tooltip be appended to `$body` instead of + the parent element? + +The tooltip directives require the `$position` service. + +**Triggers** + +The following show triggers are supported out of the box, along with their +provided hide triggers: + +- `mouseenter`: `mouseleave` +- `click`: `click` +- `focus`: `blur` + +For any non-supported value, the trigger will be used to both show and hide the +tooltip. + +**$tooltipProvider** + +Through the `$tooltipProvider`, you can change the way tooltips and popovers +behave by default; the attributes above always take precedence. The following +methods are available: + +- `setTriggers( obj )`: Extends the default trigger mappings mentioned above + with mappings of your own. E.g. `{ 'openTrigger': 'closeTrigger' }`. +- `options( obj )`: Provide a set of defaults for certain tooltip and popover + attributes. Currently supports 'placement', 'animation', 'popupDelay', and + `appendToBody`. Here are the defaults: + +
+  placement: 'top',
+  animation: true,
+  popupDelay: 0,
+  appendToBody: false
+  
+ diff --git a/lib/tooltip/tooltip.dart b/lib/tooltip/tooltip.dart new file mode 100644 index 0000000..3adf6f9 --- /dev/null +++ b/lib/tooltip/tooltip.dart @@ -0,0 +1,417 @@ +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +library angular.ui.tooltip; + +import 'dart:html' as dom; +import 'dart:async' as async; +import "package:angular/angular.dart"; +import "package:angular/core_dom/module_internal.dart"; +import 'package:angular_ui/utils/timeout.dart'; +import 'package:angular_ui/utils/position.dart'; +import 'package:angular_ui/utils/utils.dart'; + +/** + * Tooltip Module. + */ +class TooltipModule extends Module { + TooltipModule() { + install(new TimeoutModule()); + install(new PositionModule()); + bind(TooltipConfig, toValue:new TooltipConfig()); + bind(Tooltip); + } +} + +@Injectable() +class TooltipConfig { + // The default options tooltip and popover. + Map defaultOptions = { + 'placement': 'top', + 'animation': true, + 'popupDelay': 0 + }; + + // Default hide triggers for each show trigger + Map triggerMap = { + 'mouseenter': 'mouseleave', + 'click': 'click', + 'focus': 'blur' + }; + + // The options specified to the provider globally. + Map globalOptions = {}; + + /** + * `options({})` allows global configuration of all tooltips in the + * application. + * + * var app = angular.module( 'App', ['ui.bootstrap.tooltip'], function( $tooltipProvider ) { + * // place tooltips left instead of top by default + * $tooltipProvider.options( { placement: 'left' } ); + * }); + */ + void set options( value ) { + globalOptions = value; + } + + /** + * This allows you to extend the set of trigger mappings available. E.g.: + * + * $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' ); + */ + void set triggers(value) { + triggerMap = value; + } + + /** + * This is a helper function for translating camel-case to snake-case. + */ + String snakeCase(String name){ + var regexp = new RegExp('/[A-Z]/'); + var separator = '-'; +// return name.replace(regexp, (letter, pos) { +// return (pos ? separator : '') + letter.toLowerCase(); +// }); + return name.replaceAllMapped(regexp, (Match match) { + return (match != null ? separator : '') + match.group(0).toLowerCase(); + }); + } +} + +@Component( + selector:'[tooltip]', + useShadowDom: false +) +//@Decorator(selector:'tooltip') +class Tooltip extends TooltipBase { + + var template = '
'; + + Tooltip(dom.Element element, NodeAttrs attrs, Timeout timeout, TooltipConfig config, Interpolate interpolate, Position position, Injector injector, Compiler compiler) : + super("tooltip", 'tooltip', 'mouseenter', element, attrs, timeout, config, interpolate, position, injector, compiler); + + void render() { + tooltip.classes.clear(); + tooltip.classes.add('tooltip'); + // + var _placement = tt_placement; + if (_placement != null && _placement.length > 0) { + tooltip.classes.add(_placement); + } + // + var _in = toBool(eval(_scope, tt_isOpen)); + if (_in) { + tooltip.classes.add('in'); + } + // + var _fade = toBool(eval(_scope, tt_animation)); + if (_fade) { + tooltip.classes.add('fade'); + } + // + dom.Element innerTooltip = tooltip.children[1]; + innerTooltip.setInnerHtml(tt_content); + } +} + +/** + * Base class for all tooltipable components. + */ +abstract class TooltipBase implements ScopeAware { + + Map options; + + var directiveName; + + var template; + + dom.Element tooltip; + + var transitionTimeout; + async.Completer popupTimeout; + bool appendToBody = false; + Map triggers; + bool hasEnableExp = false; + + Scope _scope; + dom.Element _element; + NodeAttrs _attrs; + + Timeout _timeout; + TooltipConfig _config; + String defaultTriggerShow; + String _type; + Interpolate _interpolate; + String prefix = ''; + Position _position; + Injector _injector; + Compiler _compiler; + + var tt_isOpen, tt_content, tt_title, tt_placement, tt_popupDelay, tt_animation; + + TooltipBase(this._type, this.prefix, this.defaultTriggerShow, + this._element, this._attrs, this._timeout, this._config, + this._interpolate, this._position, this._injector, this._compiler); + + Scope get scope => _scope; + void set scope(Scope scope) { + _scope = scope; + + options = new Map.from(_config.defaultOptions)..addAll(_config.globalOptions); + + directiveName = _config.snakeCase(_type); + + appendToBody = options.containsKey('append-to-body') ? options['append-to-body'] : false; + triggers = getTriggers(); + hasEnableExp = _attrs.containsKey('${prefix}-enable'); + + // By default, the tooltip is not open. + // TODO add ability to start tooltip opened + tt_isOpen = false; + + /** + * Observe the relevant attributes. + */ + _attrs.observe(_type, (val) { + tt_content = val; + + if (val == null && tt_isOpen) { + hide(); + } + }); + + _attrs.observe('${prefix}-title', (val) { + tt_title = val; + }); + + _attrs.observe('${prefix}-placement', (val) { + tt_placement = val != null ? val : options['placement']; + }); + + _attrs.observe('${prefix}-popup-delay', (val) { + var delay = eval(_scope, val, options['popupDelay']); + tt_popupDelay = delay is int ? delay : options['popupDelay']; + }); + + _attrs.observe('${prefix}-trigger', (val) { + unregisterTriggers(); + + triggers = getTriggers( val ); + + if (triggers['show'] == triggers['hide'] ) { + _element.addEventListener(triggers['show'], toggleTooltipBind); + } else { + _element.addEventListener(triggers['show'], showTooltipBind ); + _element.addEventListener(triggers['hide'], hideTooltipBind ); + } + }); + + bool attrAnimation = toBool(_scope.eval(_attrs['${prefix}-animation'])); + tt_animation = attrAnimation != null ? attrAnimation : options['animation']; + + _attrs.observe('${prefix}-append-to-body', (val) { + appendToBody = val != null ? _scope.eval(val) : appendToBody; + }); + + // if a tooltip is attached to we need to remove it on + // location change as its parent scope will probably not be destroyed + // by the change. + if (appendToBody) { + _scope.on('locationChangeSuccess').listen((evt) { + if (tt_isOpen) { + hide(); + } + }); + } + // Make sure tooltip is destroyed and removed. + _scope.on(ScopeEvent.DESTROY).listen((evt) { + _timeout.cancel(transitionTimeout); + _timeout.cancel(popupTimeout); + unregisterTriggers(); + removeTooltip(); + }); + } + + /** + * Returns an object of show and hide triggers. + * + * If a trigger is supplied, + * it is used to show the tooltip; otherwise, it will use the `trigger` + * option passed to the `$tooltipProvider.options` method; else it will + * default to the trigger supplied to this directive factory. + * + * The hide trigger is based on the show trigger. If the `trigger` option + * was passed to the `$tooltipProvider.options` method, it will use the + * mapped trigger from `triggerMap` or the passed trigger if the map is + * undefined; otherwise, it uses the `triggerMap` value of the show + * trigger; else it will just use the show trigger. + */ + Map getTriggers([String trigger = null]) { + String show = trigger != null ? trigger : options.containsKey('trigger') ? options['trigger'] : defaultTriggerShow; + String hide = _config.triggerMap.containsKey(show) ? _config.triggerMap[show] : show; + return { + 'show': show, + 'hide': hide + }; + } + + // Show the tooltip popup element. + void show() { + + String scopeContent = tt_content; + // Don't show empty tooltips. + if (scopeContent == null || scopeContent.length == 0 ) { + return null; //angular.noop; + } + + createTooltip(); + + // If there is a pending remove transition, we must cancel it, lest the + // tooltip be mysteriously removed. + if (transitionTimeout != null) { + _timeout.cancel(transitionTimeout); + } + + // Set the initial positioning. + tooltip.style.top = "0"; + tooltip.style.left = "0"; + tooltip.style.display = 'inline'; + + // Now we add it to the DOM because need some info about it. But it's not + // visible yet anyway. + if (appendToBody) { + dom.document.body.append(tooltip); + } else { + _element.parent.append(tooltip); + } + + positionTooltip(); + + // And show the tooltip. + tt_isOpen = true; + _scope.apply(); // digest required as $apply is not called + + render(); + + positionTooltip(); + } + + // Hide the tooltip popup element. + void hide() { + // First things first: we don't show it anymore. + tt_isOpen = false; + + //if tooltip is going to be shown after delay, we must cancel this + _timeout.cancel(popupTimeout); + + // And now we remove it from the DOM. However, if we have animation, we + // need to wait for it to expire beforehand. + // FIXME: this is a placeholder for a port of the transitions library. + if (tt_animation) { + transitionTimeout = _timeout(removeTooltip, delay:500); + } else { + removeTooltip(); + } + } + + void createTooltip() { + // There can only be one tooltip element per directive shown at once. + removeTooltip(); + tooltip = compile(template, _injector, _compiler); + } + + void removeTooltip() { + if (tooltip != null) { + tooltip.remove(); + tooltip = null; + } + } + + + // Show the tooltip with delay if specified, otherwise show it immediately + void showTooltipBind([dom.Event evt]) { + if(hasEnableExp && !_scope.eval(_attrs['${prefix}-enable'])) { + return; + } + + int delay = tt_popupDelay; + if (delay != null && delay > 0) { + popupTimeout = _timeout(show, delay:delay, invokeApply:false); + } else { + show(); + } + } + + void hideTooltipBind([dom.Event evt]) { + _scope.apply(() { + hide(); + }); + } + + + void toggleTooltipBind([dom.Event evt]) { + if (!tt_isOpen) { + showTooltipBind(); + } else { + hideTooltipBind(); + } + } + + void positionTooltip() { + int ttWidth, ttHeight; + Map ttPosition; + // Get the position of the directive element. + //position = appendToBody ? _position.offset(_element) : _position.position(_element); + dom.Rectangle position = appendToBody ? _element.offset : _element.offset; + + // Get the height and width of the tooltip so we can center it. + ttWidth = tooltip.offsetWidth; + ttHeight = tooltip.offsetHeight; + + // Calculate the tooltip's top and left coordinates to center it with + // this directive. + switch (tt_placement) { + case 'right': + ttPosition = { + 'top': position.top + (position.height - ttHeight) / 2, + 'left': position.left + position.width + }; + break; + case 'bottom': + ttPosition = { + 'top': position.top + position.height, + 'left': position.left + (position.width - ttWidth) / 2 + }; + break; + case 'left': + ttPosition = { + 'top': position.top + (position.height - ttHeight) / 2, + 'left': position.left - ttWidth + }; + break; + default: + ttPosition = { + 'top': position.top - ttHeight, + 'left': position.left + (position.width - ttWidth) / 2 + }; + break; + } + + // Now set the calculated positioning. + tooltip.style.top = "${ttPosition['top']}px"; + tooltip.style.left = "${ttPosition['left']}px"; + } + + void unregisterTriggers() { + if (_element != null) { + _element.removeEventListener(triggers['show'], showTooltipBind); + _element.removeEventListener(triggers['hide'], hideTooltipBind); + } + } + + /** + * Rendering function. + */ + void render(); +} \ No newline at end of file diff --git a/lib/transition.dart b/lib/transition.dart deleted file mode 100644 index ecfc87c..0000000 --- a/lib/transition.dart +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (c) 2013 - 2014, akserg (Sergey Akopkokhyants) -// https://github.com/akserg/angular.dart.ui -// All rights reserved. Please see the LICENSE.md file. -library angular.ui.transition; - -import 'dart:html' as dom; -import 'dart:async' as async; -import "package:angular/angular.dart"; - -import 'timeout.dart'; - -/** - * Transition Module. - */ -class TransitionModule extends Module { - TransitionModule() { - install(new TimeoutModule()); - type(Transition); - } -} - -class Transition { - - // Work out the name of the transitionEnd event - var _transElement = new dom.DivElement(); - var _transitionEndEventNames = { - 'WebkitTransition': 'webkitTransitionEnd', - 'MozTransition': 'transitionend', - 'OTransition': 'oTransitionEnd', - 'transition': 'transitionend' - }; - var _animationEndEventNames = { - 'WebkitTransition': 'webkitAnimationEnd', - 'MozTransition': 'animationend', - 'OTransition': 'oAnimationEnd', - 'transition': 'animationend' - }; - - var transitionEndEventName, animationEndEventName; - - Timeout timeout; - - Transition(this.timeout) { - transitionEndEventName = _findEndEventName(_transitionEndEventNames); - animationEndEventName = _findEndEventName(_animationEndEventNames); - } - - async.Completer call(dom.Element element, trigger, {Map options:null}) { - options = options != null ? options : {}; - // - async.Completer deferred = new async.Completer(); - // - var endEventName = options.containsKey('animation') ? animationEndEventName : transitionEndEventName; - // - dom.EventListener transitionEndHandler; - transitionEndHandler = (dom.Event event) { - element.removeEventListener(endEventName, transitionEndHandler); - if (!deferred.isCompleted) { - deferred.complete(element); - } - }; - - if (endEventName != null) { - element.addEventListener(endEventName, transitionEndHandler); - } - - // Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur - //new async.Timer(const Duration(milliseconds: 200), () { - timeout(() { - if (trigger is String) { - element.classes.add(trigger); - } else if (trigger is Function) { - trigger(element); - } else if (trigger is Map) { - trigger.forEach((propertyName, value) => element.style.setProperty(propertyName, value)); - } - //If browser does not support transitions, instantly resolve - if (endEventName == null && !deferred.isCompleted) { - deferred.complete(element); - } - }); - - // Add our custom cancel function to the promise that is returned - // We can call this if we are about to run a new transition, which we know will prevent this transition from ending, - // i.e. it will therefore never raise a transitionEnd event for that transition - deferred.future.catchError((error) { - if (endEventName != null) { - element.removeEventListener(endEventName, transitionEndHandler); - } - if (!deferred.isCompleted) { - deferred.completeError('Transition cancelled'); - } - }); - - return deferred; - } - - String _findEndEventName(endEventNames) { - endEventNames.forEach((k, v) { - if (_transElement.style.getPropertyValue(k) != '') { - return v; - } - }); - return null; - } -} - diff --git a/lib/typeahead/module.dart b/lib/typeahead/module.dart new file mode 100644 index 0000000..32b3059 --- /dev/null +++ b/lib/typeahead/module.dart @@ -0,0 +1,29 @@ +library angular.ui.typeahead; + +import 'dart:html' as dom; +import 'dart:async'; +import 'dart:math'; + +import 'package:angular/angular.dart'; +import 'package:angular/core/module_internal.dart'; +import 'package:angular/core/parser/syntax.dart' show Expression; + +import 'package:angular_ui/utils/utils.dart'; +import 'package:angular_ui/utils/position.dart'; + +part 'typeahead_parser.dart'; +part 'typeahead_highlight.dart'; +part 'typeahead_popup.dart'; +part 'typeahead_decorator.dart'; + +class TypeaheadModule extends Module { + + TypeaheadModule() { + install(new PositionModule()); + bind(TypeaheadParser); + bind(TypeaheadHighlightFilter); + bind(TypeaheadMatch); + bind(TypeaheadPopup); + bind(TypeaheadDecorator); + } +} \ No newline at end of file diff --git a/lib/typeahead/typeahead-match.html b/lib/typeahead/typeahead-match.html new file mode 100644 index 0000000..571f876 --- /dev/null +++ b/lib/typeahead/typeahead-match.html @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/lib/typeahead/typeahead-popup.html b/lib/typeahead/typeahead-popup.html new file mode 100644 index 0000000..fe8df71 --- /dev/null +++ b/lib/typeahead/typeahead-popup.html @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/lib/typeahead/typeahead.html b/lib/typeahead/typeahead.html new file mode 100644 index 0000000..87a1019 --- /dev/null +++ b/lib/typeahead/typeahead.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/lib/typeahead/typeahead_decorator.dart b/lib/typeahead/typeahead_decorator.dart new file mode 100644 index 0000000..e98dd15 --- /dev/null +++ b/lib/typeahead/typeahead_decorator.dart @@ -0,0 +1,287 @@ +part of angular.ui.typeahead; + +@Decorator( + selector : 'input[typeahead][ng-model]', + map: const { + 'typeahead': '@expression', + 'typeahead-template-url': '@templateUrl', + 'typeahead-min-length': '@minLength', + 'typeahead-append-to-body': '@appendToBody', + 'typeahead-input-formatter': '&inputFormatter', + 'typeahead-wait-ms': '@waitInMs', + 'typeahead-loading': '<=>isLoading', + 'typeahead-on-select': '&onSelectCallback', + 'typeahead-editable': '@isEditable' + } +) +class TypeaheadDecorator extends TemplateBasedComponent implements AttachAware, ScopeAware { + + final TypeaheadParser _typeaheadParser; + final NgModel _ngModel; + Scope _scope; + dom.InputElement _element; + + final FormatterMap _formatters; + final Injector _injector; + final Position _positionService; + + String templateUrl; + int _minLength; + int _waitInMs; + bool _isEditable; + bool _appendToBody; + Function inputFormatter; + bool _isInputFormatterEnabled; + bool isLoading = false; + Function onSelectCallback; + + String popupId; + + TypeaheadParseResult _typeaheadParserResult; + + var keyMappings; + bool _hasFocus; + + StreamSubscription _clickSubscription; + Timer _matchesLookupTimer; + + // Popup specific params; + List matches = []; + int _active = -1; + Rect position; + String query; + + TypeaheadDecorator(this._ngModel, this._injector, dom.Element element, this._typeaheadParser, this._formatters, ViewFactoryCache viewCache, this._positionService) : + super(viewCache) { + this._element = element as dom.InputElement; + keyMappings = {dom.KeyCode.ENTER : _onKeyPressEnter, dom.KeyCode.TAB : _onKeyPressEnter, dom.KeyCode.DOWN : _onKeyPressDown, dom.KeyCode.UP : _onKeyPressUp, dom.KeyCode.ESC : _onKeyPressEsc}; + + _isInputFormatterEnabled = _element.getAttribute('typeahead-input-formatter') != null; + + active = -1; + } + + void set scope(Scope scope) { + _scope = scope; + popupId = 'typeahead-${_scope.id}-${new Random().nextInt(10000)}'; + _element.attributes.addAll({'aria-autocomplete': 'list', 'aria-expanded': 'false', 'aria-owns': popupId}); + } + + set expression(value) { + assert(value != null); + + _typeaheadParserResult = _typeaheadParser.parse(value); + var formatterFunc = (modelValue) { + + if(_isInputFormatterEnabled) { + return inputFormatter({r'$model' : modelValue}); + } else { + try { + var locals = { + _typeaheadParserResult.itemName : modelValue + }; + return eval(_typeaheadParserResult.viewMapper, locals); + } catch(e, s) { + return modelValue; + } + } + }; + _ngModel.converter = new TypeaheadConverter(formatterFunc); + } + + set minLength(value) => _minLength = (value == null) ? 1 : toInt(value); + set waitInMs(value) => _waitInMs = (value == null) ? 0 : toInt(value); + set isEditable(value) => _isEditable = (value == null) ? true : toBool(value); + set appendToBody(value) => _appendToBody = (value == null) ? false : toBool(value); + + int get active => _active; + set active(int value) { + _active = value; + if (value < 0) { + _element.attributes.remove('aria-activedescendant'); + } else { + _element.attributes['aria-activedescendant'] = _getMatchItemId(value); + } + } + + void select(int index) { + + var item = matches[index]['model']; + var locals = {_typeaheadParserResult.itemName : item}; + var model = eval(_typeaheadParserResult.modelMapper, locals); + _scope.apply((){ + _ngModel.setter(model); + _ngModel.removeError('ng-editable'); + _resetMatches(); + }); + + onSelectCallback({r'$item' : item, r'$model' : model, r'$label': eval(_typeaheadParserResult.viewMapper,locals)}); + + new Future.microtask(()=> _element.focus()); + } + + void attach() { + _element + ..onChange.listen(_onValueChanged) + ..onCut.listen(_onValueChanged) + ..onPaste.listen(_onValueChanged) + ..onInput.listen(_onValueChanged); + + _element.onKeyDown.listen(_onKeyPress); + + _element.onBlur.listen((event) => _hasFocus = false); + + _clickSubscription = dom.document.onClick.listen((event){ + if(_element != event.target) { + _scope.apply(()=> _resetMatches()); + } + }); + } + + void detach() { + _clickSubscription.cancel(); + super.detach(); + } + + eval(expression, locals) => expression.eval(new ContextLocals(_scope.context, locals), _formatters); + + _scheduleSearchWithTimeout(inputValue) { + _matchesLookupTimer = new Timer(new Duration(milliseconds : _waitInMs), ()=>_getMatchesAsync(inputValue)); + } + + _cancelPreviousTimeout() { + if(_matchesLookupTimer != null) { + _matchesLookupTimer.cancel(); + } + _matchesLookupTimer = null; + } + + void _onValueChanged(dom.Event event) { + + _hasFocus = true; + + String inputValue = _element.value; + + if(inputValue != null || inputValue.length >= _minLength) { + if(_waitInMs != 0) { + _cancelPreviousTimeout(); + _scheduleSearchWithTimeout(inputValue); + } else { + _getMatchesAsync(inputValue); + } + } else { + _scope.apply(() { + isLoading = false; + _cancelPreviousTimeout(); + _resetMatches(); + }); + } + + if(!_isEditable) { + if(inputValue == null || inputValue.isEmpty) { + _ngModel.removeError('ng-editable'); + } else { + _ngModel.addError('ng-editable'); + } + } + } + + void _onKeyPress(dom.KeyboardEvent event) { + + if(matches.length == 0 || !keyMappings.containsKey(event.keyCode)) { + return; + } + + event.preventDefault(); + + keyMappings[event.keyCode](event); + } + + void _onKeyPressEnter(dom.KeyboardEvent event) { + select(active); + } + + void _onKeyPressDown(dom.KeyboardEvent event) { + _scope.apply(() => active = (active + 1) % matches.length); + } + + void _onKeyPressUp(dom.KeyboardEvent event) { + _scope.apply(() => active = (active ==0? matches.length : active) - 1); + } + + void _onKeyPressEsc(dom.KeyboardEvent event) { + event.stopPropagation(); + _scope.apply(() => _resetMatches()); + } + + String _getMatchItemId(int index) => '${popupId}-option-$index'; + + void _getMatchesAsync(String inputValue) { + isLoading = true; + + var func = () { + return eval(_typeaheadParserResult.source, {r'$viewValue': inputValue}); + }; + + new Future(func).then((matches){ + var onCurrentRequest = (inputValue == _ngModel.viewValue); + if(onCurrentRequest && _hasFocus) { + if (matches.length > 0) { + //_scope.apply(() => _updatePopup(inputValue, matches)); + _updatePopup(inputValue, matches); + } else { + //_scope.apply(() => _resetMatches()); + _resetMatches(); + } + } + if(onCurrentRequest) + isLoading = false; + }).catchError((error){ + _resetMatches(); + isLoading = false; + }); + } + + void _updatePopup(String inputValue, Iterable values) { + + if(_view == null) { + loadView(_appendToBody? dom.document.body :_element.parent, _injector, _scope, 'packages/angular_ui/typeahead/typeahead.html', this); + } + + active = 0; + query = inputValue; + + matches.clear(); + for(var index = 0; index < values.length; ++index) { + var item = values.elementAt(index); + matches.add({'id':_getMatchItemId(index), 'label':eval(_typeaheadParserResult.viewMapper, {_typeaheadParserResult.itemName: item}), 'model':item}); + } + + //position pop-up with matches - we need to re-calculate its position each time we are opening a window + //with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page + //due to other elements being rendered + position = _appendToBody? _positionService.offset(_element) : _positionService.position(_element); + position.top += _element.offsetHeight; + + _element.attributes['aria-expanded'] = 'true'; + } + + void _resetMatches() { + matches.clear(); + active = -1; + _element.attributes['aria-expanded'] = 'false'; + } + +} + +typedef FormatterFunc(value); + +@Injectable() +class TypeaheadConverter extends NgModelConverter { + final name = 'typeahead'; + final FormatterFunc _formatter; + + TypeaheadConverter(this._formatter); + + format(value) => _formatter(value); +} \ No newline at end of file diff --git a/lib/typeahead/typeahead_highlight.dart b/lib/typeahead/typeahead_highlight.dart new file mode 100644 index 0000000..f5a5728 --- /dev/null +++ b/lib/typeahead/typeahead_highlight.dart @@ -0,0 +1,28 @@ +part of angular.ui.typeahead; + +@Formatter(name : 'highlight') +class TypeaheadHighlightFilter implements Function { + + escapeRegexp(String queryToEscape) { + var result = queryToEscape.replaceAllMapped(new RegExp(r'([.?*+^$[\]\\(){}|-])'), (Match m)=> '\\${m[0]}'); + return result; + } + + String call(String matchItem, String query) { + if (query == null || query.isEmpty) { + return matchItem; + } else { + var queryArray = escapeRegexp(query).split(" "); + StringBuffer queryString = new StringBuffer(); + if (queryArray.length > 1) { + queryString.writeAll(queryArray,'|'); + } else { + queryString.write(queryArray[0]); + } + return ('$matchItem') + .replaceAllMapped(new RegExp( + queryString.toString().trim(), caseSensitive: false), ( + Match m) => '${m[0]}'); + } + } +} \ No newline at end of file diff --git a/lib/typeahead/typeahead_parser.dart b/lib/typeahead/typeahead_parser.dart new file mode 100644 index 0000000..b69d9f7 --- /dev/null +++ b/lib/typeahead/typeahead_parser.dart @@ -0,0 +1,32 @@ +part of angular.ui.typeahead; + +class TypeaheadParseResult { + String itemName; + Expression source; + Expression viewMapper; + Expression modelMapper; + + TypeaheadParseResult(this.itemName, this.source, this.viewMapper, this.modelMapper); +} + +@Injectable() +class TypeaheadParser { + static final RegExp _SYNTAX = new RegExp(r'^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$'); + final Parser _parser; + + TypeaheadParser(this._parser); + + TypeaheadParseResult parse(String input) { + input = input.replaceAll('\n', ''); + var match = _SYNTAX.firstMatch(input); + var itemName = match.group(3); + var sourceExpression = match.group(4); + var modelExpression = match.group(1); + + var viewExpression = match.group(2); + if(viewExpression == null) { + viewExpression = modelExpression; + } + return new TypeaheadParseResult(itemName, _parser(sourceExpression), _parser(viewExpression), _parser(modelExpression)); + } +} \ No newline at end of file diff --git a/lib/typeahead/typeahead_popup.dart b/lib/typeahead/typeahead_popup.dart new file mode 100644 index 0000000..92341dc --- /dev/null +++ b/lib/typeahead/typeahead_popup.dart @@ -0,0 +1,147 @@ +part of angular.ui.typeahead; + +@Component( + selector: 'typeahead-popup', +// templateUrl: 'packages/angular_ui/typeahead/typeahead-popup.html', + template: r''' +''', + useShadowDom: false, + map: const { + 'matches': '=>matches', + 'active': '<=>active', + 'select': '&selectEventHandler', + 'position': '=>position', + 'template-url': '=>templateUrl', + 'query': '=>query' + } +) +//
+//@Component(selector: '[typeahead-popup]', +// templateUrl: 'packages/angular_ui/typeahead/typeahead-popup.html', +// publishAs: 'ctrl', +// useShadowDom: false, +// map: const { +// 'matches': '=>matches', +// 'active': '<=>active', +// 'select': '&selectEventHandler', +// 'position': '=>position', +// 'template-url': '=>templateUrl', +// 'query': '=>query' +//}) +class TypeaheadPopup { + + List matches; + int active; + Function selectEventHandler; + Rect _position; + String templateUrl; + String query; + + TypeaheadPopup(); + + get position => _position; + set position(value) => _position = value; + + bool get isOpen => (matches != null) && (matches.length != 0); + + bool isActive(index) => index == active; + + selectActive(index) => active = index; + + void selectMatch(index) { + selectEventHandler({ + 'activeIdx' : index + }); + } +} + +@Injectable() +class TemplateBasedComponent implements DetachAware { + + final ViewFactoryCache _viewCache; + + Scope _viewScope; + View _view; + + TemplateBasedComponent(this._viewCache); + + void detach() { + _cleanUp(); + } + + void loadView(dom.Element element, Injector injector, Scope scope, String templateUrl, Object locals, [bool replace = false]) { + DirectiveMap directives = injector.get(DirectiveMap); + _viewCache.fromUrl(templateUrl, directives).then((ViewFactory viewFactory) { + _cleanUp(); + // create a new scope + DirectiveInjector directiveInjector = injector.get(DirectiveInjector); + _viewScope = scope.createChild(locals); + _view = viewFactory(_viewScope, directiveInjector); + + if(replace) { + element.replaceWith(_view.nodes.firstWhere((e) { + return !(e is dom.Text); + })); + } else { + _view.nodes.forEach((e) { + return element.append(e); + }); + } + }); + } + + _cleanUp() { + if (_view == null) + return; + + _view.nodes.forEach((node) => node.remove()); + _viewScope.destroy(); + + _view = null; + _viewScope = null; + } +} + + +@Decorator( + selector : 'typeahead-match', + map: const { + 'index': '=>!index', + 'match': '=>!match', + 'query': '=>!query', + 'template-url': '=>!templateUrl' + } +) +@Decorator(selector : '[typeahead-match]', + map: const { + 'index': '=>!index', + 'match': '=>!match', + 'query': '=>!query', + 'template-url': '=>!templateUrl' + } +) +class TypeaheadMatch extends TemplateBasedComponent implements AttachAware, ScopeAware { + static const String DEFAULT_MATCHED_ITEM_TEMPLATE = 'packages/angular_ui/typeahead/typeahead-match.html'; + + final Injector _injector; + final dom.Element _element; + + Scope scope; + + int index; + var match; + String query; + String _templateUrl = DEFAULT_MATCHED_ITEM_TEMPLATE; + + TypeaheadMatch(this._element, this._injector, ViewFactoryCache viewCache) : super(viewCache); + + set templateUrl(String value) => _templateUrl = (value == null || value.isEmpty)? DEFAULT_MATCHED_ITEM_TEMPLATE: value; + + void attach() { + loadView(_element, _injector, scope, _templateUrl, {'match': match, 'index':index, 'query':query}, true); + } +} \ No newline at end of file diff --git a/lib/utils/README.md b/lib/utils/README.md new file mode 100644 index 0000000..f8a088e --- /dev/null +++ b/lib/utils/README.md @@ -0,0 +1 @@ +Set of utility classes implemented to help components to be shown, collapse and etc. \ No newline at end of file diff --git a/lib/utils/content_append.dart b/lib/utils/content_append.dart new file mode 100644 index 0000000..0278007 --- /dev/null +++ b/lib/utils/content_append.dart @@ -0,0 +1,34 @@ +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +library angular.ui.utils.content_append; + +import "package:angular/angular.dart"; +import 'dart:html'; + +class ContentAppendModule extends Module { + ContentAppendModule() { + bind(ContentAppendComponent); + } +} + +@Decorator( + selector: 'content-append' +) +class ContentAppendComponent { + Element _element; + + @NgOneWay('node') + set append(var node) { + if (node != null) { + if (node is String){ + _element.appendText(node as String); + } else if (node is Node) { + _element.append(node as Node); + } + } + } + + ContentAppendComponent(this._element); + +} \ No newline at end of file diff --git a/lib/utils/dbl_click_preventer.dart b/lib/utils/dbl_click_preventer.dart new file mode 100644 index 0000000..152b2e0 --- /dev/null +++ b/lib/utils/dbl_click_preventer.dart @@ -0,0 +1,36 @@ +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +library angular.ui.dbl_click_preventer; + +import "package:angular/angular.dart"; + +import 'timeout.dart'; + +/** + * Double Click Preventer Module. + */ +class DblClickPreventerModule extends Module { + DblClickPreventerModule() { + install(new TimeoutModule()); + bind(DblClickPreventer); + } +} + +@Injectable() +class DblClickPreventer { + bool _isNotWaiting = true; + final Timeout timeout; + + DblClickPreventer(this.timeout); + + call(Function func, {int delay:500}) { + if (_isNotWaiting) { + _isNotWaiting = false; + func(); + timeout(() { + _isNotWaiting = true; + }, delay: delay); + } + } +} \ No newline at end of file diff --git a/lib/utils/extend.dart b/lib/utils/extend.dart new file mode 100644 index 0000000..ac896c8 --- /dev/null +++ b/lib/utils/extend.dart @@ -0,0 +1,47 @@ +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +library angular.ui.utils.extend; + +Map extend(Map dst, List src) { + var tmpSrc = new List.from(src); + + while(tmpSrc.length > 0) { + if(tmpSrc[0] != null) { + tmpSrc[0].forEach((k, v) { + if(v != null) { + dst[k] = copy(v); + } + }); + } + tmpSrc.removeAt(0); + } + return dst; +} + +dynamic copy(source, [destination]) { + var dst; + if(destination != null) { + if(source is List) { + dst = new List(); + source.forEach((e) => dst.add(copy(e, dst))); + } else if(source is Map) { + dst = new Map(); + source.forEach((k, v) => dst[k] = copy(v)); + } else { + return source; + } + } else { + if(source is List) { + dst = []; + source.forEach((e) => dst.add(copy(e, dst))); + } else if(source is Map) { + dst = {}; + + source.forEach((k, v) => dst[k] = copy(v)); + } else { + return source; + } + } + return dst; +} \ No newline at end of file diff --git a/lib/utils/injectable_service.dart b/lib/utils/injectable_service.dart new file mode 100644 index 0000000..5f2a9f0 --- /dev/null +++ b/lib/utils/injectable_service.dart @@ -0,0 +1,12 @@ +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +library angular.ui.utils.injectable_service; + +/** + * An annotation specifying that the annotated class will be instantiated by + * di Injector and type factory code generator should include it in its output. + */ +class InjectableService { + const InjectableService(); +} \ No newline at end of file diff --git a/lib/utils/ng_pseudo.dart b/lib/utils/ng_pseudo.dart new file mode 100644 index 0000000..3800ecd --- /dev/null +++ b/lib/utils/ng_pseudo.dart @@ -0,0 +1,17 @@ +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +part of angular.ui.utils; + +@Decorator( + selector: '[ng-pseudo]', + map: const { 'ng-pseudo': '@name' } +) +class NgPseudo { + dom.Element _element; + set name(n) { + _element.attributes['pseudo'] = n; + } + + NgPseudo(dom.Element this._element); +} \ No newline at end of file diff --git a/lib/position.dart b/lib/utils/position.dart similarity index 56% rename from lib/position.dart rename to lib/utils/position.dart index bf9198f..dab8879 100644 --- a/lib/position.dart +++ b/lib/utils/position.dart @@ -1,7 +1,7 @@ -// Copyright (c) 2013 - 2014, akserg (Sergey Akopkokhyants) +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. // https://github.com/akserg/angular.dart.ui // All rights reserved. Please see the LICENSE.md file. -library angular.ui.position; +library angular.ui.utils.position; import 'dart:html' as dom; import "package:angular/angular.dart"; @@ -11,10 +11,7 @@ import "package:angular/angular.dart"; */ class PositionModule extends Module { PositionModule() { -// factory(Position, (Injector injector) { -// return new Position(dom.document, dom.window); -// }); - type(Position); + bind(Position); } } @@ -25,84 +22,51 @@ class PositionModule extends Module { * relation to other, existing elements (this is the case for tooltips, popovers, * typeahead suggestions etc.). */ +@Injectable() class Position { -// dom.Document document; -// dom.Window window; - -// Position(this.document, this.window); Position(); - + dynamic _getStyle(dom.Element el, String cssprop) { -// if (el.currentStyle) { //IE -// return el.currentStyle[cssprop]; -// } else if ($window.getComputedStyle) { -// return $window.getComputedStyle(el)[cssprop]; -// } -// // finally try and get inline style -// return el.style[cssprop]; return el.style.getPropertyValue(cssprop); } - + /** * Checks if a given element is statically positioned * @param element - raw DOM element */ bool _isStaticPositioned(dom.Element element) { -// return (_getStyle(element, "position") || 'static' ) === 'static'; String pos = _getStyle(element, "position"); return pos != null && pos == 'static'; } - + /** * returns the closest, non-statically positioned parentOffset of a given element * @param element */ dynamic _parentOffsetEl(dom.Element element) { -// var docDomEl = $document[0]; -// var offsetParent = element.offsetParent || docDomEl; -// while (offsetParent && offsetParent !== docDomEl && _isStaticPositioned(offsetParent) ) { -// offsetParent = offsetParent.offsetParent; -// } -// return offsetParent || docDomEl; - var docDomEl = dom.document; + var docDomEl = dom.document.body; var offsetParent = element.offsetParent != null ? element.offsetParent : docDomEl; while (offsetParent != null && offsetParent != docDomEl && _isStaticPositioned(offsetParent) ) { offsetParent = offsetParent.offsetParent; } - return offsetParent == null ? docDomEl : offsetParent; + return offsetParent != null ? offsetParent : docDomEl; } - + /** * Provides read-only equivalent of jQuery's position function: * http://api.jquery.com/position/ */ - dynamic position(dom.Element element) { -// var elBCR = offset(element); -// var offsetParentBCR = { top: 0, left: 0 }; -// var offsetParentEl = parentOffsetEl(element[0]); -// if (offsetParentEl != $document[0]) { -// offsetParentBCR = this.offset(angular.element(offsetParentEl)); -// offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop; -// offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft; -// } -// -// var boundingClientRect = element[0].getBoundingClientRect(); -// return { -// width: boundingClientRect.width || element.prop('offsetWidth'), -// height: boundingClientRect.height || element.prop('offsetHeight'), -// top: elBCR.top - offsetParentBCR.top, -// left: elBCR.left - offsetParentBCR.left -// }; + Rect position(dom.Element element) { var elBCR = offset(element); var offsetParentBCR = new Rect(); var offsetParentEl = _parentOffsetEl(element); - if (offsetParentEl != dom.document) { + if (offsetParentEl != dom.document.body) { offsetParentBCR = offset(offsetParentEl); offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop; offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft; } - + var boundingClientRect = element.getBoundingClientRect(); return new Rect( width: boundingClientRect.width != null ? boundingClientRect.width : element.offsetWidth, @@ -110,19 +74,12 @@ class Position { top: elBCR.top - offsetParentBCR.top, left: elBCR.left - offsetParentBCR.left); } - + /** * Provides read-only equivalent of jQuery's offset function: * http://api.jquery.com/offset/ */ Rect offset(dom.Element element) { -// var boundingClientRect = element[0].getBoundingClientRect(); -// return { -// width: boundingClientRect.width || element.prop('offsetWidth'), -// height: boundingClientRect.height || element.prop('offsetHeight'), -// top: boundingClientRect.top + ($window.pageYOffset || $document[0].body.scrollTop || $document[0].documentElement.scrollTop), -// left: boundingClientRect.left + ($window.pageXOffset || $document[0].body.scrollLeft || $document[0].documentElement.scrollLeft) -// }; var boundingClientRect = element.getBoundingClientRect(); var offs = new Rect( width: boundingClientRect.width != null ? boundingClientRect.width : element.offsetWidth, @@ -136,8 +93,9 @@ class Position { /** * A class for representing two-dimensional rectangles. */ +@Injectable() class Rect { - var left, top, width, height; - + num left, top, width, height; + Rect({this.left:0, this.top:0, this.width:0, this.height:0}); } \ No newline at end of file diff --git a/lib/timeout.dart b/lib/utils/timeout.dart similarity index 64% rename from lib/timeout.dart rename to lib/utils/timeout.dart index f4dd708..a2123ea 100644 --- a/lib/timeout.dart +++ b/lib/utils/timeout.dart @@ -1,4 +1,4 @@ -// Copyright (c) 2013 - 2014, akserg (Sergey Akopkokhyants) +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. // https://github.com/akserg/angular.dart.ui // All rights reserved. Please see the LICENSE.md file. library angular.ui.timeout; @@ -6,52 +6,68 @@ library angular.ui.timeout; import 'dart:async' as async; import "package:angular/angular.dart"; +import 'package:logging/logging.dart' show Logger; +final _log = new Logger('angular.ui.timeout'); + /** * Timeout Module. */ class TimeoutModule extends Module { TimeoutModule() { - type(Timeout); + bind(Timeout); } } +@Injectable() +class TimeItem { + Function fn; + async.Timer timer; + + TimeItem(this.fn, this.timer); +} + /** * Angular's UI wrapper for `window.setTimeout`. The `fn` function is wrapped into a try/catch * block and delegates any exceptions to * {@link ng.$exceptionHandler $exceptionHandler} service. - * + * * The return value of registering a timeout function is a promise, which will be resolved when * the timeout is reached and the timeout function is executed. * * To cancel a timeout request, call `$timeout.cancel(promise)`. - * + * * In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to * synchronously flush the queue of deferred functions. */ -class Timeout { - static Map deferreds = {}; - +@Injectable() +class Timeout implements ScopeAware { + Map deferreds = new Map(); + Scope scope; ExceptionHandler exceptionHandler; - - Timeout(this.scope, this.exceptionHandler); - + Scope rootScope; + + Timeout(Injector injector, this.exceptionHandler) { + this.rootScope = injector.get(RootScope); + } + /** * The [fn] is a function, whose execution should be delayed. * The [delay] in milliseconds. * If set invokeApply to `false` skips model dirty checking, otherwise * will invoke `fn` within the {@link ng.$rootScope.Scope#methods_$apply $apply} block. - * + * * Promise that will be resolved when the timeout is reached. The value this - * promise will be resolved with is the return value of the `fn` function. + * promise will be resolved with is the return value of the `fn` function. */ async.Completer call(Function fn, {int delay:0, bool invokeApply:true}) { assert(fn != null); - async.Completer deferred = new async.Completer(); - deferred.future.catchError((e, s) {}); - bool skipApply = !invokeApply; + async.Completer deferred = new async.Completer(); + deferred.future.catchError((e, s) { + //_log.fine('call error $e, $s'); // enabled for hard to find error source + }); var timeoutId; - + timeoutId = new async.Timer(new Duration(milliseconds: delay), () { try { if (!deferred.isCompleted) { @@ -67,39 +83,47 @@ class Timeout { deferreds.remove(deferred); } // - if (!skipApply) { - scope.$apply(); + if (invokeApply) { + rootScope.apply(); } }); - - deferreds[deferred] = fn; - + + deferreds[deferred] = new TimeItem(fn, timeoutId); + return deferred; } - + /** - * Cancels a task associated with the [promise]. + * Cancels a task associated with the [promise]. * As a result of this, the promise will be resolved with a rejection. */ bool cancel([async.Completer promise = null]) { if (promise != null && deferreds.containsKey(promise)) { + deferreds[promise].timer.cancel(); promise.completeError('canceled'); deferreds.remove(promise); return true; } return false; } - + /** * Call all functions in [deferreds]. */ - void flush() { - deferreds.forEach((async.Completer deferred, Function fn) { + void flush({bool cancel:false}) { + deferreds.forEach((async.Completer deferred, TimeItem timeItem) { try { - deferred.complete(fn()); + if (cancel) { + deferred.completeError('canceled'); + } else { + deferred.complete(timeItem.fn()); + } } catch(e, s) { deferred.completeError(e); exceptionHandler(e, s); + } finally { + timeItem.timer.cancel(); + timeItem.timer = null; } }); deferreds.clear(); diff --git a/lib/utils/transition.dart b/lib/utils/transition.dart new file mode 100644 index 0000000..23e915e --- /dev/null +++ b/lib/utils/transition.dart @@ -0,0 +1,112 @@ +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +library angular.ui.transition; + +import 'dart:html' as dom; +import 'dart:async' as async; +import "package:angular/angular.dart"; + +import 'timeout.dart'; + +import 'package:logging/logging.dart' show Logger; +final _log = new Logger('angular.ui.transition'); + + +/** + * Transition Module. + */ +class TransitionModule extends Module { + TransitionModule() { + install(new TimeoutModule()); + bind(Transition); + } +} + +@Injectable() +class Transition { + static const Map transitionEndEventNames = const { + 'WebkitTransition': 'webkitTransitionEnd', + 'MozTransition': 'transitionend', + 'OTransition': 'oTransitionEnd', + 'transition': 'transitionend' + }; + static const Map animationEndEventNames = const { + 'WebkitTransition': 'webkitAnimationEnd', + 'MozTransition': 'animationend', + 'OTransition': 'oAnimationEnd', + 'transition': 'animationend' + }; + + Timeout timeout; + String transitionEndEventName, animationEndEventName; + + Transition(this.timeout) { + _log.fine("Transition"); + + var transElement = dom.document.createElement('trans'); + + findEndEventName(Map endEventNames) { + for (String name in endEventNames.keys) { + if (transElement.style.supportsProperty(name)) { + return endEventNames[name]; + } + } + return null; + } + transitionEndEventName = findEndEventName(transitionEndEventNames); + animationEndEventName = findEndEventName(animationEndEventNames); + } + + async.Completer call(dom.Element element, trigger, {Map options: const{}}) { + async.Completer deferred = new async.Completer(); + var endEventName; + if (options.containsKey('animation')) { + endEventName = animationEndEventName; + } else { + endEventName = transitionEndEventName; + } + + var transitionEndHandler; + + transitionEndHandler = (dom.Event event) { + if (endEventName != null) { + element.removeEventListener(endEventName, transitionEndHandler); + } + if (!deferred.isCompleted) { + deferred.complete(element); + } + }; + + if (endEventName != null) { + element.addEventListener(endEventName, transitionEndHandler); + } + + // Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur + timeout(() { + if (trigger is String) { + element.classes.add(trigger); + } else if (trigger is Function) { + trigger(element); + } else if (trigger is Map) { + trigger.forEach((propertyName, value) => element.style.setProperty(propertyName, value)); + } + + //If browser does not support transitions, instantly resolve + if (endEventName == null) { + deferred.complete(element); + } + }); + + // Add our custom cancel function to the promise that is returned + // We can call this if we are about to run a new transition, which we know will prevent this transition from ending, + // i.e. it will therefore never raise a transitionEnd event for that transition + deferred.future.then((value) { + if (value == 'Canceled' && endEventName != null) { + element.removeEventListener(endEventName, transitionEndHandler); + } + }); + + return deferred; + } +} \ No newline at end of file diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart new file mode 100644 index 0000000..2255ab8 --- /dev/null +++ b/lib/utils/utils.dart @@ -0,0 +1,132 @@ +// Copyright (C) 2013 - 2015 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +library angular.ui.utils; + +import 'dart:html' as dom; +import 'package:intl/intl.dart' as intl; +import "package:angular/angular.dart"; + +part 'ng_pseudo.dart'; + +bool toBool(x) { + if (x is bool) return x; + if (x is num) return x != 0; + if (x is String) return (x as String).toLowerCase() == "true"; + return false; +} + +int toInt(x) { + if (x is num) return x.toInt(); + if (x is String) return int.parse(x); + throw new Exception("Can't translate $x to int"); +} + +dynamic eval(Scope scope, value, [defaultValue = null]) { + var val = null; + if (value != null) { + val = scope.eval(value is String ? value : value.toString()); + } + return val != null ? val : defaultValue; +} + +dom.Element getFirstDiv(doc) => doc.children.firstWhere(isDiv); + +bool isDiv(dom.Element element) => element is dom.DivElement; + +dom.Element getFirstUList(doc) => doc.children.firstWhere(isUList); + +bool isUList(dom.Element element) => element is dom.UListElement; + +/** + * Convert an [html] String to a [List] of [Element]s. + */ +List toNodeList(html) { + var div = new dom.DivElement(); + div.setInnerHtml(html, treeSanitizer: new NullTreeSanitizer()); + var nodes = []; + for(var node in div.nodes) { + nodes.add(node); + } + return nodes; +} + +// Split array into smaller arrays +List split(List arr, int size) { + var arrays = []; + for (int b = 0, e = size;;b += size, e += size) { + if (e < arr.length) { + arrays.add(arr.getRange(b, e).toList()); + } else { + arrays.add(arr.getRange(b, arr.length).toList()); + break; + } + } + return arrays; +} + +/** + * Try treat [model] as [String], [int] or [DateTime] to convert to [DateTime] + * or return null. If specified the [DateFormat] will be used to parse date. + */ +DateTime parseDate(model, [intl.DateFormat format = null]) { + DateTime date; + + if (model != null) { + try { + if (model is String) { + if (format != null) { + date = format.parse(model); + } else { + date = DateTime.parse(model); + } + } else if (model is int) { + date = new DateTime.fromMillisecondsSinceEpoch(model); + } else { + date = model as DateTime; + } + } on Exception catch(e) { + } + } + return date; +} + +/** + * Use to compile HTML and activate its directives. + * + * If [html] parameter is: + * + * - [String] then treat it as HTML + * - [Node] then treat it as the root node + * - [List] then treat it as a collection of nods + * + * After the compilation the [rootElements] contains an array of compiled root nodes, + * and [rootElement] contains the first element from the [rootElemets]. + * + * An option [scope] parameter can be supplied to link it with non root scope. + */ +dom.Element compile(html, Injector injector, Compiler compiler, {Scope scope, DirectiveMap directives}) { + List rootElements; + if (scope != null) { + injector = new ModuleInjector([new Module()..bind(Scope, toValue:scope)], injector); + } + if (html is String) { + rootElements = toNodeList(html.trim()); + } else if (html is dom.Node) { + rootElements = [html]; + } else if (html is List) { + rootElements = html; + } else { + throw 'Expecting: String, Node, or List got $html.'; + } + dom.Element rootElement = rootElements.length > 0 && rootElements[0] is dom.Element ? rootElements[0] : null; + if (directives == null) { + directives = injector.get(DirectiveMap); + } + ViewFactory viewFactory = compiler(rootElements, directives); + Scope _scope = scope != null ? scope : injector.get(Scope); + DirectiveInjector directiveInjector = injector.get(DirectiveInjector); + View rootView = viewFactory(_scope, directiveInjector, rootElements); + return rootElement; +} + diff --git a/package.json b/package.json new file mode 100644 index 0000000..18630d1 --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "angular.dart.ui", + "repository": { + "type": "git", + "url": "https://github.com/akserg/angular.dart.ui.git" + }, + "devDependencies": { + "karma" : "~0.12.0", + "karma-dart" : "~0.2.6", + "karma-chrome-launcher": "~0.1.3", + "karma-junit-reporter": "~0.2.1" + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..5f07c6e --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,165 @@ +# Generated by pub +# See http://pub.dartlang.org/doc/glossary.html#lockfile +packages: + analyzer: + description: analyzer + source: hosted + version: "0.26.4" + angular: + description: angular + source: hosted + version: "1.1.2+2" + args: + description: args + source: hosted + version: "0.12.2+6" + async: + description: async + source: hosted + version: "1.8.0" + barback: + description: barback + source: hosted + version: "0.15.2+7" + browser: + description: browser + source: hosted + version: "0.10.0+2" + charcode: + description: charcode + source: hosted + version: "1.1.0" + cli_util: + description: cli_util + source: hosted + version: "0.0.1+2" + code_transformers: + description: code_transformers + source: hosted + version: "0.2.11" + collection: + description: collection + source: hosted + version: "1.1.3" + csslib: + description: csslib + source: hosted + version: "0.12.2" + dartmocks: + description: dartmocks + source: hosted + version: "0.5.2+1" + di: + description: di + source: hosted + version: "3.3.5+1" + glob: + description: glob + source: hosted + version: "1.1.0" + guinness: + description: guinness + source: hosted + version: "0.1.18" + html: + description: html + source: hosted + version: "0.12.2+1" + html5lib: + description: html5lib + source: hosted + version: "0.12.1" + intl: + description: intl + source: hosted + version: "0.8.10+4" + logging: + description: logging + source: hosted + version: "0.11.2" + matcher: + description: matcher + source: hosted + version: "0.11.4+4" + meta: + description: meta + source: hosted + version: "0.8.8" + mock: + description: mock + source: hosted + version: "0.11.0+4" + observe: + description: observe + source: hosted + version: "0.13.1+3" + package_config: + description: package_config + source: hosted + version: "0.1.3" + path: + description: path + source: hosted + version: "1.3.9" + perf_api: + description: perf_api + source: hosted + version: "0.0.9" + plugin: + description: plugin + source: hosted + version: "0.1.0" + pool: + description: pool + source: hosted + version: "1.2.1" + route_hierarchical: + description: route_hierarchical + source: hosted + version: "0.6.2" + smoke: + description: smoke + source: hosted + version: "0.3.5" + source_maps: + description: source_maps + source: hosted + version: "0.10.1" + source_span: + description: source_span + source: hosted + version: "1.2.1" + stack_trace: + description: stack_trace + source: hosted + version: "1.6.0" + string_scanner: + description: string_scanner + source: hosted + version: "0.1.4+1" + unittest: + description: unittest + source: hosted + version: "0.11.6+1" + utf: + description: utf + source: hosted + version: "0.9.0+3" + watcher: + description: watcher + source: hosted + version: "0.9.7" + when: + description: when + source: hosted + version: "0.2.0" + which: + description: which + source: hosted + version: "0.1.3" + yaml: + description: yaml + source: hosted + version: "2.1.8" +sdks: + dart: ">=1.12.0 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 2b214be..95b6e9a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,13 +1,49 @@ name: angular_ui -version: 0.2.0 -author: Sergey Akopkokhyants , Tonis Pool . +version: 0.6.13 +authors: +- Sergey Akopkokhyants +- Tonis Pool +- Gunter Zochbauer +- Francesco Cina +- Neeraj Mittal description: Port of Angular-UI to Dart. homepage: http://akserg.github.io/angular.dart.ui environment: - sdk: '>=1.0.0+10 <2.0.0' + sdk: '>=1.6.0 <2.0.0' dependencies: - angular: '>=0.9.4' - browser: '>=0.9.1' - js: '>=0.2.1' + browser: '>=0.10.0+2 <0.11.0' + angular: '>=1.1.0 <2.0.0' dev_dependencies: - unittest: '>=0.9.3' + dartmocks: '>= 0.5.0' + guinness: '>= 0.1.3' +transformers: +- angular: + html_files: + - web/buttons/buttons_demo.html + - web/alert/alert_demo.html + - web/dragdrop/dragdrop_demo_tpl.html + - web/dragdrop/dragdrop_demo_image.html + - web/dragdrop/dragdrop_demo_list.html + - web/dragdrop/dragdrop_demo_multidropzones.html + - web/dragdrop/dragdrop_demo_shopping.html + - web/dragdrop/sortable_demo_simple.html + - web/dragdrop/sortable_demo_multi.html + - web/collapse/collapse_demo.html + - web/dropdown_toggle/dropdown_toggle_demo.html + - web/rating/rating_demo.html + - web/timepicker/timepicker_demo.html + - web/tooltip/tooltip_demo.html + - web/pagination/pagination_demo.html + - web/modal/modal_demo_embedded_template.html + - web/modal/modal_demo_embedded_template_static_backdrop.html + - web/modal/modal_demo_template_element.html + - web/modal/modal_demo_template_from_other_file.html + - web/accordion/accordion_demo.html + - web/popover/popover_demo.html + - web/progressbar/progressbar_demo.html + - web/tabs/tabs_demo.html + - web/carousel/carousel_demo.html + - web/datepicker/datepicker_demo.html + - web/typeahead/typeahead_demo.html +- $dart2js: + commandLineOptions: [--enable-experimental-mirrors] \ No newline at end of file diff --git a/scripts/travis/run_tests.sh b/scripts/travis/run_tests.sh new file mode 100755 index 0000000..7febda4 --- /dev/null +++ b/scripts/travis/run_tests.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +set -e + +DART_DIST=dartsdk-linux-x64-release.zip +DARTIUM_DIST=dartium-linux-x64-release.zip + +echo Fetching dart sdk and Dartium +curl http://storage.googleapis.com/dart-archive/channels/stable/release/latest/sdk/$DART_DIST > $DART_DIST +curl http://storage.googleapis.com/dart-archive/channels/stable/raw/latest/dartium/$DARTIUM_DIST > $DARTIUM_DIST + +echo Unzipping the files +unzip $DART_DIST > /dev/null +unzip $DARTIUM_DIST > /dev/null +rm $DART_DIST +rm $DARTIUM_DIST + +echo Rename dartium +mv dartium-* dartium; + +echo Setting up the environment variables +export DART_SDK="$PWD/dart-sdk" +export PATH="$DART_SDK/bin:$PATH" +export DARTIUM_BIN="$PWD/dartium/chrome" + +echo Installing all the dependencies +pub install + +echo Launching xvfb +sh -e /etc/init.d/xvfb start + +echo Starting Karma +./node_modules/karma/bin/karma start --single-run --browsers Dartium \ No newline at end of file diff --git a/test/_specs.dart b/test/_specs.dart deleted file mode 100644 index 03ef19e..0000000 --- a/test/_specs.dart +++ /dev/null @@ -1,264 +0,0 @@ -// The MIT License -// -// Copyright (c) 2010-2012 Google, Inc. http://angularjs.org -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -library ng_specs; - -import 'dart:html'; -import 'dart:mirrors' as mirror; -import 'package:unittest/unittest.dart' as unit; -import 'package:angular/angular.dart'; -import 'package:angular/mock/module.dart'; - -import 'jasmine_syntax.dart'; - -export 'dart:html'; -export 'jasmine_syntax.dart' hide main; -export 'package:unittest/unittest.dart'; -export 'package:unittest/mock.dart'; -export 'package:di/dynamic_injector.dart'; -export 'package:angular/angular.dart'; -export 'package:angular/mock/module.dart'; -export 'package:perf_api/perf_api.dart'; - -es(String html) { - var div = new DivElement(); - div.setInnerHtml(html, treeSanitizer: new NullTreeSanitizer()); - return div.nodes; -} - -e(String html) => es(html).first; - -renderedText(n, [bool notShadow = false]) { - if (n is List) { - return n.map((nn) => renderedText(nn)).join(""); - } - - if (n is Comment) return ''; - - if (!notShadow && n is Element && n.shadowRoot != null) { - var shadowText = n.shadowRoot.text; - var domText = renderedText(n, true); - return shadowText.replaceFirst("SHADOW-CONTENT", domText); - } - - if (n.nodes == null || n.nodes.length == 0) return n.text; - - return n.nodes.map((cn) => renderedText(cn)).join(""); -} - -Expect expect(actual, [unit.Matcher matcher = null]) { - if (matcher != null) { - unit.expect(actual, matcher); - } - return new Expect(actual); -} - -class Expect { - var actual; - var not; - Expect(this.actual) { - not = new NotExpect(this); - } - - toEqual(expected) => unit.expect(actual, unit.equals(expected)); - toContain(expected) => unit.expect(actual, unit.contains(expected)); - toBe(expected) => unit.expect(actual, - unit.predicate((actual) => identical(expected, actual), '$expected')); - toThrow([exception]) => unit.expect(actual, exception == null ? unit.throws : unit.throwsA(new ExceptionContains(exception))); - toBeFalsy() => unit.expect(actual, (v) => v == null ? true : v is bool ? v == false : false); - toBeTruthy() => unit.expect(actual, (v) => v is bool ? v == true : true); - toBeDefined() => unit.expect(actual, (v) => v != null); - toBeNull() => unit.expect(actual, unit.isNull); - toBeNotNull() => unit.expect(actual, unit.isNotNull); - - toHaveBeenCalled() => unit.expect(actual.called, true, reason: 'method not called'); - toHaveBeenCalledOnce() => unit.expect(actual.count, 1, reason: 'method invoked ${actual.count} expected once'); - toHaveBeenCalledWith([a,b,c,d,e,f]) => - unit.expect(actual.firstArgsMatch(a,b,c,d,e,f), true, - reason: 'method invoked with correct arguments'); - toHaveBeenCalledOnceWith([a,b,c,d,e,f]) => - unit.expect(actual.count == 1 && actual.firstArgsMatch(a,b,c,d,e,f), - true, - reason: 'method invoked once with correct arguments. (Called ${actual.count} times)'); - - toHaveClass(cls) => unit.expect(actual.classes.contains(cls), true, reason: ' Expected ${actual} to have css class ${cls}'); - - toEqualSelect(options) { - var actualOptions = []; - - for(var option in actual.querySelectorAll('option')) { - if (option.selected) { - actualOptions.add([option.value]); - } else { - actualOptions.add(option.value); - } - } - return unit.expect(actualOptions, options); - } - - toEqualValid() { -// TODO: implement onece we have forms - } - toEqualInvalid() { -// TODO: implement onece we have forms - } - toEqualPristine() { -// TODO: implement onece we have forms - } - toEqualDirty() { -// TODO: implement onece we have forms - } -} - -class NotExpect { - Expect expect; - get actual => expect.actual; - NotExpect(this.expect); - - toHaveBeenCalled() => unit.expect(actual.called, false, reason: 'method called'); - toThrow() => actual(); - - toHaveClass(cls) => unit.expect(actual.classes.contains(cls), false, reason: ' Expected ${actual} to not have css class ${cls}'); - toBe(expected) => unit.expect(actual, - unit.predicate((actual) => !identical(expected, actual), '$expected')); - toEqual(expected) => unit.expect(actual, - unit.predicate((actual) => expected != actual, '$expected')); - toContain(expected) => unit.expect(actual, - unit.predicate((actual) => !actual.contains(expected), '$expected')); -} - -class ExceptionContains extends unit.Matcher { - - final _expected; - - const ExceptionContains(this._expected); - - bool matches(item, Map matchState) { - if (item is String) { - return item.indexOf(_expected) >= 0; - } - return matches('$item', matchState); - } - - unit.Description describe(unit.Description description) => - description.add('exception contains ').addDescriptionOf(_expected); - - unit.Description describeMismatch(item, unit.Description mismatchDescription, - Map matchState, bool verbose) { - return super.describeMismatch('$item', mismatchDescription, matchState, - verbose); - } -} - -$(selector) { - return new JQuery(selector); -} - - -class GetterSetter { - Getter getter(String key) => null; - Setter setter(String key) => null; -} -var getterSetter = new GetterSetter(); - -class JQuery implements List { - List _list = []; - - JQuery([selector]) { - if (selector == null) { -// do nothing; - } else if (selector is String) { - _list.addAll(es(selector)); - } else if (selector is List) { - _list.addAll(selector); - } else if (selector is Node) { - add(selector); - } else { - throw selector; - } - } - - noSuchMethod(Invocation invocation) => mirror.reflect(_list).delegate(invocation); - - _toHtml(node, [bool outer = false]) { - if (node is Comment) { - return ''; - } else { - return outer ? node.outerHtml : node.innerHtml; - } - } - - accessor(Function getter, Function setter, [value, single=false]) { -// TODO(dart): ?value does not work, since value was passed. :-( - var setterMode = value != null; - var result = setterMode ? this : ''; - _list.forEach((node) { - if (setterMode) { - setter(node, value); - } else { - result = single ? getter(node) : '$result${getter(node)}'; - } - }); - return result; - } - - html([String html]) => accessor( - (n) => _toHtml(n), - (n, v) => n.setInnerHtml(v, treeSanitizer: new NullTreeSanitizer()), - html); - val([String text]) => accessor((n) => n.value, (n, v) => n.value = v); - text([String text]) => accessor((n) => n.text, (n, v) => n.text = v, text); - contents() => fold(new JQuery(), (jq, node) => jq..addAll(node.nodes)); - toString() => fold('', (html, node) => '$html${_toHtml(node, true)}'); - eq(num childIndex) => $(this[childIndex]); - remove(_) => forEach((n) => n.remove()); - attr([String name, String value]) => accessor( - (n) => n.attributes[name], - (n, v) => n.attributes[name] = v, - value, - true); - prop([String name]) => accessor( - (n) => getterSetter.getter(name)(n), - (n, v) => getterSetter.setter(name)(n, v), - null, - true); - textWithShadow() => fold('', (t, n) => '${t}${renderedText(n)}'); - find(selector) => fold(new JQuery(), (jq, n) => jq..addAll( - (n is Element ? (n as Element).querySelectorAll(selector) : []))); - hasClass(String name) => fold(false, (hasClass, node) => - hasClass || (node is Element && (node as Element).classes.contains(name))); - addClass(String name) => _list.forEach((node) => - (node is Element) ? (node as Element).classes.add(name) : null); - removeClass(String name) => _list.forEach((node) => - (node is Element) ? (node as Element).classes.remove(name) : null); - css(String name, [String value]) => accessor( - (Element n) => n.style.getPropertyValue(name), - (Element n, v) => n.style.setProperty(name, value), value); - children() => new JQuery(this[0].childNodes); -} - - -main() { - beforeEach(setUpInjector); - beforeEach(() => wrapFn(sync)); - afterEach(tearDownInjector); -} \ No newline at end of file diff --git a/test/angular_ui_tests.dart b/test/angular_ui_tests.dart new file mode 100644 index 0000000..3c0105e --- /dev/null +++ b/test/angular_ui_tests.dart @@ -0,0 +1,130 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. + +library angular_ui_test; + +import 'package:guinness/guinness_html.dart'; +import 'dart:async'; +import 'dart:html' as dom; + +import 'package:angular/angular.dart'; +import 'package:angular/core/module_internal.dart'; +import 'package:angular/mock/module.dart'; +import 'package:angular/mock/test_injection.dart'; + +import 'package:angular_ui/utils/position.dart'; +import 'package:angular_ui/utils/timeout.dart'; +import 'package:angular_ui/utils/transition.dart'; +import 'package:angular_ui/utils/content_append.dart'; +import 'package:angular_ui/utils/dbl_click_preventer.dart'; + +import 'package:angular_ui/buttons/buttons.dart'; +import 'package:angular_ui/alert/alert.dart'; +import 'package:angular_ui/collapse/collapse.dart'; +import 'package:angular_ui/dropdown/dropdown_toggle.dart'; +import 'package:angular_ui/rating/rating.dart'; +import 'package:angular_ui/timepicker/timepicker.dart'; +import 'package:angular_ui/tooltip/tooltip.dart'; +import 'package:angular_ui/pagination/pagination.dart'; +import 'package:angular_ui/accordion/accordion.dart'; +import 'package:angular_ui/popover/popover.dart'; +import 'package:angular_ui/progressbar/progressbar.dart'; +import 'package:angular_ui/tabs/tabset.dart'; +import 'package:angular_ui/carousel/carousel.dart'; +import 'package:angular_ui/datepicker/datepicker.dart'; +import 'package:angular_ui/dragdrop/dragdrop.dart'; +import 'package:angular_ui/typeahead/module.dart'; +import 'package:angular_ui/modal/modal.dart'; + +part 'unit/utils/position_test.dart'; +part 'unit/utils/timeout_test.dart'; +part 'unit/utils/transition_test.dart'; +part 'unit/utils/content_append_test.dart'; + +part 'unit/buttons/checkbox_component_test.dart'; +part 'unit/buttons/radiobutton_component_test.dart'; +part 'unit/alert/alert_component_test.dart'; +part 'unit/collapse/collapse_component_test.dart'; +part 'unit/dropdown/dropdown_component_test.dart'; +part 'unit/rating/rating_component_test.dart'; +part 'unit/timepicker/timepicker_component_test.dart'; +part 'unit/tooltip/tooltip_component_test.dart'; +part 'unit/pagination/pager_component_test.dart'; +part 'unit/pagination/pagination_component_test.dart'; +part 'unit/accordion/accordion_component_test.dart'; +part 'unit/accordion/accordion_group_component_test.dart'; +part 'unit/popover/popover_component_test.dart'; +part 'unit/progressbar/progressbar_component_test.dart'; +part 'unit/tabs/tabs_component_test.dart'; +part 'unit/carousel/carousel_component_test.dart'; +part 'unit/datepicker/datepicker_component_test.dart'; +part 'unit/dragdrop/dragdrop_sortable_test.dart'; +part 'unit/dragdrop/dragdrop_test.dart'; +part 'unit/typeahead/typeahead_parser_tests.dart'; +part 'unit/typeahead/typeahead_highlight_tests.dart'; +part 'unit/typeahead/typeahead_popup_tests.dart'; +part 'unit/typeahead/typeahead_tests.dart'; +part 'unit/modal/modal_component_test.dart'; + +main(){ + guinnessEnableHtmlMatchers(); + + testPosition(); + testTimeout(); + testTransition(); + testContentAppendComponent(); + + testCheckboxComponent(); + testRadiobuttonComponent(); + testAlertComponent(); + testCollapseComponent(); + testDropdownComponent(); + testRatingComponent(); + testTimepickerComponent(); + testTooltipComponent(); + testPagerComponent(); + testPaginationComponent(); + testAccordionComponent(); + testAccordionGroupComponent(); + testPopoverComponent(); + testProgressbarComponent(); + testTabsComponent(); + testCarouselComponent(); + testDatepickerComponent(); + typeaheadParserTests(); + typeaheadHighlightFilterTests(); + typeaheadPopupTests(); + typeaheadComponentTests(); + testModalComponent(); + + guinness.initSpecs(); +} + +loadTemplates(List templates){ + updateCache(template, response) => inject((TemplateCache cache) => cache.put(template, response)); + + final futures = templates.map((template) => + dom.HttpRequest.request('packages/angular_ui/' + template, method: "GET"). + then((_) => updateCache(template, new HttpResponse(200, _.response)))); + + return Future.wait(futures); +} + +compileComponent(String html, Map scope, callback){ + return async(() { + inject((TestBed tb) { + final s = tb.rootScope.createChild(scope); + final el = tb.compile('
$html
', scope: s); + + microLeap(); + digest(); + + callback(s, el); + }); + }); +} + +digest(){ + inject((TestBed tb) { tb.rootScope.apply(); }); +} diff --git a/test/jasmine_syntax.dart b/test/jasmine_syntax.dart deleted file mode 100644 index 41bbf53..0000000 --- a/test/jasmine_syntax.dart +++ /dev/null @@ -1,175 +0,0 @@ -// The MIT License -// -// Copyright (c) 2010-2012 Google, Inc. http://angularjs.org -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -library jasmine; - -import 'package:unittest/unittest.dart' as unit; -import 'package:angular/utils.dart' as utils; - -Function _wrapFn; - -_maybeWrapFn(fn) => () { - if (_wrapFn != null) { - _wrapFn(fn)(); - } else { - fn(); - } -}; - -it(name, fn) => unit.test(name, _maybeWrapFn(fn)); -iit(name, fn) => unit.solo_test(name, _maybeWrapFn(fn)); -xit(name, fn) {} -xdescribe(name, fn) {} -ddescribe(name, fn) => describe(name, fn, true); - - -class Describe { - Describe parent; - String name; - bool exclusive; - List beforeEachFns = []; - List afterEachFns = []; - - Describe(this.name, this.parent, [bool this.exclusive=false]) { - if (parent != null && parent.exclusive) { - exclusive = true; - } - } - - setUp() { - beforeEachFns.forEach((fn) => fn()); - } - - tearDown() { - afterEachFns.forEach((fn) => fn()); - } -} - -Describe currentDescribe = new Describe('', null); -bool ddescribeActive = false; - -describe(name, fn, [bool exclusive=false]) { - var lastDescribe = currentDescribe; - currentDescribe = new Describe(name, lastDescribe, exclusive); - if (exclusive) { - name = 'DDESCRIBE: $name'; - ddescribeActive = true; - } - try { - unit.group(name, () { - unit.setUp(currentDescribe.setUp); - fn(); - unit.tearDown(currentDescribe.tearDown); - }); - } finally { - currentDescribe = lastDescribe; - } -} - -beforeEach(fn) => currentDescribe.beforeEachFns.add(fn); -afterEach(fn) => currentDescribe.afterEachFns.insert(0, fn); - -wrapFn(fn) => _wrapFn = fn; - -var jasmine = new Jasmine(); - -class SpyFunctionInvocationResult { - final List args; - SpyFunctionInvocationResult(this.args); -} - -class SpyFunction { - String name; - List> invocations = []; - List> invocationsWithoutTrailingNulls = []; - var _andCallFakeFn; - - SpyFunction([this.name]); - call([a0, a1, a2, a3, a4, a5]) { - var args = []; - args.add(a0); - args.add(a1); - args.add(a2); - args.add(a3); - args.add(a4); - args.add(a5); - invocations.add(args); - - var withoutNulls = new List.from(args); - while (!withoutNulls.isEmpty && withoutNulls.last == null) { - withoutNulls.removeLast(); - } - invocationsWithoutTrailingNulls.add(withoutNulls); - - if (_andCallFakeFn != null) { - utils.relaxFnApply(_andCallFakeFn, args); - } - } - - andCallFake(fn) { - _andCallFakeFn = fn; - return this; - } - - reset() => invocations = []; - - num get count => invocations.length; - bool get called => count > 0; - - num get callCount => count; - get argsForCall => invocationsWithoutTrailingNulls; - - firstArgsMatch(a,b,c,d,e,f) { - var fi = invocations.first; - assert(fi.length == 6); - if ("${fi[0]}" != "$a") return false; - if ("${fi[1]}" != "$b") return false; - if ("${fi[2]}" != "$c") return false; - if ("${fi[3]}" != "$d") return false; - if ("${fi[4]}" != "$e") return false; - if ("${fi[5]}" != "$f") return false; - - return true; - } - - get mostRecentCall { - if (invocations.isEmpty) { - throw ["No calls"]; - } - return new SpyFunctionInvocationResult(invocations.last); - } -} - -class Jasmine { - createSpy([String name]) { - return new SpyFunction(name); - } - - SpyFunction spyOn(receiver, methodName) { - throw ["spyOn not implemented"]; - } -} - -main(){ - unit.setUp(currentDescribe.setUp); - unit.tearDown(currentDescribe.tearDown); -} \ No newline at end of file diff --git a/test/tests/alert_tests.dart b/test/tests/alert_tests.dart deleted file mode 100644 index 2199839..0000000 --- a/test/tests/alert_tests.dart +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) 2013 - 2014, akserg (Sergey Akopkokhyants) -// https://github.com/akserg/angular.dart.ui -// All rights reserved. Please see the LICENSE.md file. - -part of angular.ui.test; - -void alertTests() { - - - describe('Testing alert:', () { - TestBed _; - Scope scope; - dom.Element element; - TemplateCache cache; - - beforeEach(setUpInjector); - beforeEach(module((Module module) { - module.install(new AlertModule()); - })); - beforeEach(inject((TestBed tb) => _ = tb)); - beforeEach(inject((Scope s) => scope = s)); - beforeEach(inject((TemplateCache c) => cache = c)); - - afterEach(tearDownInjector); - - List createAlerts() { - cache.put('packages/angular_ui/alert.html', new HttpResponse(200, '
')); - element = _.compile("
" + - "{{alert.msg}}" + - "" + - "
"); - scope.alerts = [ - { 'msg':'foo', 'type':'success'}, - { 'msg':'bar', 'type':'error'}, - { 'msg':'baz'} - ]; - scope.$digest(); - return element.querySelectorAll('alert'); - }; - - dom.Element findCloseButton(index) { - return element.querySelectorAll('.close')[index]; - } - - dom.Element findContent(index) { - return element.querySelectorAll('alert')[index]; - } - - it("should generate alerts using ng-repeat", () { - var alerts = createAlerts(); - expect(alerts.length).toEqual(3); - }); - - it('should show the alert content', () { - var alerts = createAlerts(); - - for (var i = 0, n = alerts.length; i < n; i++) { - expect(findContent(i).text).toEqual(scope.alerts[i]['msg']); - } - }); - }); -} \ No newline at end of file diff --git a/test/tests/buttons_tests.dart b/test/tests/buttons_tests.dart deleted file mode 100644 index 5ecbfc2..0000000 --- a/test/tests/buttons_tests.dart +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright (c) 2013 - 2014, akserg (Sergey Akopkokhyants) -// https://github.com/akserg/angular.dart.ui -// All rights reserved. Please see the LICENSE.md file. - -part of angular.ui.test; - -void buttonsTests() { - - - describe('Testing Checkbox buttons:', () { - TestBed _; - Scope scope; - - beforeEach(setUpInjector); - beforeEach(module((Module module) { - module.install(new ButtonModule()); - })); - beforeEach(inject((TestBed tb) => _ = tb)); - beforeEach(inject((Scope s) => scope = s)); - - afterEach(tearDownInjector); - - var compileButton = (String markup) { - var el = _.compile(markup); - scope.$digest(); - return el; - }; - - //model -> UI - it('should work correctly with default model values', () { - scope.model = false; - var btn = compileButton(''); - expect(btn).not.toHaveClass('active'); - - scope.model = true; - scope.$digest(); - expect(btn).toHaveClass('active'); - }); - - it('should bind custom model values', () { - scope.model = 1; - var btn = compileButton(''); - expect(btn).toHaveClass('active'); - - scope.model = 0; - scope.$digest(); - expect(btn).not.toHaveClass('active'); - }); - - //UI-> model - it('should toggle default model values on click', () { - scope.model = false; - var btn = compileButton(''); - - btn.click(); - scope.$digest(); - expect(scope.model).toEqual(true); - expect(btn).toHaveClass('active'); - - btn.click(); - scope.$digest(); - expect(scope.model).toEqual(false); - expect(btn).not.toHaveClass('active'); - }); - - it('should toggle custom model values on click', () { - scope.model = 0; - var btn = compileButton(''); - - btn.click(); - scope.$digest(); - expect(scope.model).toEqual(1); - expect(btn).toHaveClass('active'); - - btn.click(); - scope.$digest(); - expect(scope.model).toEqual(0); - expect(btn).not.toHaveClass('active'); - }); - - it('should monitor true / false value changes', () { - scope.model = 1; - scope.trueVal = 1; - var btn = compileButton(''); - - expect(btn).toHaveClass('active'); - expect(scope.model).toEqual(1); - - scope.model = 2; - scope.trueVal = 2; - scope.$digest(); - - expect(btn).toHaveClass('active'); - expect(scope.model).toEqual(2); - }); - }); - - describe('Testing radio buttons:', () { - TestBed _; - Scope scope; - - beforeEach(setUpInjector); - beforeEach(module((Module module) { - module.install(new ButtonModule()); - })); - beforeEach(inject((TestBed tb) => _ = tb)); - beforeEach(inject((Scope s) => scope = s)); - - afterEach(tearDownInjector); - - var compileButtons = (String markup) { - var el = _.compile('
'+markup+'
'); - scope.$digest(); - return el.querySelectorAll('button'); - }; - - //model -> UI - it('should work correctly set active class based on model', () { - var btns = compileButtons(''); - expect(btns[0]).not.toHaveClass('active'); - expect(btns[1]).not.toHaveClass('active'); - - scope.model = 2; - scope.$digest(); - expect(btns[0]).not.toHaveClass('active'); - expect(btns[1]).toHaveClass('active'); - }); - - //UI->model - it('should work correctly set active class based on model', () { - var btns = compileButtons(''); - expect(scope.model).toBeNull(); - - btns[0].click(); - scope.$digest(); - expect(scope.model).toEqual(1); - expect(btns[0]).toHaveClass('active'); - expect(btns[1]).not.toHaveClass('active'); - - btns[1].click(); - scope.$digest(); - expect(scope.model).toEqual(2); - expect(btns[1]).toHaveClass('active'); - expect(btns[0]).not.toHaveClass('active'); - }); - - it('should watch btn-radio values and update state accordingly', () { - scope.myValues = ["value1", "value2"]; - - var btns = compileButtons(''); - expect(btns[0]).not.toHaveClass('active'); - expect(btns[1]).not.toHaveClass('active'); - - scope.model = "value2"; - scope.$digest(); - expect(btns[0]).not.toHaveClass('active'); - expect(btns[1]).toHaveClass('active'); - - scope.myValues[1] = "value3"; - scope.model = "value3"; - scope.$digest(); - expect(btns[0]).not.toHaveClass('active'); - expect(btns[1]).toHaveClass('active'); - }); - }); -} \ No newline at end of file diff --git a/test/tests/collapse_tests.dart b/test/tests/collapse_tests.dart deleted file mode 100644 index c2dff04..0000000 --- a/test/tests/collapse_tests.dart +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) 2013 - 2014, akserg (Sergey Akopkokhyants) -// https://github.com/akserg/angular.dart.ui -// All rights reserved. Please see the LICENSE.md file. - -part of angular.ui.test; - -void collapseTests() { - - describe('Testing collapse:', () { - TestBed _; - Scope scope; - Transition transition; - - beforeEach(setUpInjector); - beforeEach(module((Module module) { - module.install(new CollapseModule()); - })); - beforeEach(inject((TestBed tb) => _ = tb)); - beforeEach(inject((Scope s) => scope = s)); - beforeEach(inject((Transition t) => transition = t)); - - afterEach(tearDownInjector); - - dom.Element element; - - beforeEach(() { - element = _.compile('
Some Content
'); - dom.document.body.append(element); - }); - - afterEach(() { - element.remove(); - }); - - it('should be hidden on initialization if isCollapsed = true without transition', () { - scope.isCollapsed = true; - scope.$digest(); - //No animation timeout here - expect(element.style.height, equals('0px')); - }); - }); -} \ No newline at end of file diff --git a/test/tests/dropdown_toggle_tests.dart b/test/tests/dropdown_toggle_tests.dart deleted file mode 100644 index b4bece4..0000000 --- a/test/tests/dropdown_toggle_tests.dart +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2013 - 2014, akserg (Sergey Akopkokhyants) -// https://github.com/akserg/angular.dart.ui -// All rights reserved. Please see the LICENSE.md file. -// -// Credits: Tonis Pool who wrote and donate that code. -// - -part of angular.ui.test; - -void dropdownToggleTests() { - describe('Testing dropdownToggle:', () { - TestBed _; - beforeEach(setUpInjector); - beforeEach(module((Module module) { - module.type(DropdownToggle); - })); - beforeEach(inject((TestBed tb) => _ = tb)); - - afterEach(tearDownInjector); - - dom.Element dropdown() { - return _.compile(''); - } - - it('should toggle on `a` click', () { - dom.Element elm = dropdown(); - expect(elm.classes.contains('open')).toBe(false); - elm.querySelector('a').click(); - expect(elm.classes.contains('open')).toBe(true); - elm.querySelector('a').click(); - expect(elm.classes.contains('open')).toBe(false); - }); - - it('should toggle on `ul` click', () { - dom.Element elm = dropdown(); - expect(elm.classes.contains('open')).toBe(false); - elm.querySelector('ul').click(); - expect(elm.classes.contains('open')).toBe(true); - elm.querySelector('ul').click(); - expect(elm.classes.contains('open')).toBe(false); - }); - - it('should close on elm click', () { - dom.Element elm = dropdown(); - elm.querySelector('a').click(); - elm.click(); - expect(elm.classes.contains('open')).toBe(false); - }); - - it('should close on document click', () { - dom.Element elm = dropdown(); - elm.querySelector('a').click(); - dom.document.body.click(); - expect(elm.classes.contains('open')).toBe(false); - }); - - it('should only allow one dropdown to be open at once', () { - dom.Element elm1 = dropdown(); - dom.Element elm2 = dropdown(); - elm1.querySelector('a').click(); - elm2.querySelector('a').click(); - expect(elm1.classes.contains('open')).toBe(false); - expect(elm2.classes.contains('open')).toBe(true); - }); - }); -} \ No newline at end of file diff --git a/test/tests/position_tests.dart b/test/tests/position_tests.dart deleted file mode 100644 index a1089d5..0000000 --- a/test/tests/position_tests.dart +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) 2013 - 2014, akserg (Sergey Akopkokhyants) -// https://github.com/akserg/angular.dart.ui -// All rights reserved. Please see the LICENSE.md file. - -part of angular.ui.test; - -void positionTests() { - - - describe('Testing position:', () { - - TestBed _; - Position position; - - beforeEach(setUpInjector); - beforeEach(module((Module module) { - module.install(new PositionModule()); - })); - beforeEach(inject((TestBed tb) => _ = tb)); - beforeEach(inject((Position p) => position = p)); - - afterEach(tearDownInjector); - - it('test position', () { - expect(position).toBeNotNull(); - }); - }); -} \ No newline at end of file diff --git a/test/ui_tests.dart b/test/ui_tests.dart deleted file mode 100644 index bd40946..0000000 --- a/test/ui_tests.dart +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) 2013 - 2014, akserg (Sergey Akopkokhyants) -// https://github.com/akserg/angular.dart.ui -// All rights reserved. Please see the LICENSE.md file. - -library angular.ui.test; - -/** - * Unit testing for Angular UI library. - */ - -import 'dart:html' as dom; -import 'dart:async'; -// -import 'package:unittest/html_enhanced_config.dart'; -import '_specs.dart'; -import 'package:angular/core_dom/module.dart'; - -import 'package:angular_ui/position.dart'; -import 'package:angular_ui/transition.dart'; -import 'package:angular_ui/buttons.dart'; -import 'package:angular_ui/collapse.dart'; -import 'package:angular_ui/dropdown_toggle.dart'; -import 'package:angular_ui/alert.dart'; -import 'package:angular_ui/timeout.dart'; - -part 'tests/position_tests.dart'; -part 'tests/transition_tests.dart'; -part 'tests/buttons_tests.dart'; -part 'tests/collapse_tests.dart'; -part 'tests/dropdown_toggle_tests.dart'; -part 'tests/alert_tests.dart'; -part 'tests/timeout_tests.dart'; - -void main() { - print('Running unit tests for Angular UI library.'); - useHtmlEnhancedConfiguration(); - group('All Tests:', () { - test('Position', () => positionTests()); - test('Timeout', () => timeoutTests()); - test('Transition', () => transitionTests()); - test('Buttons', () => buttonsTests()); - test('DropdownToggle', () => dropdownToggleTests()); - test('Collapse', () => collapseTests()); - test('Alert', () => alertTests()); - }); -} \ No newline at end of file diff --git a/test/ui_tests.html b/test/ui_tests.html deleted file mode 100644 index 77e97e4..0000000 --- a/test/ui_tests.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - Unit Tests for Angular Dart UI Library - - - - - - - diff --git a/test/unit/accordion/accordion_component_test.dart b/test/unit/accordion/accordion_component_test.dart new file mode 100644 index 0000000..2675222 --- /dev/null +++ b/test/unit/accordion/accordion_component_test.dart @@ -0,0 +1,116 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. + +part of angular_ui_test; + +testAccordionComponent() { + + DblClickPreventer dblClickPreventer; + Timeout timeout; + + describe("[Accordion Component]", () { + + beforeEach(setUpInjector); + afterEach(tearDownInjector); + afterEach((){ + timeout.flush(); + }); + + beforeEach(() { + module((Module _) => _ + ..install(new AccordionModule()) + ); + inject((DblClickPreventer d) => dblClickPreventer = d); + inject((Timeout t) => timeout = t); + //return loadTemplates(['/accordion/accordion.html', 'accordion/accordion_group.html']); + }); + + describe('Accordion', () { + + AccordionComponent ctrl; + AccordionGroupComponent group1, group2, group3; + + void createElements(AccordionConfig config) { + ctrl = new AccordionComponent(config); + group1 = new AccordionGroupComponent(ctrl, dblClickPreventer, timeout) + ..isOpen = true; + group2 = new AccordionGroupComponent(ctrl, dblClickPreventer, timeout) + ..isOpen = true; + group3 = new AccordionGroupComponent(ctrl, dblClickPreventer, timeout) + ..isOpen = true; + }; + + it('adds a the specified panel to the collection', async(inject(() { + createElements(new AccordionConfig()); + expect(ctrl.groups.length).toBe(3); + expect(ctrl.groups[0]).toBe(group1); + expect(ctrl.groups[1]).toBe(group2); + expect(ctrl.groups[2]).toBe(group3); + group2.detach(); + expect(ctrl.groups.length).toBe(2); + expect(ctrl.groups[0]).toBe(group1); + expect(ctrl.groups[1]).toBe(group3); + }))); + + it('should close other panels if close-others attribute is not defined', async(inject(() { + createElements(new AccordionConfig()); + group2.isOpen = true; + expect(group1.isOpen).toBe(false); + expect(group2.isOpen).toBe(true); + expect(group3.isOpen).toBe(false); + }))); + + it('should close other panels if close-others attribute is true', async(inject(() { + AccordionConfig config = new AccordionConfig(); + config.closeOthers = true; + createElements(config); + group3.isOpen = true; + expect(group1.isOpen).toBe(false); + expect(group2.isOpen).toBe(false); + expect(group3.isOpen).toBe(true); + }))); + + it('should not close other panels if close-others attribute is false', async(inject(() { + AccordionConfig config = new AccordionConfig(); + config.closeOthers = false; + createElements(config); + + group1.isOpen = true; + group2.isOpen = true; + group3.isOpen = true; + expect(group1.isOpen).toBe(true); + expect(group2.isOpen).toBe(true); + expect(group3.isOpen).toBe(true); + }))); + + describe('setting accordionConfig', () { + var originalCloseOthers; + beforeEach(inject((AccordionConfig accordionConfig) { + originalCloseOthers = accordionConfig.closeOthers; + accordionConfig.closeOthers = false; + })); + afterEach(inject((AccordionConfig accordionConfig) { + // return it to the original value + accordionConfig.closeOthers = originalCloseOthers; + })); + + it('should not close other panels if accordionConfig.closeOthers is false', () { + ctrl.closeOthers(group2); + expect(group1.isOpen).toBe(true); + expect(group2.isOpen).toBe(true); + expect(group3.isOpen).toBe(true); + }); + }); + + describe('removeGroup', () { + it('should remove the specified panel', () { + ctrl.removeGroup(group2); + expect(ctrl.groups.length).toBe(2); + expect(ctrl.groups[0]).toBe(group1); + expect(ctrl.groups[1]).toBe(group3); + }); + }); + }); + }); +} diff --git a/test/unit/accordion/accordion_group_component_test.dart b/test/unit/accordion/accordion_group_component_test.dart new file mode 100644 index 0000000..2d2136a --- /dev/null +++ b/test/unit/accordion/accordion_group_component_test.dart @@ -0,0 +1,397 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. + +part of angular_ui_test; + +testAccordionGroupComponent() { + + Timeout timeout; + + describe("[Accordion Group Component]", () { + + beforeEach(setUpInjector); + afterEach(tearDownInjector); + + beforeEach(() { + module((Module _) => _ + ..install(new AccordionModule()) + ); + inject((Timeout t) => timeout = t); + //return loadTemplates(['/accordion/accordion.html', 'accordion/accordion_group.html']); + }); + + dom.Element findGroupLink(List groups, int index) { + return groups[index].querySelector('.accordion-toggle'); + }; + + dom.Element findGroupBody(List groups, int index) { + return groups[index].querySelector('.panel-body'); + }; + + describe('with static panels', () { + + getHtml() { + return r''' + + Content 1 + Content 2 +'''; + } + + it('adds a the specified panel to the collection', compileComponent( + getHtml(), + {}, + (Scope scope, dom.HtmlElement shadowRoot) { + var groups = shadowRoot.querySelectorAll('accordion-group'); + + expect(groups.length).toEqual(2); + expect(groups[0].attributes['heading']).toEqual('title 1'); + expect(findGroupBody(groups, 0).text.trim()).toEqual('Content 1'); + expect(groups[1].attributes['heading']).toEqual('title 2'); + expect(findGroupBody(groups, 1).text.trim()).toEqual('Content 2'); + + timeout.flush(); + })); + + it('should change selected element on click', compileComponent( + getHtml(), + {}, + (Scope scope, dom.HtmlElement shadowRoot) { + var groups = shadowRoot.querySelectorAll('accordion-group'); + + findGroupLink(groups, 0).click(); + timeout.flush(); + digest(); + AccordionGroupComponent group0 = ngProbe(groups[0]).directives.firstWhere((d) => d is AccordionGroupComponent); + AccordionGroupComponent group1 = ngProbe(groups[1]).directives.firstWhere((d) => d is AccordionGroupComponent); + expect(group0.isOpen).toBe(true); + expect(group1.isOpen).toBe(false); + + findGroupLink(groups, 1).click(); + timeout.flush(); + digest(); + expect(group0.isOpen).toBe(false); + expect(group1.isOpen).toBe(true); + + timeout.flush(); + })); + + it('should toggle element on click', compileComponent( + getHtml(), + {}, + (Scope scope, dom.HtmlElement shadowRoot) { + var groups = shadowRoot.querySelectorAll('accordion-group'); + AccordionGroupComponent group0 = ngProbe(groups[0]).directives.firstWhere((d) => d is AccordionGroupComponent); + AccordionGroupComponent group1 = ngProbe(groups[1]).directives.firstWhere((d) => d is AccordionGroupComponent); + + findGroupLink(groups, 0).click(); + timeout.flush(); + digest(); + expect(group0.isOpen).toBe(true); + + findGroupLink(groups, 0).click(); + timeout.flush(); + digest(); + expect(group0.isOpen).toBe(false); + + timeout.flush(); + })); + }); + + describe('with dynamic panels', () { + getHtml() { + return r''' + + {{group.content}} + +'''; + } + + getModel() { + return [ + {'name': 'title 1', 'content': 'Content 1'}, + {'name': 'title 2', 'content': 'Content 2'} + ]; + } + + it('should have no panels initially', compileComponent( + getHtml(), + {}, + (Scope scope, dom.HtmlElement shadowRoot) { + var groups = shadowRoot.querySelectorAll('accordion-group'); + + expect(groups.length).toEqual(0); + + timeout.flush(); + })); + + it('should have a panel for each model item', compileComponent( + getHtml(), + {}, + (Scope scope, dom.HtmlElement shadowRoot) { + + scope.context['groups'] = getModel(); + digest(); + + var groups = shadowRoot.querySelectorAll('accordion-group'); + AccordionGroupComponent group0 = ngProbe(groups[0]).directives.firstWhere((d) => d is AccordionGroupComponent); + AccordionGroupComponent group1 = ngProbe(groups[1]).directives.firstWhere((d) => d is AccordionGroupComponent); + + expect(groups.length).toEqual(2); + expect(groups[0].attributes['heading']).toEqual('title 1'); + expect(groups[0].text.trim()).toEqual('Content 1'); + expect(groups[1].attributes['heading']).toEqual('title 2'); + expect(groups[1].text.trim()).toEqual('Content 2'); + + timeout.flush(); + })); + +// it('should react properly on removing items from the model', compileComponent( +// getHtml(), +// {}, +// (Scope scope, dom.HtmlElement shadowRoot) { +// +// scope.context['groups'] = getModel(); +// digest(); +// var groups = shadowRoot.querySelectorAll('accordion-group'); +// expect(groups.length).toEqual(2); +// +// scope.context['groups'].removeAt(0); +// digest(); +// groups = shadowRoot.querySelectorAll('accordion-group'); +// expect(groups.length).toEqual(1); +// })); + }); + + describe('is-open attribute', () { + getHtml() { + return r''' + + Content 1 + Content 2 +'''; + }; + + getScope() { + return {'open': { 'first': false, 'second': true }}; + } + + it('should open the panel with isOpen set to true', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var groups = shadowRoot.querySelectorAll('accordion-group'); + AccordionGroupComponent group0 = ngProbe(groups[0]).directives.firstWhere((d) => d is AccordionGroupComponent); + AccordionGroupComponent group1 = ngProbe(groups[1]).directives.firstWhere((d) => d is AccordionGroupComponent); + + expect(group0.isOpen).toBe(false); + expect(group1.isOpen).toBe(true); + + timeout.flush(); + })); + + it('should toggle variable on element click', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var groups = shadowRoot.querySelectorAll('accordion-group'); + AccordionGroupComponent group0 = ngProbe(groups[0]).directives.firstWhere((d) => d is AccordionGroupComponent); + AccordionGroupComponent group1 = ngProbe(groups[1]).directives.firstWhere((d) => d is AccordionGroupComponent); + + findGroupLink(groups, 0).click(); + timeout.flush(); + digest(); + expect(scope.context['open']['first']).toBe(true); + + findGroupLink(groups, 0).click(); + timeout.flush(); + digest(); + expect(scope.context['open']['second']).toBe(false); + + timeout.flush(); + })); + }); + + describe('is-open attribute with dynamic content', () { + getHtml() { + return r''' + +
{{item}}
+ Static content +
'''; + } + + getScope() { + return { + 'items': ['Item 1', 'Item 2', 'Item 3'], + 'open1': true, + 'open2': false + }; + } + + it('should have visible panel body when the group with isOpen set to true', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + dom.document.body.append(shadowRoot); + digest(); + microLeap(); + var groups = shadowRoot.querySelectorAll('accordion-group'); + + expect(groups[0].querySelector('.panel-collapse').clientHeight).not.toBe(0); + // This code commented out because the clientHeight of panel could be equals: + // - 18px on windows + // - 19px on linux + //expect(groups[1].querySelector('.panel-collapse').clientHeight).toBe(18); + shadowRoot.remove(); + + timeout.flush(); + })); + }); + + describe('is-open attribute with dynamic groups', () { + getHtml() { + return r''' + + {{group.content}} +'''; + } + + getScope() { + return { + 'groups': [ + {'name': 'title 1', 'content': 'Content 1', 'open': false}, + {'name': 'title 2', 'content': 'Content 2', 'open': true} + ] + }; + } + + it('should have visible group body when the group with isOpen set to true', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + + + var groups = shadowRoot.querySelectorAll('accordion-group'); + AccordionGroupComponent group0 = ngProbe(groups[0]).directives.firstWhere((d) => d is AccordionGroupComponent); + AccordionGroupComponent group1 = ngProbe(groups[1]).directives.firstWhere((d) => d is AccordionGroupComponent); + + expect(group0.isOpen).toBe(false); + expect(group1.isOpen).toBe(true); + + timeout.flush(); + })); + + it('should toggle element on click', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + microLeap(); + + var groups = shadowRoot.querySelectorAll('accordion-group'); + AccordionGroupComponent group0 = ngProbe(groups[0]).directives.firstWhere((d) => d is AccordionGroupComponent); + AccordionGroupComponent group1 = ngProbe(groups[1]).directives.firstWhere((d) => d is AccordionGroupComponent); + + findGroupLink(groups, 0).click(); + timeout.flush(); + digest(); + expect(group0.isOpen).toBe(true); + expect(scope.context['groups'][0]['open']).toBe(true); + + findGroupLink(groups, 0).click(); + timeout.flush(); + digest(); + expect(group0.isOpen).toBe(false); + expect(scope.context['groups'][0]['open']).toBe(false); + + timeout.flush(); + })); + }); + + describe('`is-disabled` attribute', () { + + getHtml() { + return r''' + + Content 1 +'''; + } + + getScope() { + return { 'disabled': true }; + } + + it('should open the panel with isOpen set to true', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var group = shadowRoot.querySelector('accordion-group'); + AccordionGroupComponent groupBody = ngProbe(group).directives.firstWhere((d) => d is AccordionGroupComponent); + + expect(groupBody.isOpen).toBeFalsy(); + + timeout.flush(); + })); + + it('should not toggle if disabled', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var group = shadowRoot.querySelector('accordion-group'); + AccordionGroupComponent groupBody = ngProbe(group).directives.firstWhere((d) => d is AccordionGroupComponent); + + group.querySelector('.accordion-toggle').click(); + timeout.flush(); + digest(); + expect(groupBody.isOpen).toBeFalsy(); + + timeout.flush(); + })); + + it('should toggle after enabling', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var group = shadowRoot.querySelector('accordion-group'); + AccordionGroupComponent groupBody = ngProbe(group).directives.firstWhere((d) => d is AccordionGroupComponent); + + scope.context['disabled'] = false; + digest(); + expect(groupBody.isOpen).toBeFalsy(); + + group.querySelector('.accordion-toggle').click(); + timeout.flush(); + digest(); + expect(groupBody.isOpen).toBeTruthy(); + + timeout.flush(); + })); + }); + + describe('accordion-heading element', () { + getHtml() { + return r''' + + + 123 + Body + +'''; + } + + it('transcludes the content into the heading link', compileComponent( + getHtml(), + {}, + (Scope scope, dom.HtmlElement shadowRoot) { + dom.document.body.append(shadowRoot); + digest(); + microLeap(); + var group = shadowRoot.querySelector('accordion-group'); + expect(group.querySelector('.accordion-toggle').text).toEqual('123'); + + timeout.flush(); + })); + }); + }); +} diff --git a/test/unit/alert/alert_component_test.dart b/test/unit/alert/alert_component_test.dart new file mode 100644 index 0000000..a3e5269 --- /dev/null +++ b/test/unit/alert/alert_component_test.dart @@ -0,0 +1,56 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. + +part of angular_ui_test; + +testAlertComponent() { + describe("[AlertComponent]", () { + TestBed _; + Scope scope; + + beforeEach(setUpInjector); + afterEach(tearDownInjector); + + beforeEach(() { + module((Module _) => _ + ..install(new AlertModule()) + ); +// return loadTemplates(['/alert/alert.html']); + }); + + String getHtml() { + return "{{alert.msg}}" + + ""; + }; + + Map getScopeContent() { + return {'alerts': [ + { 'msg':'foo', 'type':'success'}, + { 'msg':'bar', 'type':'error'}, + { 'msg':'baz'} + ]}; + }; + + it("should generate alerts using ng-repeat", compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final alerts = shadowRoot.querySelectorAll('alert'); + expect(alerts.length).toEqual(3); + })); + + it('should show the alert content', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final alerts = shadowRoot.querySelectorAll('alert'); + + for (var i = 0; i < alerts.length; i++) { + dom.Element el = shadowRoot.querySelectorAll('alert')[i]; + expect(el.text).toEqual(scope.context['alerts'][i]['msg']); + } + })); + }); +} diff --git a/test/unit/buttons/checkbox_component_test.dart b/test/unit/buttons/checkbox_component_test.dart new file mode 100644 index 0000000..7b17d85 --- /dev/null +++ b/test/unit/buttons/checkbox_component_test.dart @@ -0,0 +1,96 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. + +part of angular_ui_test; + +testCheckboxComponent() { + describe("[CheckboxComponent]", () { + TestBed _; + Scope scope; + + beforeEach(setUpInjector); + afterEach(tearDownInjector); + + beforeEach(() { + module((Module _) => _ + ..install(new ButtonModule()) + ); + }); + + describe("[model -> UI]", () { + it('should work correctly with default model values', compileComponent( + '', + {'model':false}, + (Scope scope, dom.HtmlElement shadowRoot) { + final btn = shadowRoot.querySelector("button[btn-checkbox]"); + expect(btn).not.toHaveClass('active'); + + scope.context['model'] = true; + digest(); + expect(btn).toHaveClass('active'); + })); + + it('should bind custom model values', compileComponent( + '', + {'model':1}, + (Scope scope, dom.HtmlElement shadowRoot) { + final btn = shadowRoot.querySelector("button[btn-checkbox]"); + expect(btn).toHaveClass('active'); + + scope.context['model'] = 0; + digest(); + expect(btn).not.toHaveClass('active'); + })); + }); + + describe("[UI-> model]", () { + it('should toggle default model values on click', compileComponent( + '', + {'model':false}, + (Scope scope, dom.HtmlElement shadowRoot) { + final btn = shadowRoot.querySelector("button[btn-checkbox]"); + + btn.click(); + expect(scope.context['model']).toEqual(true); + expect(btn).toHaveClass('active'); + + btn.click(); + expect(scope.context['model']).toEqual(false); + expect(btn).not.toHaveClass('active'); + })); + + it('should toggle custom model values on click', compileComponent( + '', + {'model':false}, + (Scope scope, dom.HtmlElement shadowRoot) { + final btn = shadowRoot.querySelector("button[btn-checkbox]"); + + btn.click(); + expect(scope.context['model']).toEqual(1); + expect(btn).toHaveClass('active'); + + btn.click(); + expect(scope.context['model']).toEqual(0); + expect(btn).not.toHaveClass('active'); + })); + + it('should monitor true / false value changes', compileComponent( + '', + {'model':1, 'trueVal':1}, + (Scope scope, dom.HtmlElement shadowRoot) { + final btn = shadowRoot.querySelector("button[btn-checkbox]"); + + expect(btn).toHaveClass('active'); + expect(scope.context['model']).toEqual(1); + + scope.context['model'] = 2; + scope.context['trueVal'] = 2; + digest(); + + expect(btn).toHaveClass('active'); + expect(scope.context['model']).toEqual(2); + })); + }); + }); +} diff --git a/test/unit/buttons/radiobutton_component_test.dart b/test/unit/buttons/radiobutton_component_test.dart new file mode 100644 index 0000000..49e6853 --- /dev/null +++ b/test/unit/buttons/radiobutton_component_test.dart @@ -0,0 +1,79 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. + +part of angular_ui_test; + +testRadiobuttonComponent() { + describe("[RadiobuttonComponent]", () { + TestBed _; + Scope scope; + + beforeEach(setUpInjector); + afterEach(tearDownInjector); + + beforeEach(() { + module((Module _) => _ + ..install(new ButtonModule()) + ); + }); + + describe("[model -> UI]", () { + it('should work correctly set active class based on model', compileComponent( + '', + {'model':false}, + (Scope scope, dom.HtmlElement shadowRoot) { + final btns = shadowRoot.querySelectorAll("button[btn-radio]"); + expect(btns[0]).not.toHaveClass('active'); + expect(btns[1]).not.toHaveClass('active'); + + scope.context['model'] = 2; + digest(); + expect(btns[0]).not.toHaveClass('active'); + expect(btns[1]).toHaveClass('active'); + })); + }); + + describe("[UI -> model]", () { + it('should work correctly set active class based on model', compileComponent( + '', + {}, + (Scope scope, dom.HtmlElement shadowRoot) { + final btns = shadowRoot.querySelectorAll("button[btn-radio]"); + expect(scope.context['model']).toBeNull(); + + btns[0].click(); + digest(); + expect(scope.context['model']).toEqual(1); + expect(btns[0]).toHaveClass('active'); + expect(btns[1]).not.toHaveClass('active'); + + btns[1].click(); + digest(); + expect(scope.context['model']).toEqual(2); + expect(btns[1]).toHaveClass('active'); + expect(btns[0]).not.toHaveClass('active'); + })); + + it('should watch btn-radio values and update state accordingly', compileComponent( + '', + {'myValues':["value1", "value2"]}, + (Scope scope, dom.HtmlElement shadowRoot) { + final btns = shadowRoot.querySelectorAll("button[btn-radio]"); + expect(btns[0]).not.toHaveClass('active'); + expect(btns[1]).not.toHaveClass('active'); + + scope.context['model'] = "value2"; + digest(); + expect(btns[0]).not.toHaveClass('active'); + expect(btns[1]).toHaveClass('active'); + + scope.context['myValues'][1] = "value3"; + scope.context['model'] = "value3"; + digest(); + expect(btns[0]).not.toHaveClass('active'); + expect(btns[1]).toHaveClass('active'); + })); + }); + }); +} diff --git a/test/unit/carousel/carousel_component_test.dart b/test/unit/carousel/carousel_component_test.dart new file mode 100644 index 0000000..6d86559 --- /dev/null +++ b/test/unit/carousel/carousel_component_test.dart @@ -0,0 +1,384 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. + +part of angular_ui_test; + +testCarouselComponent() { + describe("[CarouselComponent]", () { + TestBed _; + Scope scope; + Timeout timeout; + + beforeEach(setUpInjector); + afterEach(tearDownInjector); + + beforeEach(() { + module((Module _) => _ + ..install(new TimeoutModule()) + ..install(new CarouselModule()) + ); + inject((Timeout t) => timeout = t); + //return loadTemplates(['/carousel/carousel.html', '/carousel/slide.html', '/carousel/carousel.css']); + //return loadTemplates(['/carousel/slide.css']); + }); + + describe('basics', () { + String getHtml() { + return ''' + + {{slide['content']}} +'''; + }; + + Map getScope() { + return {'slides': [ + {'active':false,'content':'one'}, + {'active':false,'content':'two'}, + {'active':false,'content':'three'} + ], + 'interval':50000, + 'nopause': null}; + } + + void testSlideActive(Scope scope, slideIndex) { + for (var i = 0; i < scope.context['slides'].length; i++) { + if (i == slideIndex) { + expect(scope.context['slides'][i]['active']).toBe(true); + } else { + expect(scope.context['slides'][i]['active']).toBe(false); + } + } + } + + + it('should set the selected slide to active = true', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + + expect(scope.context['slides'][0]['content']).toEqual('one'); + + scope.apply("slides[0]['active']=true"); + testSlideActive(scope, 0); + + microLeap(); + timeout.flush(cancel:true); + })); + + it('should create clickable prev nav button', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + microLeap(); + digest(); + + var navPrev = ngQuery(shadowRoot, 'a.left'); + var navNext = ngQuery(shadowRoot, 'a.right'); + + expect(navPrev.length).toBe(1); + expect(navNext.length).toBe(1); + + microLeap(); + timeout.flush(cancel:true); + })); + + it('should display clickable slide indicators', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + microLeap(); + digest(); + + var indicators = ngQuery(shadowRoot, 'ol.carousel-indicators > li'); + expect(indicators.length).toBe(3); + + microLeap(); + timeout.flush(cancel:true); + })); + + it('should hide navigation when only one slide', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + microLeap(); + digest(); + + scope.context['slides'] = [{'active':false,'content':'one'}]; + digest(); + + var indicators = ngQuery(shadowRoot, 'ol.carousel-indicators > li'); + expect(indicators.length).toBe(0); + + var navNext = ngQuery(shadowRoot, 'a.right')[0]; + expect(navNext).toHaveClass('ng-hide'); + + var navPrev = ngQuery(shadowRoot, 'a.left')[0]; + expect(navPrev).toHaveClass('ng-hide'); + + microLeap(); + timeout.flush(cancel:true); + })); + + it('should show navigation when there are 3 slides', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + microLeap(); + digest(); + + var indicators = ngQuery(shadowRoot, 'ol.carousel-indicators > li'); + expect(indicators.length).not.toBe(0); + + var navNext = ngQuery(shadowRoot, 'a.right')[0]; + expect(navNext).not.toHaveClass('ng-hide'); + + var navPrev = ngQuery(shadowRoot, 'a.left')[0]; + expect(navPrev).not.toHaveClass('ng-hide'); + + microLeap(); + timeout.flush(cancel:true); + })); + + it('should go to next when clicking next button', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + microLeap(); + digest(); + + var navNext = ngQuery(shadowRoot, 'a.right')[0]; + testSlideActive(scope, 0); + + navNext.click(); + digest(); + testSlideActive(scope, 1); + + navNext.click(); + digest(); + testSlideActive(scope, 2); + + navNext.click(); + digest(); + testSlideActive(scope, 0); + + microLeap(); + timeout.flush(cancel:true); + })); + + it('should go to prev when clicking prev button', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + microLeap(); + digest(); + + var navPrev = ngQuery(shadowRoot, 'a.left')[0]; + testSlideActive(scope, 0); + + navPrev.click(); + digest(); + testSlideActive(scope, 2); + + navPrev.click(); + digest(); + testSlideActive(scope, 1); + + navPrev.click(); + digest(); + testSlideActive(scope, 0); + + microLeap(); + timeout.flush(cancel:true); + })); + + it('should select a slide when clicking on slide indicators', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + microLeap(); + digest(); + + var indicators = ngQuery(shadowRoot, 'ol.carousel-indicators > li'); + indicators[1].click(); + digest(); + testSlideActive(scope, 1); + + microLeap(); + timeout.flush(cancel:true); + })); + +// it('shouldnt go forward if interval is NaN or negative', compileComponent( +// getHtml(), +// getScope(), +// (Scope scope, dom.HtmlElement shadowRoot) { +// microLeap(); +// digest(); +// +// testSlideActive(scope, 0); +// +// scope.apply('interval = -1'); +// //no timeout to flush, interval watch doesn't make a new one when interval is invalid +// testSlideActive(scope, 0); +// +// scope.apply('interval = 1000'); +// microLeap(); +// timeout.flush(); +// testSlideActive(scope, 1); +// +// scope.apply('interval = false'); +// testSlideActive(scope, 1); +// +// scope.apply('interval = 1000'); +// microLeap(); +// timeout.flush(); +// testSlideActive(scope, 2); +// +// microLeap(); +// timeout.flush(cancel:true); +// })); + + it('should bind the content to slides', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + microLeap(); + digest(); + + var contents = ngQuery(shadowRoot, 'div.item'); + + expect(contents.length).toBe(3); + expect(contents[0].text.trim()).toEqual('one'); + expect(contents[1].text.trim()).toEqual('two'); + expect(contents[2].text.trim()).toEqual('three'); + + scope.apply(() { + scope.context['slides'][0]['content'] = 'what'; + scope.context['slides'][1]['content'] = 'no'; + scope.context['slides'][2]['content'] = 'maybe'; + }); + + expect(contents[0].text.trim()).toEqual('what'); + expect(contents[1].text.trim()).toEqual('no'); + expect(contents[2].text.trim()).toEqual('maybe'); + + microLeap(); + timeout.flush(cancel:true); + })); + +// it('should be playing by default and cycle through slides', compileComponent( +// getHtml(), +// getScope(), +// (Scope scope, dom.HtmlElement shadowRoot) { +// microLeap(); +// digest(); +// +// testSlideActive(scope, 0); +// +// timeout.flush(); +// testSlideActive(scope, 1); +// +// timeout.flush(); +// testSlideActive(scope, 2); +// +// timeout.flush(); +// testSlideActive(scope, 0); +// +// microLeap(); +// timeout.flush(cancel:true); +// })); + +// it('should pause and play on mouseover', compileComponent( +// getHtml(), +// getScope(), +// (Scope scope, dom.HtmlElement shadowRoot) { +// microLeap(); +// digest(); +// +// testSlideActive(scope, 0); +// +// timeout.flush(); +// testSlideActive(scope, 1); +// +// _.triggerEvent(shadowRoot, 'mouseenter'); +// expect(timeout.flush).toThrowWith();//pause should cancel current timeout +// testSlideActive(scope, 1); +// +// _.triggerEvent(shadowRoot, 'mouseleave'); +// timeout.flush(); +// testSlideActive(scope, 2); +// +// microLeap(); +// timeout.flush(cancel:true); +// })); + +// it('should not pause on mouseover if noPause', compileComponent( +// getHtml(), +// getScope(), +// (Scope scope, dom.HtmlElement shadowRoot) { +// microLeap(); +// digest(); +// +// scope.apply('nopause = true'); +// testSlideActive(scope, 0); +// +// _.triggerEvent(shadowRoot, 'mouseenter'); +// timeout.flush(); +// testSlideActive(scope, 1); +// +// _.triggerEvent(shadowRoot, 'mouseleave'); +// timeout.flush(); +// testSlideActive(scope, 2); +// +// microLeap(); +// timeout.flush(cancel:true); +// })); + + it('should remove slide from dom and change active slide', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + microLeap(); + digest(); + + scope.apply('slides[2]["active"] = true'); + testSlideActive(scope, 2); + + scope.apply('slides.removeRange(0,1)'); + expect(ngQuery(shadowRoot, 'div.item').length).toBe(2); + testSlideActive(scope, 1); + + scope.apply('slides.removeRange(0,1)'); + expect(ngQuery(shadowRoot, 'div.item').length).toBe(1); + testSlideActive(scope, 0); + + microLeap(); + timeout.flush(cancel:true); + })); + +// it('should change dom when you reassign ng-repeat slides array', compileComponent( +// getHtml(), +// getScope(), +// (Scope scope, dom.HtmlElement shadowRoot) { +// microLeap(); +// digest(); +// +// scope.context['slides'] = [ +// {'active':false,'content':'new1'}, +// {'active':false,'content':'new2'} +// ]; +// +// scope.apply(); +// +// var contents = ngQuery(shadowRoot, 'div.item'); +// expect(contents.length).toBe(2); +// expect(contents[0].text).toBe('new1'); +// expect(contents[1].text).toBe('new2'); +// +// microLeap(); +// timeout.flush(cancel:true); +// })); + + }); + }); +} diff --git a/test/unit/collapse/collapse_component_test.dart b/test/unit/collapse/collapse_component_test.dart new file mode 100644 index 0000000..8afc628 --- /dev/null +++ b/test/unit/collapse/collapse_component_test.dart @@ -0,0 +1,118 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. + +part of angular_ui_test; + +testCollapseComponent() { + describe("[CollapseComponent]", () { + TestBed _; + Scope scope; + Timeout timeout; + + beforeEach(setUpInjector); + afterEach(tearDownInjector); + + beforeEach(() { + module((Module _) => _ + ..install(new CollapseModule()) + ); + inject((Timeout t) => timeout = t); + }); + + String getHtml() { + return '
Some Content
'; + }; + + describe("[Collapse with static content]", () { + + it('should be hidden on initialization if isCollapsed = true without transition', compileComponent( + getHtml(), + {'isCollapsed':true}, + (Scope scope, dom.HtmlElement shadowRoot) { + var collapse = shadowRoot.querySelector('[collapse]'); + //No animation timeout here + expect(collapse.style.height).toEqual('0px'); + })); + + it('should collapse if isCollapsed = true with animation on subsequent use', compileComponent( + getHtml(), + {'isCollapsed':false}, + (Scope scope, dom.HtmlElement shadowRoot) { + var collapse = shadowRoot.querySelector('[collapse]'); + scope.context['isCollapsed'] = true; + digest(); + timeout.flush(); + expect(collapse.style.height).toEqual('0px'); + })); + + it('should be shown on initialization if isCollapsed = false without transition', compileComponent( + getHtml(), + {'isCollapsed':false}, + (Scope scope, dom.HtmlElement shadowRoot) { + var collapse = shadowRoot.querySelector('[collapse]'); + //No animation timeout here + expect(collapse.style.height).not.toEqual('0px'); + })); + +// it('should expand if isCollapsed = false with animation on subsequent use', compileComponent( +// getHtml(), +// {'isCollapsed':false}, +// (Scope scope, dom.HtmlElement shadowRoot) { +// var collapse = shadowRoot.querySelector('[collapse]'); +// scope.context['isCollapsed'] = true; +// digest(); +// scope.context['isCollapsed'] = false; +// digest(); +// timeout.flush(); +// expect(collapse.style.height).not.toEqual('0px'); +// })); + + it('should expand if isCollapsed = true with animation on subsequent uses', compileComponent( + getHtml(), + {'isCollapsed':false}, + (Scope scope, dom.HtmlElement shadowRoot) { + var collapse = shadowRoot.querySelector('[collapse]'); + scope.context['isCollapsed'] = true; + digest(); + scope.context['isCollapsed'] = false; + digest(); + scope.context['isCollapsed'] = true; + digest(); + timeout.flush(); + expect(collapse.style.height).toEqual('0px'); + + Collapse collapseComponent = ngProbe(collapse).directives.firstWhere((d) => d is Collapse); + collapseComponent.currentTransition.complete(true); + expect(collapse.style.height).toEqual('0px'); + })); + }); + +// describe("[Collapse with dynamic content]", () { +// it('should grow accordingly when content size inside collapse increases', compileComponent( +// '

Initial content

Additional content
', +// {'isCollapsed':true, 'hid':true}, +// (Scope scope, dom.HtmlElement shadowRoot) { +// var collapse = shadowRoot.querySelector('[collapse]'); +// Collapse collapseComponent = ngProbe(collapse).directives.firstWhere((d) => d is Collapse); +// collapseComponent.isCollapsed = false; +// digest(); +// var collapseHeight = collapse.clientHeight; +// scope.context['hid'] = false; +// digest(); +// expect(collapse.clientHeight).toBeGreaterThan(collapseHeight); +// })); +// +// it('should shrink accordingly when content size inside collapse decreases', compileComponent( +// '

Initial content

Additional content
', +// {'isCollapsed':false, 'hid':false}, +// (Scope scope, dom.HtmlElement shadowRoot) { +// var collapse = shadowRoot.querySelector('[collapse]'); +// var collapseHeight = collapse.clientHeight; +// scope.context['hid'] = true; +// digest(); +// expect(collapse.clientHeight).toBeLessThan(collapseHeight); +// })); +// }); + }); +} diff --git a/test/unit/datepicker/datepicker_component_test.dart b/test/unit/datepicker/datepicker_component_test.dart new file mode 100644 index 0000000..0635b38 --- /dev/null +++ b/test/unit/datepicker/datepicker_component_test.dart @@ -0,0 +1,1596 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. + +part of angular_ui_test; + +testDatepickerComponent() { + + describe("[DatepickerComponent]", () { + + TestBed _; + + beforeEach(setUpInjector); + afterEach(tearDownInjector); + + beforeEach(() { + module((Module _) => _ + ..install(new DatepickerModule()) + ); + inject((TestBed tb) => _ = tb); +// return loadTemplates(['/datepicker/datepicker.html', '/datepicker/popup.html']); + }); + + String getHtml({String extra:''}) { + return r''; + }; + + Map getScopeContent() { + return {'date': new DateTime(2010, 9, 30, 15, 30)}; + }; + + String getTitle(dom.Element element) { + List ths = element.querySelectorAll('th'); + List btns = ths[1].querySelectorAll('button') as List; + return btns.first.text; + } + + void clickTitleButton(dom.Element element, [int times = 1]) { + List els = element.querySelectorAll('th'); + dom.ButtonElement btn = els[1].querySelector('button'); + for (var i = 0; i < times; i++) { + btn.click(); + } + } + + void clickPreviousButton(dom.Element element, [int times = 1]) { + List els = element.querySelectorAll('th'); + List btns = els[0].querySelectorAll('button') as List; + for (var i = 0; i < times; i++) { + _.triggerEvent(btns.first, 'click'); + } + } + + void clickNextButton(dom.Element element, [int times = 1]) { + List els = element.querySelectorAll('th'); + List btns = els[2].querySelectorAll('button') as List; + for (var i = 0; i < times; i++) { + _.triggerEvent(btns.first, 'click'); + } + } + + dom.TableRowElement getLabelsRow(dom.Element element) { + List rows = element.querySelectorAll('thead > tr') as List; + return rows[1]; + } + + List getLabels(dom.Element element) { + List els = getLabelsRow(element).querySelectorAll('td'); // TODO: Must be TH + + var labels = []; + for (var i = 1; i < els.length; i++) { + labels.add(els[i].text); + } + return labels; + } + + List getWeeks(dom.Element element) { + List rows = element.querySelectorAll('tbody > tr'); + List weeks = []; + for (var i = 0; i < rows.length; i++) { + weeks.add(rows[i].querySelectorAll('td').first.text); + } + return weeks; + } + + List getOptions(dom.Element element) { + List tr = element.querySelectorAll('tbody > tr'); + List rows = []; + + for (var j = 0; j < tr.length; j++) { + List cols = tr[j].querySelectorAll('td'); + List days = []; + for (var i = 1; i < cols.length; i++) { + days.add(cols[i].querySelector('button').text); + } + rows.add(days); + } + return rows; + } + + dom.Element _getOptionEl(dom.Element element, rowIndex, colIndex) { + return element.querySelectorAll('tbody > tr')[rowIndex].querySelectorAll('td')[colIndex + 1]; + } + + void clickOption(dom.Element element, rowIndex, colIndex) { + _getOptionEl(element, rowIndex, colIndex).querySelector('button').click(); + } + + bool isDisabledOption(dom.Element element, rowIndex, colIndex) { + return (_getOptionEl(element, rowIndex, colIndex).querySelector('button') as dom.ButtonElement).disabled; + } + + List getAllOptionsEl(dom.Element element) { + var tr = element.querySelectorAll('tbody > tr'); + List rows = []; + for (var i = 0; i < tr.length; i++) { + List tds = tr[i].querySelectorAll('td'); + List cols = []; + for (var j = 0; j < tds.length - 1; j++) { + cols.add(tds[j + 1]); + } + rows.add(cols); + } + return rows; + } + + void expectSelectedElement(dom.Element element, row, col ) { + var options = getAllOptionsEl(element); + for (var i = 0; i < options.length; i++) { + List optionsRow = options[i]; + for (var j = 0; j < optionsRow.length; j ++) { + dom.ButtonElement btn = optionsRow[j].querySelector('button'); + if (btn.classes.contains('btn-info')) { + expect(i == row && j == col).toBeTruthy(); + } + } + } + } + + describe('', () { + it('is a \'\' element', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + expect(datepicker.children.length).toBe(1); + expect(datepicker.children[0].tagName).toEqual('TABLE'); + expect(datepicker.children[0].querySelectorAll('thead > tr').length).toBe(2); + })); + + it('shows the correct title', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + expect(getTitle(datepicker)).toEqual('September 2010'); + })); + + it('shows the label row & the correct day labels', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + expect(getLabelsRow(datepicker).style.display).not.toEqual('none'); + expect(getLabels(datepicker)).toEqual(['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']); + })); + + it('renders the calendar days correctly', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + expect(getOptions(datepicker)).toEqual([ + ['30', '31', '01', '02', '03', '04', '05'], + ['06', '07', '08', '09', '10', '11', '12'], + ['13', '14', '15', '16', '17', '18', '19'], + ['20', '21', '22', '23', '24', '25', '26'], + ['27', '28', '29', '30', '01', '02', '03'] + ]); + })); + + it('renders the week numbers based on ISO 8601', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + expect(getWeeks(datepicker)).toEqual(['35', '36', '37', '38', '39']); + })); + + it('value is correct', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + + expect(scope.context['date']).toEqual(new DateTime(2010, 9, 30, 15, 30)); + })); + + it('has \'selected\' only the correct day', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + expectSelectedElement(datepicker, 4, 3); + })); + + it('has no \'selected\' day when model is cleared', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + scope.context['date'] = null; + scope.apply(); + + expect(scope.context['date']).toBe(null); + expectSelectedElement(datepicker, null, null ); + })); + + it('does not change current view when model is cleared', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + scope.context['date'] = null; + scope.rootScope.apply(); + + expect(scope.context['date']).toBe(null); + expect(getTitle(datepicker)).toEqual('September 2010'); + })); + + it('\'disables\' visible dates from other months', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + var options = getAllOptionsEl(datepicker); + for (var i = 0; i < 5; i ++) { + for (var j = 0; j < 7; j ++) { + dom.Element el = options[i][j].querySelector('button > span'); + if (el.classes.contains('text-muted')) { + expect((i == 0 && j < 2) || (i == 4 && j > 3)).toBeTruthy(); + } + } + } + })); + + it('updates the model when a day is clicked', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickOption(datepicker, 2, 2); + microLeap(); + scope.rootScope.apply(); + + expect(scope.context['date']).toEqual(new DateTime(2010, 9, 15, 15, 30)); + })); + + it('moves to the previous month & renders correctly when \'previous\' button is clicked', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickPreviousButton(datepicker); + microLeap(); + + expect(getTitle(datepicker)).toEqual('August 2010'); + expect(getLabels(datepicker)).toEqual(['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']); + expect(getOptions(datepicker)).toEqual([ + ['26', '27', '28', '29', '30', '31', '01'], + ['02', '03', '04', '05', '06', '07', '08'], + ['09', '10', '11', '12', '13', '14', '15'], + ['16', '17', '18', '19', '20', '21', '22'], + ['23', '24', '25', '26', '27', '28', '29'], + ['30', '31', '01', '02', '03', '04', '05'] + ]); + + expectSelectedElement(datepicker, null, null ); + })); + + it('updates the model only when when a day is clicked in the \'previous\' month', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickPreviousButton(datepicker); + microLeap(); + expect(scope.context['date']).toEqual(new DateTime(2010, 9, 30, 15, 30)); + + clickOption(datepicker, 2, 3); + microLeap(); + expect(scope.context['date']).toEqual(new DateTime(2010, 8, 12, 15, 30)); + })); + + it('moves to the next month & renders correctly when \'next\' button is clicked', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickNextButton(datepicker); + microLeap(); + + expect(getTitle(datepicker)).toEqual('October 2010'); + expect(getLabels(datepicker)).toEqual(['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']); + expect(getOptions(datepicker)).toEqual([ + ['27', '28', '29', '30', '01', '02', '03'], + ['04', '05', '06', '07', '08', '09', '10'], + ['11', '12', '13', '14', '15', '16', '17'], + ['18', '19', '20', '21', '22', '23', '24'], + ['25', '26', '27', '28', '29', '30', '31'], + ['01', '02', '03', '04', '05', '06', '07'] + ]); + + expectSelectedElement(datepicker, 0, 3); + })); + + it('updates the model only when when a day is clicked in the \'next\' month', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickNextButton(datepicker); + microLeap(); + expect(scope.context['date']).toEqual(new DateTime(2010, 9, 30, 15, 30)); + + clickOption(datepicker, 2, 2); + microLeap(); + expect(scope.context['date']).toEqual(new DateTime(2010, 10, 13, 15, 30)); + })); + + it('updates the calendar when a day of another month is selected', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickOption(datepicker, 4, 4); + microLeap(); + expect(scope.context['date']).toEqual(new DateTime(2010, 10, 1, 15, 30)); + expect(getTitle(datepicker)).toEqual('October 2010'); + expect(getLabels(datepicker)).toEqual(['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']); + expect(getOptions(datepicker)).toEqual([ + ['27', '28', '29', '30', '01', '02', '03'], + ['04', '05', '06', '07', '08', '09', '10'], + ['11', '12', '13', '14', '15', '16', '17'], + ['18', '19', '20', '21', '22', '23', '24'], + ['25', '26', '27', '28', '29', '30', '31'], + ['01', '02', '03', '04', '05', '06', '07'] + ]); + + expectSelectedElement(datepicker, 0, 4); + })); + + describe('when \'model\' changes', () { + void testCalendar(dom.Element element) { + expect(getTitle(element)).toEqual('November 2005'); + expect(getOptions(element)).toEqual([ + ['31', '01', '02', '03', '04', '05', '06'], + ['07', '08', '09', '10', '11', '12', '13'], + ['14', '15', '16', '17', '18', '19', '20'], + ['21', '22', '23', '24', '25', '26', '27'], + ['28', '29', '30', '01', '02', '03', '04'] + ]); + + expectSelectedElement(element, 1, 0); + } + + describe('to a Date object', () { + it('updates', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + scope.context['date'] = new DateTime(2005, 11, 7, 23, 30); + scope.rootScope.apply(); + + testCalendar(datepicker); + expect(scope.context['date'] is DateTime).toBe(true); + })); + }); + + describe('not to a Date object', () { + + it('to a Number, it updates calendar', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + scope.context['date'] = new DateTime(2005, 11, 7, 23, 30).millisecondsSinceEpoch; + scope.rootScope.apply(); + testCalendar(datepicker); + expect(scope.context['date'] is num).toBe(true); + })); + + it('to a string that can be parsed by Date, it updates calendar', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + /* The function parses a subset of ISO 8601. Examples of accepted strings: + * + * * `"2012-02-27 13:27:00"` + * * `"2012-02-27 13:27:00.123456z"` + * * `"20120227 13:27:00"` + * * `"20120227T132700"` + * * `"20120227"` + * * `"+20120227"` + * * `"2012-02-27T14Z"` + * * `"2012-02-27T14+00:00"` + * * `"-123450101 00:00:00 Z"`: in the year -12345. + */ + scope.context['date'] = '2005-11-07 23:30:00'; + scope.rootScope.apply(); + testCalendar(datepicker); + expect(scope.context['date'] is String).toBe(true); + })); + +// it('to a string that cannot be parsed by Date, it gets invalid', async(inject(() { +// dom.Element element = createDatapicker(); +// +// scope.context['date'] = 'pizza'; +// scope.rootScope.apply(); +// expect(element.classes.contains('ng-invalid')).toBeTruthy(); +// expect(element.classes.contains('ng-invalid-date')).toBeTruthy(); +// expect(scope.context['date']).toBe('pizza'); +// }))); + }); + }); + + it('loops between different modes', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + expect(getTitle(datepicker)).toEqual('September 2010'); + + clickTitleButton(datepicker); + microLeap(); + expect(getTitle(datepicker)).toEqual('2010'); + + clickTitleButton(datepicker); + microLeap(); + expect(getTitle(datepicker)).toEqual('2001 - 2020'); + + clickTitleButton(datepicker); + microLeap(); + expect(getTitle(datepicker)).toEqual('September 2010'); + })); + + describe('month selection mode', () { + it('shows the year as title', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickTitleButton(datepicker); + microLeap(); + + expect(getTitle(datepicker)).toEqual('2010'); + })); + + it('shows months as options', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickTitleButton(datepicker); + microLeap(); + + expect(getLabels(datepicker)).toEqual([]); + expect(getOptions(datepicker)).toEqual([ + ['January', 'February', 'March'], + ['April', 'May', 'June'], + ['July', 'August', 'September'], + ['October', 'November', 'December'] + ]); + })); + + it('does not change the model', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickTitleButton(datepicker); + microLeap(); + + expect(scope.context['date']).toEqual(new DateTime(2010, 9, 30, 15, 30)); + })); + + it('has \'selected\' only the correct month', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickTitleButton(datepicker); + microLeap(); + + expectSelectedElement(datepicker, 2, 2); + })); + + it('moves to the previous year when \'previous\' button is clicked', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickTitleButton(datepicker); + microLeap(); + + clickPreviousButton(datepicker); + microLeap(); + + expect(getTitle(datepicker)).toEqual('2009'); + expect(getLabels(datepicker)).toEqual([]); + expect(getOptions(datepicker)).toEqual([ + ['January', 'February', 'March'], + ['April', 'May', 'June'], + ['July', 'August', 'September'], + ['October', 'November', 'December'] + ]); + + expectSelectedElement(datepicker, null, null); + })); + + it('moves to the next year when \'next\' button is clicked', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickTitleButton(datepicker); + microLeap(); + + clickNextButton(datepicker); + microLeap(); + + expect(getTitle(datepicker)).toEqual('2011'); + expect(getLabels(datepicker)).toEqual([]); + expect(getOptions(datepicker)).toEqual([ + ['January', 'February', 'March'], + ['April', 'May', 'June'], + ['July', 'August', 'September'], + ['October', 'November', 'December'] + ]); + + expectSelectedElement(datepicker, null, null); + })); + + it('renders correctly when a month is clicked', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickTitleButton(datepicker); + microLeap(); + + clickPreviousButton(datepicker, 5); + microLeap(); + expect(getTitle(datepicker)).toEqual('2005'); + + clickOption(datepicker, 3, 1); + microLeap(); + expect(scope.context['date']).toEqual(new DateTime(2010, 9, 30, 15, 30)); + expect(getTitle(datepicker)).toEqual('November 2005'); + expect(getOptions(datepicker)).toEqual([ + ['31', '01', '02', '03', '04', '05', '06'], + ['07', '08', '09', '10', '11', '12', '13'], + ['14', '15', '16', '17', '18', '19', '20'], + ['21', '22', '23', '24', '25', '26', '27'], + ['28', '29', '30', '01', '02', '03', '04'] + ]); + + clickOption(datepicker, 2, 2); + microLeap(); + expect(scope.context['date']).toEqual(new DateTime(2005, 11, 16, 15, 30)); + })); + }); + + describe('year selection mode', () { + + it('shows the year range as title', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickTitleButton(datepicker, 2); + expect(getTitle(datepicker)).toEqual('2001 - 2020'); + })); + + it('shows years as options', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickTitleButton(datepicker, 2); + microLeap(); + + expect(getLabels(datepicker)).toEqual([]); + expect(getOptions(datepicker)).toEqual([ + ['2001', '2002', '2003', '2004', '2005'], + ['2006', '2007', '2008', '2009', '2010'], + ['2011', '2012', '2013', '2014', '2015'], + ['2016', '2017', '2018', '2019', '2020'] + ]); + })); + + it('does not change the model', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickTitleButton(datepicker, 2); + microLeap(); + + expect(scope.context['date']).toEqual(new DateTime(2010, 9, 30, 15, 30)); + })); + + it('has \'selected\' only the selected year', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickTitleButton(datepicker, 2); + microLeap(); + + expectSelectedElement(datepicker, 1, 4); + })); + + it('moves to the previous year set when \'previous\' button is clicked', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickTitleButton(datepicker, 2); + microLeap(); + + clickPreviousButton(datepicker); + microLeap(); + + expect(getTitle(datepicker)).toEqual('1981 - 2000'); + expect(getLabels(datepicker)).toEqual([]); + expect(getOptions(datepicker)).toEqual([ + ['1981', '1982', '1983', '1984', '1985'], + ['1986', '1987', '1988', '1989', '1990'], + ['1991', '1992', '1993', '1994', '1995'], + ['1996', '1997', '1998', '1999', '2000'] + ]); + expectSelectedElement(datepicker, null, null); + })); + + it('moves to the next year set when \'next\' button is clicked', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickTitleButton(datepicker, 2); + microLeap(); + + clickNextButton(datepicker); + microLeap(); + + expect(getTitle(datepicker)).toEqual('2021 - 2040'); + expect(getLabels(datepicker)).toEqual([]); + expect(getOptions(datepicker)).toEqual([ + ['2021', '2022', '2023', '2024', '2025'], + ['2026', '2027', '2028', '2029', '2030'], + ['2031', '2032', '2033', '2034', '2035'], + ['2036', '2037', '2038', '2039', '2040'] + ]); + + expectSelectedElement(datepicker, null, null); + })); + }); + + describe('attribute \'starting-day\'', () { + + String getHtml({String extra:''}) { + return r''; + }; + + Map getScopeContent() { + return {'date': new DateTime(2010, 9, 30, 15, 30)}; + }; + + it('shows the day labels rotated', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + microLeap(); + digest(); + final datepicker = shadowRoot.querySelector('datepicker'); + + expect(getLabels(datepicker)).toEqual(['Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun', 'Mon']); + })); + + it('renders the calendar days correctly', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + microLeap(); + digest(); + final datepicker = shadowRoot.querySelector('datepicker'); + + expect(getOptions(datepicker)).toEqual([ + ['31', '01', '02', '03', '04', '05', '06'], + ['07', '08', '09', '10', '11', '12', '13'], + ['14', '15', '16', '17', '18', '19', '20'], + ['21', '22', '23', '24', '25', '26', '27'], + ['28', '29', '30', '01', '02', '03', '04'] + ]); + })); + + it('renders the week numbers correctly', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + microLeap(); + digest(); + final datepicker = shadowRoot.querySelector('datepicker'); + + expect(getWeeks(datepicker)).toEqual(['35', '36', '37', '38', '39']); + })); + }); + + describe('attribute \'show-weeks\'', () { + dom.Element weekHeader, weekElement; + + String getHtml({String extra:''}) { + return r''; + }; + + Map getScopeContent() { + return { + 'date': new DateTime(2010, 9, 30, 15, 30), + 'showWeeks': false + }; + }; + + it('hides week numbers based on variable', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + microLeap(); + digest(); + final datepicker = shadowRoot.querySelector('datepicker'); + dom.Element weekHeader = getLabelsRow(datepicker).querySelectorAll('td').first; // TODO: TH + dom.Element weekElement = datepicker.querySelectorAll('tbody > tr')[1].querySelectorAll('td').first; + + expect(weekHeader.text).toEqual('#'); + expect(weekHeader.classes).toContain('ng-hide'); + expect(weekElement.classes).toContain('ng-hide'); + })); + + it('toggles week numbers', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + microLeap(); + digest(); + final datepicker = shadowRoot.querySelector('datepicker'); + dom.Element weekHeader = getLabelsRow(datepicker).querySelectorAll('td').first; // TODO: TH + dom.Element weekElement = datepicker.querySelectorAll('tbody > tr')[1].querySelectorAll('td').first; + + scope.context['showWeeks'] = true; + scope.rootScope.apply(); + expect(weekHeader.text).toEqual('#'); + expect(weekHeader.classes).not.toContain('ng-hide'); + expect(weekElement.classes).not.toContain('ng-hide'); + + scope.context['showWeeks'] = false; + scope.rootScope.apply(); + expect(weekHeader.text).toEqual('#'); + expect(weekHeader.classes).toContain('ng-hide'); + expect(weekElement.classes).toContain('ng-hide'); + })); + }); + + describe('min attribute', () { + String getHtml() { + return r''; + }; + + Map getScopeContent() { + return { + 'date': new DateTime(2010, 9, 30, 15, 30), + 'mindate': new DateTime(2010, 9, 13) + }; + }; + + it('disables appropriate days in current month', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + for (var i = 0; i < 5; i ++) { + for (var j = 0; j < 7; j ++) { + expect(isDisabledOption(datepicker, i, j)).toBe( (i < 2) ); + } + } + })); + + it('disables appropriate days when min date changes', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + scope.context['mindate'] = new DateTime(2010, 9, 6); + scope.rootScope.apply(); + for (var i = 0; i < 5; i ++) { + for (var j = 0; j < 7; j ++) { + expect(isDisabledOption(datepicker, i, j)).toBe( (i < 1) ); + } + } + })); + + it('invalidates when model is a disabled date', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + scope.context['mindate'] = new DateTime(2010, 9, 6); + scope.context['date'] = new DateTime(2010, 9, 2); + scope.rootScope.apply(); +// expect(datepicker.classes.contains('ng-invalid')).toBeTruthy(); +// expect(datepicker.classes.contains('ng-invalid-date-disabled')).toBeTruthy(); + })); + + it('disables all days in previous month', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickPreviousButton(datepicker); + microLeap(); + for (var i = 0; i < 5; i ++) { + for (var j = 0; j < 7; j ++) { + expect(isDisabledOption(datepicker, i, j)).toBe(true); + } + } + })); + + it('disables no days in next month', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickNextButton(datepicker); + microLeap(); + for (var i = 0; i < 5; i ++) { + for (var j = 0; j < 7; j ++) { + expect(isDisabledOption(datepicker, i, j)).toBe(false); + } + } + })); + + it('disables appropriate months in current year', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickTitleButton(datepicker); + microLeap(); + for (var i = 0; i < 4; i ++) { + for (var j = 0; j < 3; j ++) { + expect(isDisabledOption(datepicker, i, j)).toBe( (i < 2 || (i == 2 && j < 2)) ); + } + } + })); + + it('disables all months in previous year', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickTitleButton(datepicker); + microLeap(); + clickPreviousButton(datepicker); + microLeap(); + for (var i = 0; i < 4; i ++) { + for (var j = 0; j < 3; j ++) { + expect(isDisabledOption(datepicker, i, j)).toBe(true); + } + } + })); + + it('disables no months in next year', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickTitleButton(datepicker); + microLeap(); + clickNextButton(datepicker); + microLeap(); + for (var i = 0; i < 4; i ++) { + for (var j = 0; j < 3; j ++) { + expect(isDisabledOption(datepicker, i, j)).toBe(false); + } + } + })); + + it('enables everything before if it is cleared', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + scope.context['mindate'] = null; + scope.context['date'] = new DateTime(1949, 12, 20); + scope.rootScope.apply(); + + clickTitleButton(datepicker); + microLeap(); + for (var i = 0; i < 4; i ++) { + for (var j = 0; j < 3; j ++) { + expect(isDisabledOption(datepicker, i, j)).toBe(false); + } + } + })); + }); + + describe('max attribute', () { + String getHtml() { + return r''; + }; + + Map getScopeContent() { + return { + 'date': new DateTime(2010, 9, 30, 15, 30), + 'maxdate': new DateTime(2010, 9, 26) + }; + }; + + it('disables appropriate days in current month', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + for (var i = 0; i < 5; i ++) { + for (var j = 0; j < 7; j ++) { + expect(isDisabledOption(datepicker, i, j)).toBe( (i == 4) ); + } + } + })); + + it('disables appropriate days when max date changes', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + scope.context['maxdate'] = new DateTime(2010, 9, 19); + scope.rootScope.apply(); + for (var i = 0; i < 5; i ++) { + for (var j = 0; j < 7; j ++) { + expect(isDisabledOption(datepicker, i, j)).toBe( (i > 2) ); + } + } + })); + + it('invalidates when model is a disabled date', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + scope.context['maxdate'] = new DateTime(2010, 9, 19); + scope.rootScope.apply(); +// expect(datepicker.classes.contains('ng-invalid')).toBeTruthy(); +// expect(datepicker.classes.contains('ng-invalid-date-disabled')).toBeTruthy(); + })); + + it('disables no days in previous month', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickPreviousButton(datepicker); + microLeap(); + for (var i = 0; i < 5; i ++) { + for (var j = 0; j < 7; j ++) { + expect(isDisabledOption(datepicker, i, j)).toBe( false ); + } + } + })); + + it('disables all days in next month', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickNextButton(datepicker); + microLeap(); + for (var i = 0; i < 5; i ++) { + for (var j = 0; j < 7; j ++) { + expect(isDisabledOption(datepicker, i, j)).toBe( true ); + } + } + })); + + it('disables appropriate months in current year', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickTitleButton(datepicker); + microLeap(); + for (var i = 0; i < 4; i ++) { + for (var j = 0; j < 3; j ++) { + expect(isDisabledOption(datepicker, i, j)).toBe( (i > 2 || (i == 2 && j > 2)) ); + } + } + })); + + it('disables no months in previous year', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickTitleButton(datepicker); + microLeap(); + clickPreviousButton(datepicker); + microLeap(); + for (var i = 0; i < 4; i ++) { + for (var j = 0; j < 3; j ++) { + expect(isDisabledOption(datepicker, i, j)).toBe( false ); + } + } + })); + + it('disables all months in next year', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickTitleButton(datepicker); + microLeap(); + clickNextButton(datepicker); + microLeap(); + for (var i = 0; i < 4; i ++) { + for (var j = 0; j < 3; j ++) { + expect(isDisabledOption(datepicker, i, j)).toBe( true ); + } + } + })); + + it('enables everything after if it is cleared', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + scope.context['maxdate'] = null; + scope.rootScope.apply(); + for (var i = 0; i < 5; i ++) { + for (var j = 0; j < 7; j ++) { + expect(isDisabledOption(datepicker, i, j)).toBe( false ); + } + } + })); + }); + +// describe('date-disabled expression', () { +// dom.Element createDatapicker() { +// scope.context['date'] = new DateTime(2010, 9, 30, 15, 30); +// scope.context['dateDisabledHandler'] = jasmine.createSpy('dateDisabledHandler'); +// dom.Element element = _.compile('', scope:scope); +// +// microLeap(); +// scope.rootScope.apply(); +// +// return element; +// } +// +// it('executes the dateDisabled expression for each visible day plus one for validation', async(inject(() { +// dom.Element element = createDatapicker(); +// +// expect(scope.context['dateDisabledHandler'].count).toEqual(35 + 1); +// }))); +// +// it('executes the dateDisabled expression for each visible month plus one for validation', async(inject(() { +// dom.Element element = createDatapicker(); +// +// scope.context['disabledHandler'].reset(); +// clickTitleButton(element); +// microLeap(); +// expect(scope.context['disabledHandler'].calls.length).toEqual(12 + 1); +// }))); +// +// it('executes the dateDisabled expression for each visible year plus one for validation', async(inject(() { +// dom.Element element = createDatapicker(); +// +// clickTitleButton(element); +// microLeap(); +// scope.context['disabledHandler'].reset(); +// clickTitleButton(element); +// microLeap(); +// expect(scope.context['disabledHandler'].calls.length).toEqual(20 + 1); +// }))); + }); + + describe('formatting attributes', () { + + String getHtml() { + return ''; + }; + + Map getScopeContent() { + return { + 'date': new DateTime(2010, 9, 30, 15, 30), + 'maxdate': new DateTime(2010, 9, 26) + }; + }; + + it('changes the title format in \'day\' mode', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + expect(getTitle(datepicker)).toEqual('September, 10'); + })); + + it('changes the title & months format in \'month\' mode', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickTitleButton(datepicker); + microLeap(); + + expect(getTitle(datepicker)).toEqual('10'); + expect(getOptions(datepicker)).toEqual([ + ['Jan', 'Feb', 'Mar'], + ['Apr', 'May', 'Jun'], + ['Jul', 'Aug', 'Sep'], + ['Oct', 'Nov', 'Dec'] + ]); + })); + + it('changes the title, year format & range in \'year\' mode', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickTitleButton(datepicker, 2); + microLeap(); + + expect(getTitle(datepicker)).toEqual('01 - 10'); + expect(getOptions(datepicker)).toEqual([ + ['01', '02', '03', '04', '05'], + ['06', '07', '08', '09', '10'] + ]); + })); + + it('shows day labels', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + expect(getLabels(datepicker)).toEqual(['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']); + })); + + it('changes the day format', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + expect(getOptions(datepicker)).toEqual([ + ['30', '31', '1', '2', '3', '4', '5'], + ['6', '7', '8', '9', '10', '11', '12'], + ['13', '14', '15', '16', '17', '18', '19'], + ['20', '21', '22', '23', '24', '25', '26'], + ['27', '28', '29', '30', '1', '2', '3'] + ]); + })); + }); + + describe('setting datepickerConfig', () { + + beforeEach(() { + inject((DatepickerConfig dc) { + dc + ..startingDay = 6 + ..showWeeks = false + ..dayFormat = 'd' + ..monthFormat = 'MMM' + ..yearFormat = 'yy' + ..yearRange = 10 + ..dayHeaderFormat = 'EEEE' + ..dayTitleFormat = 'MMMM, yy' + ..monthTitleFormat = 'yy'; + }); + }); + + String getHtml() { + return ''; + }; + + Map getScopeContent() { + return { + 'date': new DateTime(2010, 9, 30, 15, 30), + 'maxdate': new DateTime(2010, 9, 26) + }; + }; + + it('changes the title format in \'day\' mode', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + expect(getTitle(datepicker)).toEqual('September, 10'); + })); + + it('changes the title & months format in \'month\' mode', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickTitleButton(datepicker); + microLeap(); + + expect(getTitle(datepicker)).toEqual('10'); + expect(getOptions(datepicker)).toEqual([ + ['Jan', 'Feb', 'Mar'], + ['Apr', 'May', 'Jun'], + ['Jul', 'Aug', 'Sep'], + ['Oct', 'Nov', 'Dec'] + ]); + })); + + it('changes the title, year format & range in \'year\' mode', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + clickTitleButton(datepicker, 2); + microLeap(); + + expect(getTitle(datepicker)).toEqual('01 - 10'); + expect(getOptions(datepicker)).toEqual([ + ['01', '02', '03', '04', '05'], + ['06', '07', '08', '09', '10'] + ]); + })); + + it('changes the \'starting-day\' & day headers & format', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + expect(getLabels(datepicker)).toEqual(['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']); + expect(getOptions(datepicker)).toEqual([ + ['29', '30', '31', '1', '2', '3', '4'], + ['5', '6', '7', '8', '9', '10', '11'], + ['12', '13', '14', '15', '16', '17', '18'], + ['19', '20', '21', '22', '23', '24', '25'], + ['26', '27', '28', '29', '30', '1', '2'] + ]); + })); + + it('changes initial visibility for weeks', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final datepicker = shadowRoot.querySelector('datepicker'); + + // TODO: TH + expect(getLabelsRow(datepicker).querySelectorAll('td').first.classes).toContain('ng-hide'); + var tr = datepicker.querySelectorAll('tbody > tr'); + for (var i = 0; i < 5; i++) { + expect(tr[i].querySelectorAll('td').first.classes).toContain('ng-hide'); + } + })); + }); + +// describe('controller', () { +// +// Datepicker ctrl; +// +// beforeEach(inject((DatepickerConfig datepickerConfig, Scope scope, Injector injector, Date dateFilter) { +// var element = new dom.DivElement(); +// var attrs = new NodeAttrs(element); +// ctrl = new Datepicker.forTests(element, datepickerConfig, attrs, scope, dateFilter); +// })); +// +// +// describe('modes', () { +// var currentMode; +// +// it('to be an array', async(inject(() { +// expect(ctrl.modes.length).toBe(3); +// }))); +// +// describe('\'day\'', () { +// beforeEach(inject(() { +// currentMode = ctrl.modes[0]; +// })); +// +// it('has the appropriate name', async(inject(() { +// expect(currentMode.name).toEqual('day'); +// }))); +// +// it('returns the correct date objects', async(inject(() { +// var objs = currentMode.getVisibleDates(new DateTime(2010, 9, 1), new DateTime(2010, 9, 30)).objects; +// expect(objs.length).toBe(35); +// expect(objs[1].selected).toBeFalsy(); +// expect(objs[31].selected).toBeTruthy(); +// }))); +// +// it('can compare two dates', async(inject(() { +// expect(currentMode.compare(new DateTime(2010, 9, 30), new DateTime(2010, 9, 1)) > 0).toBeTruthy(); +// expect(currentMode.compare(new DateTime(2010, 9, 1), new DateTime(2010, 9, 30)) < 0).toBeTruthy(); +// expect(currentMode.compare(new DateTime(2010, 9, 30, 15, 30), new DateTime(2010, 9, 30, 20, 30))).toBe(0); +// }))); +// }); +// +// describe('\'month\'', () { +// beforeEach(inject(() { +// currentMode = ctrl.modes[1]; +// })); +// +// it('has the appropriate name', async(inject(() { +// expect(currentMode.name).toBe('month'); +// }))); +// +// it('returns the correct date objects', async(inject(() { +// var objs = currentMode.getVisibleDates(new DateTime(2010, 9, 1), new DateTime(2010, 9, 30)).objects; +// expect(objs.length).toBe(12); +// expect(objs[1].selected).toBeFalsy(); +// expect(objs[8].selected).toBeTruthy(); +// }))); +// +// it('can compare two dates', async(inject(() { +// expect(currentMode.compare(new DateTime(2010, 10, 30), new DateTime(2010, 9, 1)) > 0).toBeTruthy(); +// expect(currentMode.compare(new DateTime(2010, 9, 1), new DateTime(2010, 10, 30)) < 0).toBeTruthy(); +// expect(currentMode.compare(new DateTime(2010, 9, 1), new DateTime(2010, 9, 30))).toBe(0); +// }))); +// }); +// +// describe('\'year\'', () { +// beforeEach(inject(() { +// currentMode = ctrl.modes[2]; +// })); +// +// it('has the appropriate name', async(inject(() { +// expect(currentMode.name).toBe('year'); +// }))); +// +// it('returns the correct date objects', async(inject(() { +// var objs = currentMode.getVisibleDates(new DateTime(2010, 9, 1), new DateTime(2010, 9, 1)).objects; +// expect(objs.length).toBe(20); +// expect(objs[1].selected).toBeFalsy(); +// expect(objs[9].selected).toBeTruthy(); +// }))); +// +// it('can compare two dates', async(inject(() { +// expect(currentMode.compare(new DateTime(2011, 9, 1), new DateTime(2010, 10, 30)) > 0).toBeTruthy(); +// expect(currentMode.compare(new DateTime(2010, 10, 30), new DateTime(2011, 9, 1)) < 0).toBeTruthy(); +// expect(currentMode.compare(new DateTime(2010, 11, 9), new DateTime(2010, 9, 30))).toBe(0); +// }))); +// }); +// }); +// +// +// describe('\'isDisabled\' function', () { +// var date = new DateTime(2010, 9, 30, 15, 30); +// +// it('to return false if no limit is set', async(inject(() { +// expect(ctrl.isDisabled(date, 0)).toBeFalsy(); +// }))); +// +// it('to handle correctly the \'min\' date', async(inject(() { +// ctrl.minDate = new DateTime(2010, 10, 1); +// expect(ctrl.isDisabled(date, 0)).toBeTruthy(); +// expect(ctrl.isDisabled(date)).toBeTruthy(); +// +// ctrl.minDate = new DateTime(2010, 9, 1); +// expect(ctrl.isDisabled(date, 0)).toBeFalsy(); +// }))); +// +// it('to handle correctly the \'max\' date', async(inject(() { +// ctrl.maxDate = new DateTime(2010, 10, 1); +// expect(ctrl.isDisabled(date, 0)).toBeFalsy(); +// +// ctrl.maxDate = new DateTime(2010, 9, 1); +// expect(ctrl.isDisabled(date, 0)).toBeTruthy(); +// expect(ctrl.isDisabled(date)).toBeTruthy(); +// }))); +// +//// it('to handle correctly the scope \'dateDisabled\' expression', async(inject(() { +//// ctrl.setDateDisabled((attribs) { +//// return false; +//// }); +//// +//// expect(ctrl.isDisabled(date, 0)).toBeFalsy(); +// // +//// ctrl.setDateDisabled((attribs) { +//// return true; +//// }); +//// expect(ctrl.isDisabled(date, 0)).toBeTruthy(); +//// }))); +//// }); +// }); + +// describe('as popup', () { +// dom.Element divElement, dropdownEl, element; +// InputElement inputEl; +// var changeInputValueTo; +// +// void assignElements(dom.Element wrapElement) { +// inputEl = ngQuery(wrapElement, 'input').first; +// dropdownEl = ngQuery(wrapElement, 'ul').first; +// element = ngQuery(wrapElement, 'table').first; +// } +// +// void createPopup() { +// scope.context['date'] = new DateTime(2010, 9, 30, 15, 30); +// dom.Element wrapElement = _.compile('
', scope:scope); +// +// microLeap(); +// scope.apply(); + +// assignElements(wrapElement); +// +// changeInputValueTo = (InputElement el, value) { +// el.value = value; +//// el.trigger($sniffer.hasEvent('input') ? 'input' : 'change'); +// scope.rootScope.apply(); +// }; +// }; + +// describe('', () { +// +// it('to display the correct value in input', async(inject(() { +// createPopup(); +// +// expect(inputEl.value).toEqual('2010-09-30'); +// }))); +// +// it('does not to display datepicker initially', async(inject(() { +// createPopup(); +// +// expect(dropdownEl.style.display).toEqual('none'); +// }))); + +// it('displays datepicker on input focus', async(inject(() { +// createPopup(); +// +// inputEl.focus(); +// microLeap(); +// scope.rootScope.apply(); +// +// expect(dropdownEl.style.display).not.toEqual('none'); +// }))); + +// it('renders the calendar correctly', async(inject(() { +// createPopup(); +// +// expect(getLabelsRow(element).style.display).not.toBe('none'); +// expect(getLabels(element)).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']); +// expect(getOptions(element)).toEqual([ +// ['29', '30', '31', '01', '02', '03', '04'], +// ['05', '06', '07', '08', '09', '10', '11'], +// ['12', '13', '14', '15', '16', '17', '18'], +// ['19', '20', '21', '22', '23', '24', '25'], +// ['26', '27', '28', '29', '30', '01', '02'] +// ]); +// }))); + +// it('updates the input when a day is clicked', async(inject(() { +// createPopup(); +// +// clickOption(element, 2, 3); +// expect(inputEl.value).toEqual('2010-09-15'); +// expect(scope.context['date']).toEqual(new DateTime(2010, 9, 15, 15, 30)); +// }))); + +// it('should mark the input field dirty when a day is clicked', async(inject(() { +// createPopup(); +// +// expect(inputEl.classes.contains('ng-pristine')).toBeTruthy(); +// clickOption(element, 2, 3); +// expect(inputEl.classes.contains('ng-dirty')).toBeTruthy(); +// }))); + +// it('updates the input correctly when model changes', async(inject(() { +// createPopup(); +// +// scope.context['date'] = new DateTime(1983, 1, 10, 10, 0); //'January 10, 1983 10:00:00'); +// scope.apply(); +// expect(inputEl.value).toBe('1983-01-10'); +// }))); + +// it('closes the dropdown when a day is clicked', async(inject(() { +// createPopup(); +// +// inputEl.focus(); +// expect(dropdownEl.style.display).not.toEqual('none'); +// +// clickOption(element, 2, 3); +// expect(dropdownEl.style.display).toEqual('none'); +// }))); + +// it('updates the model & calendar when input value changes', async(inject(() { +// createPopup(); +// +// changeInputValueTo(inputEl, 'March 5, 1980'); +// +// expect(scope.context['date'].year).toEqual(1980); +// expect(scope.context['date'].month).toEqual(2); +// expect(scope.context['date'].day).toEqual(5); +// +// expect(getOptions(element)).toEqual([ +// ['24', '25', '26', '27', '28', '29', '01'], +// ['02', '03', '04', '05', '06', '07', '08'], +// ['09', '10', '11', '12', '13', '14', '15'], +// ['16', '17', '18', '19', '20', '21', '22'], +// ['23', '24', '25', '26', '27', '28', '29'], +// ['30', '31', '01', '02', '03', '04', '05'] +// ]); +// expectSelectedElement(element, 1, 3); +// }))); + +// it('closes when click outside of calendar', async(inject(() { +// createPopup(); +// +// document.body.click(); +// expect(dropdownEl.style.display).toEqual('none'); +// }))); + +// it('sets `ng-invalid` for invalid input', async(inject(() { +// createPopup(); +// +// changeInputValueTo(inputEl, 'pizza'); +// +// expect(inputEl.classes.contains('ng-invalid')).toBeTruthy(); +// expect(inputEl.classes.contains('ng-invalid-date')).toBeTruthy(); +// expect(scope.context['date']).toBeNull(); +// expect(inputEl.value).toEqual('pizza'); +// }))); +// }); +// }); + + }); +} \ No newline at end of file diff --git a/test/unit/dragdrop/dragdrop_sortable_test.dart b/test/unit/dragdrop/dragdrop_sortable_test.dart new file mode 100644 index 0000000..625e97d --- /dev/null +++ b/test/unit/dragdrop/dragdrop_sortable_test.dart @@ -0,0 +1,299 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. + +part of angular_ui_test; + +void dragdropSortableTests() { + + describe('Drag&Drop-Sortable -', () { + + TestBed _; + Scope scope; + DragDropSortableDataService ddsDataService; + DragDropConfigService ddConfig; + + beforeEach(() { + setUpInjector(); + module((Module module) { + module.install(new DragDropModule()); + }); + inject((TestBed tb, Scope s, DragDropSortableDataService injDDSDataService, DragDropConfigService injDragDropConfig) { + _ = tb; + scope = s; + ddsDataService = injDDSDataService; + ddConfig = injDragDropConfig; + }); + }); + + afterEach(tearDownInjector); + + void swapMultiple(List nodesOne, int firstNodeId, List nodesTwo, int secondNodeId) { + _.triggerEvent(nodesOne[firstNodeId], 'dragstart', 'MouseEvent'); + _.triggerEvent(nodesTwo[secondNodeId], 'dragenter', 'MouseEvent'); + } + + void swap(List nodes, int firstNodeId, int secondNodeId) { + swapMultiple(nodes, firstNodeId, nodes, secondNodeId); + } + + it('Single List Sortable -', () { + + dom.Element createElement(List sortableList) { + + scope.context['data'] = { + 'sortableList' : [] + }; + String html = + '''
+
    +
  • {{item}}
  • +
+
'''; + dom.Element element = _.compile(html.trim()); + + microLeap(); + scope.rootScope.apply(); + + scope.context['data']['sortableList'] = sortableList; + + microLeap(); + scope.rootScope.apply(); + + return element; + }; + + it('The elements of the list should be draggable', async(inject(() { + List values = ['one','two','three','four','five','six']; + dom.Element ulElem = ngQuery(createElement(values), 'ul')[0]; + expect(ulElem).toBeNotNull(); + expect(ulElem.children.length).toBe(values.length); + + for (dom.Element childElem in ulElem.children) { + expect(childElem.attributes['draggable']).toBeTruthy(); + } + }))); + + it('It should sort in the same list', async(inject(() { + List values = ['one','two','three','four']; + dom.Element ulElem = ngQuery(createElement(values), 'ul')[0]; + expect(ulElem).toBeNotNull(); + expect(ulElem.children.length).toBe(4); + + expect(ddsDataService.sortableData).toBeNull(); + expect(ddsDataService.index).toBeNull(); + _.triggerEvent(ulElem.children[0], 'dragstart', 'MouseEvent'); + expect(ddsDataService.sortableData).toBe(values); + expect(ddsDataService.index).toBe(0); + + swap(ulElem.children, 0, 1); + expect(values[0]).toBe('two'); + expect(ulElem.children[0].text).toEqual('two'); + expect(values[1]).toBe('one'); + expect(ulElem.children[1].text).toEqual('one'); + + }))); + + it('It should add the expected classes on drag events', async(inject(() { + List values = ['one','two','three','four']; + dom.Element ulElem = ngQuery(createElement(values), 'ul')[0]; + expect(ulElem).toBeNotNull(); + + //TODO to be checked. The classes are not added because the nodes are swapped + /* + _.triggerEvent(ulElem.children[0], 'dragstart', 'MouseEvent'); + expect(ulElem.children[0]).toHaveClass(ddConfig.sortableConfig.onDragStartClass); + + _.triggerEvent(ulElem.children[1], 'dragenter', 'MouseEvent'); + expect(ulElem.children[1]).toHaveClass(ddConfig.sortableConfig.onDragEnterClass); + + _.triggerEvent(ulElem.children[1], 'dragover', 'MouseEvent'); + expect(ulElem.children[1]).toHaveClass(ddConfig.sortableConfig.onDragOverClass); + */ + }))); + + it('It should work with arbitrary objects', async(inject(() { + var elemOne = new dom.DivElement(); + var elemTwo = 'elemTwo'; + var elemThree = {'key':'value'}; + List values = [elemOne, elemTwo, elemThree]; + dom.Element ulElem = ngQuery(createElement(values), 'ul')[0]; + expect(ulElem).toBeNotNull(); + expect(ulElem.children.length).toBe(3); + + swap(ulElem.children, 0, 1); + expect(values[0]).toBe(elemTwo); + expect(values[1]).toBe(elemOne); + + swap(ulElem.children, 1, 2); + expect(values[1]).toBe(elemThree); + expect(values[2]).toBe(elemOne); + + swap(ulElem.children, 0, 1); + expect(values[0]).toBe(elemThree); + expect(values[1]).toBe(elemTwo); + + //ulElem.remove(); + }))); + + }); + + it('Multi List Sortable -', () { + + dom.Element createElement(List singleList, List multiOneList, List multiTwoList) { + + scope.context['data'] = { + 'singleList' : [], + 'multiOneList' : [], + 'multiTwoList' : [], + }; + String html = + '''
+
+
    +
  • {{item}}
  • +
+
+
+
    +
  • {{item}}
  • +
+
+
+
    +
  • {{item}}
  • +
+
+
'''; + dom.Element element = _.compile(html.trim()); + + microLeap(); + scope.rootScope.apply(); + + scope.context['data']['singleList'] = singleList; + scope.context['data']['multiOneList'] = multiOneList; + scope.context['data']['multiTwoList'] = multiTwoList; + + microLeap(); + scope.rootScope.apply(); + + return element; + }; + + it('It should sort in the same list', async(inject(() { + List singleList = ['sOne', 'sTwo', 'sThree']; + List multiOneList = ['mOne', 'mTwo', 'mThree']; + List multiTwoList = ['mFour', 'mFive', 'mSix']; + dom.Element ulElem = createElement(singleList, multiOneList, multiTwoList); + expect(ulElem).toBeNotNull(); + expect(ulElem.children.length).toBe(3); + + dom.Element singleElem = ulElem.querySelector('#single ul'); + swap(singleElem.children, 0, 1); + expect(singleList[0]).toBe('sTwo'); + expect(singleElem.children[0].text).toEqual('sTwo'); + expect(singleList[1]).toBe('sOne'); + expect(singleElem.children[1].text).toEqual('sOne'); + + dom.Element multiOneElem = ulElem.querySelector('#multiOne ul'); + swap(multiOneElem.children, 1, 2); + expect(multiOneList[1]).toBe('mThree'); + expect(multiOneElem.children[1].text).toEqual('mThree'); + expect(multiOneList[2]).toBe('mTwo'); + expect(multiOneElem.children[2].text).toEqual('mTwo'); + + dom.Element multiTwoElem = ulElem.querySelector('#multiTwo ul'); + swap(multiTwoElem.children, 1, 2); + expect(multiTwoList[1]).toBe('mSix'); + expect(multiTwoElem.children[1].text).toEqual('mSix'); + expect(multiTwoList[2]).toBe('mFive'); + expect(multiTwoElem.children[2].text).toEqual('mFive'); + }))); + + it('It should be possible to move items from list one to list two', async(inject(() { + List singleList = ['sOne', 'sTwo', 'sThree']; + List multiOneList = ['mOne', 'mTwo', 'mThree']; + List multiTwoList = ['mFour', 'mFive', 'mSix']; + dom.Element ulElem = createElement(singleList, multiOneList, multiTwoList); + expect(ulElem).toBeNotNull(); + expect(ulElem.children.length).toBe(3); + + dom.Element multiOneElem = ulElem.querySelector('#multiOne ul'); + dom.Element multiTwoElem = ulElem.querySelector('#multiTwo ul'); + swapMultiple(multiOneElem.children, 0, multiTwoElem.children, 0); + + expect(multiOneList.length).toBe(2); + expect(multiTwoList.length).toBe(4); + + expect(multiOneList[0]).toBe('mTwo'); + expect(multiTwoList[0]).toBe('mOne'); + expect(multiTwoList[1]).toBe('mFour'); + + }))); + + it('It should not be possible to move items between lists not in the same sortable-zone', async(inject(() { + List singleList = ['sOne', 'sTwo', 'sThree']; + List multiOneList = ['mOne', 'mTwo', 'mThree']; + List multiTwoList = ['mFour', 'mFive', 'mSix']; + dom.Element ulElem = createElement(singleList, multiOneList, multiTwoList); + expect(ulElem).toBeNotNull(); + expect(ulElem.children.length).toBe(3); + + dom.Element singleElem = ulElem.querySelector('#single ul'); + dom.Element multiOneElem = ulElem.querySelector('#multiOne ul'); + swapMultiple(singleElem.children, 0, multiOneElem.children, 0); + + expect(singleList.length).toBe(3); + expect(multiOneList.length).toBe(3); + + expect(singleList[0]).toBe('sOne'); + expect(multiOneList[0]).toBe('mOne'); + }))); + + it('When the list is empty the parent must handle dragenter events', async(inject(() { + List singleList = ['sOne', 'sTwo', 'sThree']; + List multiOneList = []; + List multiTwoList = ['mOne', 'mTwo', 'mThree', 'mFour', 'mFive', 'mSix']; + dom.Element ulElem = createElement(singleList, multiOneList, multiTwoList); + expect(ulElem).toBeNotNull(); + expect(ulElem.children.length).toBe(3); + + dom.Element multiOneElem = ulElem.querySelector('#multiOne'); + dom.Element multiTwoUlElem = ulElem.querySelector('#multiTwo ul'); + + _.triggerEvent(multiTwoUlElem.children[3], 'dragstart', 'MouseEvent'); + _.triggerEvent(multiOneElem, 'dragenter', 'MouseEvent'); + + expect(multiOneList.length).toBe(1); + expect(multiTwoList.length).toBe(5); + + expect(multiTwoList[3]).toBe('mFive'); + expect(multiOneList[0]).toBe('mFour'); + }))); + + it('When the list is NOT empty the parent must NOT handle dragenter events', async(inject(() { + List singleList = ['sOne', 'sTwo', 'sThree']; + List multiOneList = ['mOne']; + List multiTwoList = ['mTwo', 'mThree', 'mFour', 'mFive', 'mSix']; + dom.Element ulElem = createElement(singleList, multiOneList, multiTwoList); + expect(ulElem).toBeNotNull(); + expect(ulElem.children.length).toBe(3); + + dom.Element multiOneElem = ulElem.querySelector('#multiOne'); + dom.Element multiTwoUlElem = ulElem.querySelector('#multiTwo ul'); + + _.triggerEvent(multiTwoUlElem.children[0], 'dragstart', 'MouseEvent'); + _.triggerEvent(multiOneElem, 'dragenter', 'MouseEvent'); + + expect(multiOneList.length).toBe(1); + expect(multiTwoList.length).toBe(5); + + expect(multiOneList[0]).toBe('mOne'); + expect(multiTwoList[0]).toBe('mTwo'); + + }))); + }); + + }); +} + diff --git a/test/unit/dragdrop/dragdrop_test.dart b/test/unit/dragdrop/dragdrop_test.dart new file mode 100644 index 0000000..51e14ae --- /dev/null +++ b/test/unit/dragdrop/dragdrop_test.dart @@ -0,0 +1,327 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. + +part of angular_ui_test; + +void dragdropTests() { + + describe('Drag&Drop - ', () { + + TestBed _; + Scope scope; + DragDropDataService ddDataService; + DragDropConfigService ddConfig; + + beforeEach(() { + setUpInjector(); + module((Module module) { + module.install(new DragDropModule()); + }); + inject((TestBed tb, Scope s, DragDropDataService injDragDropDataService,DragDropConfigService injDragDropConfig) { + _ = tb; + scope = s; + ddDataService = injDragDropDataService; + ddConfig = injDragDropConfig; + }); + }); + + afterEach(tearDownInjector); + + it('Draggable and Droppable directives -', () { + + var dragData = "Hello World at " + new DateTime.now().toString(); + + dom.Element createElement({bool dragEnabled : true, Function dragSuccessCallback, Function dropSuccessCallback }) { + + scope.context['dragEnabled'] = dragEnabled; + scope.context['dragData'] = dragData; + scope.context['dragSuccessCallback'] = dragSuccessCallback; + scope.context['dropSuccessCallback'] = dropSuccessCallback; + String html = + '''
+
+
+
+
+
'''; + dom.Element element = _.compile(html.trim()); + + microLeap(); + scope.rootScope.apply(); + + return element; + }; + + it('It should add the "draggable" attribute', async(inject(() { + dom.Element dragElem = ngQuery(createElement(), '#dragId')[0]; + expect(dragElem).toBeNotNull(); + expect(dragElem.attributes['draggable']).toBeTruthy(); + }))); + + it('The "draggable" attribute must be false if the drag is not enabled', async(inject(() { + dom.Element dragElem = ngQuery(createElement(dragEnabled: false), '#dragId')[0]; + expect(dragElem).toBeNotNull(); + expect(dragElem.attributes['draggable']).toEqual('false'); + }))); + + it('Drag events should add/remove the draggable data to/from the DragDropDataService', async(inject(() { + dom.Element dragElem = ngQuery(createElement(), '#dragId')[0]; + + expect(ddDataService.draggableData).toBeNull(); + _.triggerEvent(dragElem, 'dragstart', 'MouseEvent'); + expect(ddDataService.draggableData).toBe(dragData); + _.triggerEvent(dragElem, 'dragend', 'MouseEvent'); + expect(ddDataService.draggableData).toBeNull(); + + }))); + + it('Drag events should add/remove the expected classes to the target element', async(inject(() { + dom.Element dragElem = ngQuery(createElement(), '#dragId')[0]; + + expect(dragElem).not.toHaveClass(ddConfig.dragDropConfig.onDragStartClass); + _.triggerEvent(dragElem, 'dragstart', 'MouseEvent'); + expect(dragElem).toHaveClass(ddConfig.dragDropConfig.onDragStartClass); + _.triggerEvent(dragElem, 'dragend', 'MouseEvent'); + expect(dragElem).not.toHaveClass(ddConfig.dragDropConfig.onDragStartClass); + + }))); + + it('Drag start event should not be activated if drag is not enabled', async(inject(() { + dom.Element dragElem = ngQuery(createElement(dragEnabled: false), '#dragId')[0]; + + expect(ddDataService.draggableData).toBeNull(); + expect(dragElem).not.toHaveClass(ddConfig.dragDropConfig.onDragStartClass); + _.triggerEvent(dragElem, 'dragstart', 'MouseEvent'); + expect(ddDataService.draggableData).toBeNull(); + expect(dragElem).not.toHaveClass(ddConfig.dragDropConfig.onDragStartClass); + + }))); + + it('Drop events should add/remove the expected classes to the target element', async(inject(() { + Function dragSuccessCallback = guinness.createSpy('drag callback'); + Function dropSuccessCallback = guinness.createSpy('drop callback'); + + dom.Element elem = createElement(dropSuccessCallback:dropSuccessCallback, dragSuccessCallback:dragSuccessCallback); + dom.Element dropElem = ngQuery(elem, '#dropId')[0]; + dom.Element dragElem = ngQuery(elem, '#dragId')[0]; + + expect(dropElem).not.toHaveClass(ddConfig.dragDropConfig.onDragEnterClass); + expect(dropElem).not.toHaveClass(ddConfig.dragDropConfig.onDragOverClass); + + //The drop events should not work before a drag is started on an element with the correct drop-zone + _.triggerEvent(dropElem, 'dragenter', 'MouseEvent'); + expect(dropElem).not.toHaveClass(ddConfig.dragDropConfig.onDragEnterClass); + + _.triggerEvent(dragElem, 'dragstart', 'MouseEvent'); + + _.triggerEvent(dropElem, 'dragenter', 'MouseEvent'); + expect(dropElem).toHaveClass(ddConfig.dragDropConfig.onDragEnterClass); + expect(dropElem).not.toHaveClass(ddConfig.dragDropConfig.onDragOverClass); + + _.triggerEvent(dropElem, 'dragover', 'MouseEvent'); + expect(dropElem).toHaveClass(ddConfig.dragDropConfig.onDragEnterClass); + expect(dropElem).toHaveClass(ddConfig.dragDropConfig.onDragOverClass); + + _.triggerEvent(dropElem, 'dragleave', 'MouseEvent'); + expect(dropElem).not.toHaveClass(ddConfig.dragDropConfig.onDragEnterClass); + expect(dropElem).not.toHaveClass(ddConfig.dragDropConfig.onDragOverClass); + + _.triggerEvent(dropElem, 'dragover', 'MouseEvent'); + _.triggerEvent(dropElem, 'dragenter', 'MouseEvent'); + _.triggerEvent(dropElem, 'drop', 'MouseEvent'); + expect(dropElem).not.toHaveClass(ddConfig.dragDropConfig.onDragEnterClass); + expect(dropElem).not.toHaveClass(ddConfig.dragDropConfig.onDragOverClass); + }))); + + it('Drop event should activate the onDropSuccess and onDragSuccess callbacks', async(inject(() { + Function dragSuccessCallback = guinness.createSpy('drag callback'); + Function dropSuccessCallback = guinness.createSpy('drop callback'); + + dom.Element mainElement = createElement(dropSuccessCallback:dropSuccessCallback, dragSuccessCallback:dragSuccessCallback); + dom.Element dragElem = ngQuery(mainElement, '#dragId')[0]; + dom.Element dropElem = ngQuery(mainElement, '#dropId')[0]; + + _.triggerEvent(dragElem, 'dragstart', 'MouseEvent'); + _.triggerEvent(dragElem, 'dragend', 'MouseEvent'); + expect(dragSuccessCallback).not.toHaveBeenCalled(); + expect(dropSuccessCallback).not.toHaveBeenCalled(); + + _.triggerEvent(dragElem, 'dragstart', 'MouseEvent'); + _.triggerEvent(dropElem, 'drop', 'MouseEvent'); + expect(dropSuccessCallback).toHaveBeenCalledOnce(); + expect(dragSuccessCallback).toHaveBeenCalledOnce(); + }))); + + it('The onDropSuccess callback should receive the dragged data as paramenter', async(inject(() { + Function dragSuccessCallback = () {}; + + bool dropCallbackCalled = false; + var dropCallbackReceivedData; + Function dropSuccessCallback = (var data) { + dropCallbackCalled = true; + dropCallbackReceivedData = data; + }; + + dom.Element mainElement = createElement(dropSuccessCallback:dropSuccessCallback, dragSuccessCallback:dragSuccessCallback); + dom.Element dragElem = ngQuery(mainElement, '#dragId')[0]; + dom.Element dropElem = ngQuery(mainElement, '#dropId')[0]; + + _.triggerEvent(dragElem, 'dragstart', 'MouseEvent'); + _.triggerEvent(dropElem, 'drop', 'MouseEvent'); + expect(dropCallbackCalled).toBeTruthy(); + expect(dropCallbackReceivedData).toBe(dragData); + }))); + + + it('Drop zones should be correctly evaluated by the DroppableComponent', async(inject(() { + DragDropDataService ddService = new DragDropDataService(); + DragDropZonesService ddZonesService = new DragDropZonesService(); + DroppableComponent droppableComponent = new DroppableComponent(new dom.DivElement(), ddZonesService, ddService, new DragDropConfigService()); + + droppableComponent.dropZones = []; + ddZonesService.allowedDropZones = []; + expect(droppableComponent.isDropAllowed()).toBeTruthy(); + + droppableComponent.dropZones = 'zone1'; + ddZonesService.allowedDropZones = []; + expect(droppableComponent.isDropAllowed()).toBeFalsy(); + + droppableComponent.dropZones = []; + ddZonesService.allowedDropZones = ['zone1']; + expect(droppableComponent.isDropAllowed()).toBeFalsy(); + + droppableComponent.dropZones = 'zone1'; + ddZonesService.allowedDropZones = ['zone1','zone3']; + expect(droppableComponent.isDropAllowed()).toBeTruthy(); + + droppableComponent.dropZones = ['zone1','zone4']; + ddZonesService.allowedDropZones = ['zone1','zone3']; + expect(droppableComponent.isDropAllowed()).toBeTruthy(); + + droppableComponent.dropZones = ['zone1','zone4']; + ddZonesService.allowedDropZones = ['zone2']; + expect(droppableComponent.isDropAllowed()).toBeFalsy(); + + }))); + + }); + + it('Drop Zones -', () { + + dom.Element createElement() { + + scope.context['dragOneSuccessCallback'] = guinness.createSpy('drag one callback'); + scope.context['dragTwoSuccessCallback'] = guinness.createSpy('drag two callback'); + scope.context['dragOneTwoSuccessCallback'] = guinness.createSpy('drag one-two callback'); + scope.context['dropOneSuccessCallback'] = guinness.createSpy('drop one callback'); + scope.context['dropTwoSuccessCallback'] = guinness.createSpy('drop two callback'); + scope.context['dropOneTwoSuccessCallback'] = guinness.createSpy('drop one-two callback'); + String html = + '''
+
+
+
+
+
+
+
+
+
+
+
+
+
'''; + dom.Element element = _.compile(html.trim()); + + microLeap(); + scope.rootScope.apply(); + + return element; + }; + + it('Drop events should not be activated on the wrong drop-zone', async(inject(() { + dom.Element mainElement = createElement(); + dom.Element dragElemOne = ngQuery(mainElement, '#dragIdOne')[0]; + dom.Element dropElemTwo = ngQuery(mainElement, '#dropIdTwo')[0]; + + _.triggerEvent(dragElemOne, 'dragstart', 'MouseEvent'); + + _.triggerEvent(dropElemTwo, 'dragenter', 'MouseEvent'); + expect(dropElemTwo).not.toHaveClass(ddConfig.dragDropConfig.onDragEnterClass); + + _.triggerEvent(dropElemTwo, 'dragover', 'MouseEvent'); + expect(dropElemTwo).not.toHaveClass(ddConfig.dragDropConfig.onDragOverClass); + + _.triggerEvent(dragElemOne, 'drop', 'MouseEvent'); + expect(scope.context['dragOneSuccessCallback']).not.toHaveBeenCalled(); + expect(scope.context['dropTwoSuccessCallback']).not.toHaveBeenCalled(); + }))); + + it('Drop events should be activated on the same drop-zone', async(inject(() { + dom.Element mainElement = createElement(); + dom.Element dragElemOne = ngQuery(mainElement, '#dragIdOne')[0]; + dom.Element dropElemOne = ngQuery(mainElement, '#dropIdOne')[0]; + + _.triggerEvent(dragElemOne, 'dragstart', 'MouseEvent'); + + _.triggerEvent(dropElemOne, 'dragenter', 'MouseEvent'); + expect(dropElemOne).toHaveClass(ddConfig.dragDropConfig.onDragEnterClass); + + _.triggerEvent(dropElemOne, 'dragover', 'MouseEvent'); + expect(dropElemOne).toHaveClass(ddConfig.dragDropConfig.onDragOverClass); + + _.triggerEvent(dropElemOne, 'drop', 'MouseEvent'); + expect(scope.context['dragOneSuccessCallback']).toHaveBeenCalled(); + expect(scope.context['dropOneSuccessCallback']).toHaveBeenCalled(); + }))); + + it('Drop events on multiple drop-zone', async(inject(() { + dom.Element mainElement = createElement(); + dom.Element dragElemOneTwo = ngQuery(mainElement, '#dragIdOneTwo')[0]; + dom.Element dropElemOneTwo = ngQuery(mainElement, '#dropIdOneTwo')[0]; + + _.triggerEvent(dragElemOneTwo, 'dragstart', 'MouseEvent'); + + _.triggerEvent(dropElemOneTwo, 'dragenter', 'MouseEvent'); + expect(dropElemOneTwo).toHaveClass(ddConfig.dragDropConfig.onDragEnterClass); + + _.triggerEvent(dropElemOneTwo, 'dragover', 'MouseEvent'); + expect(dropElemOneTwo).toHaveClass(ddConfig.dragDropConfig.onDragOverClass); + + _.triggerEvent(dropElemOneTwo, 'drop', 'MouseEvent'); + expect(scope.context['dragOneTwoSuccessCallback']).toHaveBeenCalled(); + expect(scope.context['dropOneTwoSuccessCallback']).toHaveBeenCalled(); + }))); + + }); + + }); +} + diff --git a/test/unit/dropdown/dropdown_component_test.dart b/test/unit/dropdown/dropdown_component_test.dart new file mode 100644 index 0000000..360154e --- /dev/null +++ b/test/unit/dropdown/dropdown_component_test.dart @@ -0,0 +1,84 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. + +part of angular_ui_test; + +testDropdownComponent() { + describe("[DropdownComponent]", () { + TestBed _; + Scope scope; + + beforeEach(setUpInjector); + afterEach(tearDownInjector); + + beforeEach(() { + module((Module _) => _ + ..install(new DropdownToggleModule()) + ); + //return loadTemplates(['/alert/alert.html']); + }); + + String getHtml() { + return ''; + }; + + it('should toggle on `a` click', compileComponent( + getHtml(), + {}, + (Scope scope, dom.HtmlElement shadowRoot) { + var elm = shadowRoot.querySelector('.dropdown'); + expect(elm.classes.contains('open')).toBe(false); + elm.querySelector('a').click(); + expect(elm.classes.contains('open')).toBe(true); + elm.querySelector('a').click(); + expect(elm.classes.contains('open')).toBe(false); + })); + + it('should toggle on `ul` click', compileComponent( + getHtml(), + {}, + (Scope scope, dom.HtmlElement shadowRoot) { + var elm = shadowRoot.querySelector('.dropdown'); + expect(elm.classes.contains('open')).toBe(false); + elm.querySelector('ul').click(); + expect(elm.classes.contains('open')).toBe(true); + elm.querySelector('ul').click(); + expect(elm.classes.contains('open')).toBe(false); + })); + + it('should close on elm click', compileComponent( + getHtml(), + {}, + (Scope scope, dom.HtmlElement shadowRoot) { + var elm = shadowRoot.querySelector('.dropdown'); + elm.querySelector('a').click(); + elm.click(); + expect(elm.classes.contains('open')).toBe(false); + })); + + it('should close on document click', compileComponent( + getHtml(), + {}, + (Scope scope, dom.HtmlElement shadowRoot) { + var elm = shadowRoot.querySelector('.dropdown'); + elm.querySelector('a').click(); + dom.document.body.click(); + expect(elm.classes.contains('open')).toBe(false); + })); + + it('should only allow one dropdown to be open at once', compileComponent( + '''''', + {}, + (Scope scope, dom.HtmlElement shadowRoot) { + var elms = shadowRoot.querySelectorAll('.dropdown'); + var elm1 = elms[0]; + var elm2 = elms[1]; + elm1.querySelector('a').click(); + elm2.querySelector('a').click(); + expect(elm1.classes.contains('open')).toBe(false); + expect(elm2.classes.contains('open')).toBe(true); + })); + }); +} diff --git a/test/unit/modal/modal_component_test.dart b/test/unit/modal/modal_component_test.dart new file mode 100644 index 0000000..0eba9b0 --- /dev/null +++ b/test/unit/modal/modal_component_test.dart @@ -0,0 +1,117 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. + +part of angular_ui_test; + +testModalComponent() { + describe("[ModalComponent]", () { + TestBed _; + Scope rootScope; + Timeout timeout; + Modal modal; + + beforeEach(setUpInjector); + + beforeEach(() { + module((Module _) => _ + ..install(new ModalModule()) + ); + inject((Modal m, Timeout t, Scope scope) { + modal = m; + timeout = t; + rootScope = scope; + }); + //return loadTemplates(['/modal/window.html']); + }); + + /** + * Trigger / listen for event on dom.document.body. + * Seems doesn't work at all. Need to find the way how fire keyboard events. + */ + void triggerKeyDown(dom.EventTarget element, int keyCode) { + var streamDown = dom.KeyEvent.keyUpEvent.forTarget(element); + var subscription4 = streamDown.listen( + (e) => print('streamDown listener ${e.keyCode}')); + streamDown.add(new dom.KeyEvent('keydown', keyCode: keyCode)); + }; + + afterEach(() { + dom.document.body.querySelectorAll("modal-window").forEach((el) { + modal.hide(); + el.remove(); + }); + }); + + bool toHaveModalOpenWithContent({String content:'', String selector:null}) { + return dom.document.body.querySelectorAll("modal-window").any((dom.Element el) { + if (el.style.display == 'block') { + if (selector != null) { + el = el.querySelector(selector); + } + if (el.innerHtml.contains(content)) { + return true; + } + } + return false; + }); + } + + bool toHaveBackdrop() => dom.document.body.querySelector('.modal-backdrop') != null; + + int toHaveModalOpen() { + int res = 0; + var modals = dom.document.body.querySelectorAll("modal-window"); + print('!!! modals ${modals.length}'); + modals.forEach((dom.Element el){ + if (el.style.display == 'block') { + res++; + } + }); + return res; + } + + void close(result) { + modal.close(result); + rootScope.apply(); + } + + void dismiss(String reason) { + modal.dismiss(reason); + timeout.flush(); + rootScope.apply(); + } + + void clickOnBackdrop() { + // Find last backdrop + dom.Element el = dom.document.body.querySelectorAll('.modal-backdrop').last; + if (el != null) { + _.triggerEvent(el, 'click'); + timeout.flush(); + rootScope.rootScope.apply(); + } + } + + afterEach(tearDownInjector); + + it('should open and dismiss a modal with a minimal set of options', () { + ModalInstance inst = modal.open(new ModalOptions(template:"
Content
"), rootScope); + + expect(inst).toBeDefined(); +// inst.opened.then((value) { +// print("opened $value"); +// expect(toHaveModalOpen()).toEqual(1); +// expect(toHaveModalOpenWithContent(content:'Content', selector:'div')).toBeTruthy(); +// expect(toHaveBackdrop()).toBeTruthy(); +// // +// dismiss('closing in test'); +// // +// expect(toHaveModalOpen()).toEqual(0); +// expect(toHaveBackdrop()).toBeFalsy(); +// // +//// houskeepking(); +// }); +// expect(inst.opened), completes); + }); + }); +} diff --git a/test/unit/modal/modal_tests.dart b/test/unit/modal/modal_tests.dart new file mode 100644 index 0000000..24eaf4d --- /dev/null +++ b/test/unit/modal/modal_tests.dart @@ -0,0 +1,292 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. + +part of angular.ui.test; + +void modalTests() { + + describe("Testing Modals. Basic scenarios with default options", () { + TestBed _; + Scope scope, rootScope; + dom.Element element; + TemplateCache cache; + Timeout timeout; + Modal modal; + + beforeEach(() { + setUpInjector(); + module((Module module) { + module.install(new ModalModule()); + }); + inject((TestBed tb, Modal m, Timeout t, Scope s, TemplateCache c) { + _ = tb; + modal = m; + timeout = t; + // + scope = s; + rootScope = s.rootScope; + // + cache = c; + addToTemplateCache(cache, 'packages/angular_ui/modal/window.html'); + }); + }); + + afterEach(tearDownInjector); + + /** + * Trigger / listen for event on dom.document.body. + * Seems doesn't work at all. Need to find the way how fire keyboard events. + */ + void triggerKeyDown(dom.EventTarget element, int keyCode) { + var streamDown = dom.KeyEvent.keyUpEvent.forTarget(element); + var subscription4 = streamDown.listen( + (e) => print('streamDown listener ${e.keyCode}')); + streamDown.add(new dom.KeyEvent('keydown', keyCode: keyCode)); + }; + + void houskeepking() { + dom.document.body.querySelectorAll("modal-window").forEach((el) { + modal.hide(); + el.remove(); + }); + } + + bool toHaveModalOpenWithContent({String content:'', String selector:null}) { + return dom.document.body.querySelectorAll("modal-window").any((dom.Element el) { + if (el.style.display == 'block') { + if (selector != null) { + el = el.querySelector(selector); + } + if (el.innerHtml.contains(content)) { + return true; + } + } + return false; + }); + } + + bool toHaveBackdrop() => dom.document.body.querySelector('.modal-backdrop') != null; + + int toHaveModalOpen() { + int res = 0; + dom.document.body.querySelectorAll("modal-window").forEach((dom.Element el){ + if (el.style.display == 'block') { + res++; + } + }); + return res; + } + + void close(result) { + modal.close(result); + rootScope.rootScope.apply(); + } + + void dismiss(String reason) { + modal.dismiss(reason); + timeout.flush(); + rootScope.rootScope.apply(); + } + + void clickOnBackdrop() { + // Find last backdrop + dom.Element el = dom.document.body.querySelectorAll('.modal-backdrop').last; + if (el != null) { + _.triggerEvent(el, 'click'); + timeout.flush(); + rootScope.rootScope.apply(); + } + } + + it('should open and dismiss a modal with a minimal set of options', () { + ModalInstance inst = modal.open(new ModalOptions(template:"
Content
"), scope); + inst.opened.then((value) { + // + expect(toHaveModalOpen()).toEqual(1); + expect(toHaveModalOpenWithContent(content:'Content', selector:'div')).toBeTruthy(); + expect(toHaveBackdrop()).toBeTruthy(); + // + dismiss('closing in test'); + // + expect(toHaveModalOpen()).toEqual(0); + expect(toHaveBackdrop()).toBeFalsy(); + // + houskeepking(); + }); + expect(inst.opened, completes); + }); + + it('should open a modal from templateUrl', () { + cache.put('content.html', new HttpResponse(200, '
URL Content
')); + + ModalInstance inst = modal.open(new ModalOptions(templateUrl: 'content.html'), scope); + inst.opened.then((value) { + // + expect(toHaveModalOpen()).toEqual(1); + expect(toHaveModalOpenWithContent(content:'URL Content', selector:'div')).toBeTruthy(); + expect(toHaveBackdrop()).toBeTruthy(); + // + dismiss('closing in test'); + // + expect(toHaveModalOpen()).toEqual(0); + expect(toHaveBackdrop()).toBeFalsy(); + // + houskeepking(); + }); + expect(inst.opened, completes); + }); + + it('should support closing on backdrop click', () { + ModalInstance inst = modal.open(new ModalOptions(template: '
Content
'), scope); + inst.opened.then((value) { + // + expect(toHaveModalOpen()).toEqual(1); + // Trigger click event on backdrop + _.triggerEvent(dom.document.body.querySelector('.modal-backdrop'), 'click'); + timeout.flush(); + rootScope.rootScope.apply(); + // + expect(toHaveModalOpen()).toEqual(0); + expect(toHaveBackdrop()).toBeFalsy(); + // + houskeepking(); + }); + expect(inst.opened, completes); + }); + + it('should resolve returned promise on close', () { + ModalInstance inst = modal.open(new ModalOptions(template: '
Content
'), scope); + inst.opened.then((value) { + expect(inst.result, completion(equals('closed ok'))); + // + close('closed ok'); + // + houskeepking(); + }); + expect(inst.opened, completes); + }); + + it('should reject returned promise on dismiss', () { + ModalInstance inst = modal.open(new ModalOptions(template: '
Content
'), scope); + inst.opened.then((value) { + expect(inst.result, throwsA(equals('esc'))); + // + dismiss('esc'); + // + houskeepking(); + }); + expect(inst.opened, completes); + }); + + it('should not have any backdrop element if backdrop set to false', () { + ModalInstance inst = modal.open(new ModalOptions(template: '
No backdrop
', backdrop: 'false'), scope); + inst.opened.then((value) { + expect(toHaveModalOpen()).toEqual(1); + expect(toHaveBackdrop()).toBeFalsy(); + // + houskeepking(); + }); + expect(inst.opened, completes); + }); + + it('should not close modal on backdrop click if backdrop is specified as "static"', () { + ModalInstance inst = modal.open(new ModalOptions(template: '
Content
', backdrop: 'static'), scope); + inst.opened.then((value) { + expect(toHaveModalOpen()).toEqual(1); + // Trigger click event on backdrop + clickOnBackdrop(); + // + expect(toHaveModalOpen()).toEqual(1); + expect(toHaveBackdrop()).toBeTruthy(); + // + houskeepking(); + }); + expect(inst.opened, completes); + }); + + it('should close modal on button (with dismiss="modal") click if backdrop is specified as "static"', () { + ModalInstance inst = modal.open(new ModalOptions(template: '
Content
', backdrop: 'static'), scope); + inst.opened.then((value) { + expect(toHaveModalOpen()).toEqual(1); + // Trigger click event on close button + dismiss('closing in test'); + // + expect(toHaveModalOpen()).toEqual(0); + expect(toHaveBackdrop()).toBeFalsy(); + // + houskeepking(); + }); + expect(inst.opened, completes); + }); + + it('it should allow opening of multiple modals', () { + + ModalInstance inst = modal.open(new ModalOptions(template: '
Content1
'), scope); + ModalInstance inst2 = modal.open(new ModalOptions(template: '
Content2
'), scope); + Future f = Future.wait([inst.opened, inst2.opened])..then((values) { + expect(toHaveModalOpen()).toEqual(2); + + dismiss("second"); + + expect(toHaveModalOpen()).toEqual(1); + expect(toHaveModalOpenWithContent(content:'Content1', selector:'div')).toBeTruthy(); + + dismiss("first"); + expect(toHaveModalOpen()).toEqual(0); + + houskeepking(); + }); + + expect(f, completes); + }); + + it('should not close any modals on ESC if the topmost one does not allow it', () { + + ModalInstance inst = modal.open(new ModalOptions(template: '
Modal1
'), scope); + ModalInstance inst2 = modal.open(new ModalOptions(template: '
Modal2
', keyboard: false), scope); + Future f = Future.wait([inst.opened, inst2.opened])..then((values) { + expect(toHaveModalOpen()).toEqual(2); + + triggerKeyDown(dom.document, 27); + rootScope.rootScope.apply(); + + expect(toHaveModalOpen()).toEqual(2); + + houskeepking(); + }); + + expect(f, completes); + }); + + it('should not close any modals on click if a topmost modal does not have backdrop', () { + + ModalInstance inst = modal.open(new ModalOptions(template: '
Modal1
'), scope); + ModalInstance inst2 = modal.open(new ModalOptions(template: '
Modal2
', backdrop: 'false'), scope); + Future f = Future.wait([inst.opened, inst2.opened])..then((values) { + expect(toHaveModalOpen()).toEqual(2); + + clickOnBackdrop(); + + expect(toHaveModalOpen()).toEqual(2); + + houskeepking(); + }); + + expect(f, completes); + }); + + it('multiple modals should not interfere with default options', () { + ModalInstance inst = modal.open(new ModalOptions(template: '
Modal1
', backdrop: 'false'), scope); + ModalInstance inst2 = modal.open(new ModalOptions(template: '
Modal2
'), scope); + Future f = Future.wait([inst.opened, inst2.opened])..then((values) { + expect(toHaveModalOpen()).toEqual(2); + expect(toHaveBackdrop()).toBeTruthy(); + + houskeepking(); + }); + + expect(inst.opened, completes); + }); + }); +} diff --git a/test/unit/pagination/pager_component_test.dart b/test/unit/pagination/pager_component_test.dart new file mode 100644 index 0000000..a464e06 --- /dev/null +++ b/test/unit/pagination/pager_component_test.dart @@ -0,0 +1,320 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. + +part of angular_ui_test; + +testPagerComponent() { + describe("[PagerComponent]", () { + TestBed _; + Scope scope; + + beforeEach(setUpInjector); + afterEach(tearDownInjector); + + beforeEach(() { + module((Module _) => _ + ..install(new PaginationModule()) + ); + return loadTemplates(['/pagination/pager.html']); + }); + + /*****************/ + String getHtml() { + return ''; + }; + + Map getScope() { + return {'total': 47, 'currentPage': 3}; + } + + /*********************/ + int getPaginationBarSize(pager) { + return pager.querySelectorAll('li').length; + } + + dom.Element getPaginationEl(pager, index) { + return pager.querySelectorAll('li').elementAt(index); + } + + String getPaginationElText(pager, index) { + return getPaginationEl(pager, index).firstChild.text; + } + + void clickPaginationEl(pager, index) { + getPaginationEl(pager, index).querySelector('a').click(); + } + + /****************/ + void setTotalItems(scope, value) { + scope.context['total'] = value; + } + + int getCurrentPage(scope) => scope.context['currentPage']; + + void setCurrentPage(scope, value) { + scope.context['currentPage'] = value; + } + + void updateCurrentPage(scope, value) { + scope.apply(() => setCurrentPage(scope, value)); + } + + it('has a "pager" css class', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pager = shadowRoot.querySelector('pager > ul'); + expect(pager).toHaveClass('pager'); + })); + + it('contains 2 li elements', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pager = shadowRoot.querySelector('pager > ul'); + expect(getPaginationBarSize(pager)).toBe(2); + expect(getPaginationElText(pager, 0)).toEqual('« Previous'); + expect(getPaginationElText(pager, 1)).toEqual('Next »'); + })); + + it('aligns previous & next page', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pager = shadowRoot.querySelector('pager > ul'); + + expect(getPaginationEl(pager, 0)).toHaveClass('previous'); + expect(getPaginationEl(pager, 0)).not.toHaveClass('next'); + + expect(getPaginationEl(pager, 1)).not.toHaveClass('previous'); + expect(getPaginationEl(pager, 1)).toHaveClass('next'); + })); + + it('disables the "previous" link if current page is 1', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pager = shadowRoot.querySelector('pager > ul'); + + updateCurrentPage(scope, 1); + expect(getPaginationEl(pager, 0)).toHaveClass('disabled'); + })); + + it('disables the "next" link if current page is num-pages', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pager = shadowRoot.querySelector('pager > ul'); + + updateCurrentPage(scope, 5); + expect(getPaginationEl(pager, 1)).toHaveClass('disabled'); + })); + + it('changes currentPage if the "previous" link is clicked', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pager = shadowRoot.querySelector('pager > ul'); + + clickPaginationEl(pager, 0); + expect(getCurrentPage(scope)).toBe(2); + })); + + it('changes currentPage if the "next" link is clicked', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pager = shadowRoot.querySelector('pager > ul'); + + clickPaginationEl(pager, 1); + expect(getCurrentPage(scope)).toBe(4); + })); + + it('does not change the current page on "previous" click if already at first page', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pager = shadowRoot.querySelector('pager > ul'); + + updateCurrentPage(scope, 1); + clickPaginationEl(pager, 0); + expect(getCurrentPage(scope)).toBe(1); + })); + + it('does not change the current page on "next" click if already at last page', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pager = shadowRoot.querySelector('pager > ul'); + + updateCurrentPage(scope, 5); + clickPaginationEl(pager, 1); + expect(getCurrentPage(scope)).toBe(5); + })); + + it('executes the `on-select-page` expression when an element is clicked', compileComponent( + '', + {'total': 47, 'currentPage': 3, 'selectPageHandler': guinness.createSpy('selectPageHandler')}, + (Scope scope, dom.HtmlElement shadowRoot) { + var pager = shadowRoot.querySelector('pager > ul'); + + clickPaginationEl(pager, 1); + expect(scope.context['selectPageHandler']).toHaveBeenCalled(); + })); + + it('does not changes the number of pages when `total-items` changes', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pager = shadowRoot.querySelector('pager > ul'); + + scope.apply(() => setTotalItems(scope, 73)); // 8 pages + + expect(getPaginationBarSize(pager)).toBe(2); + expect(getPaginationElText(pager, 0)).toEqual('« Previous'); + expect(getPaginationElText(pager, 1)).toEqual('Next »'); + })); + + describe('`items-per-page`', () { + + String getHtml() { + return ''; + }; + + Map getScope() { + return {'total': 47, 'perPage': 5, 'selectedPage': 3}; + } + + it('does not change the number of pages', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pager = shadowRoot.querySelector('pager > ul'); + + expect(getPaginationBarSize(pager)).toBe(2); + expect(getPaginationElText(pager, 0)).toEqual('« Previous'); + expect(getPaginationElText(pager, 1)).toEqual('Next »'); + })); + + it('selects the last page when it is too big', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pager = shadowRoot.querySelector('pager > ul'); + + scope.context['perPage'] = 30; + digest(); + + expect(scope.context['selectedPage']).toBe(2); + expect(getPaginationBarSize(pager)).toBe(2); + expect(getPaginationEl(pager, 0)).not.toHaveClass('disabled'); + expect(getPaginationEl(pager, 1)).toHaveClass('disabled'); + })); + }); + + describe('when `page` is not a number', () { + it('handles string', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pager = shadowRoot.querySelector('pager > ul'); + + updateCurrentPage(scope,'1'); + expect(getPaginationEl(pager, 0)).toHaveClass('disabled'); + + updateCurrentPage(scope, '05'); + expect(getPaginationEl(pager, 1)).toHaveClass('disabled'); + })); + }); + + describe('`num-pages`', () { + it('equals to total number of pages', compileComponent( + '', + {'total': 47, 'currentPage': 3, 'numpg': null}, + (Scope scope, dom.HtmlElement shadowRoot) { + var pager = shadowRoot.querySelector('pager > ul'); + + expect(scope.context['numpg']).toBe(5); + })); + }); + + describe('setting `pagerConfig`', () { + + beforeEach(() { + inject((PagerConfig p) { + p.itemsPerPage = 10; + p.previousText = 'PR'; + p.nextText = 'NE'; + p.align = false; + }); + }); + + it('should change paging text', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pager = shadowRoot.querySelector('pager > ul'); + + expect(getPaginationElText(pager, 0)).toEqual('PR'); + expect(getPaginationElText(pager, 1)).toEqual('NE'); + })); + + it('should not align previous & next page link', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pager = shadowRoot.querySelector('pager > ul'); + + expect(getPaginationEl(pager, 0)).not.toHaveClass('previous'); + expect(getPaginationEl(pager, 1)).not.toHaveClass('next'); + })); + }); + + describe('override configuration from attributes', () { + String getHtml() { + return ''; + }; + + it('contains 2 li elements', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pager = shadowRoot.querySelector('pager > ul'); + + expect(getPaginationBarSize(pager)).toBe(2); + })); + + it('should change paging text from attributes', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pager = shadowRoot.querySelector('pager > ul'); + + expect(getPaginationElText(pager, 0)).toEqual('<'); + expect(getPaginationElText(pager, 1)).toEqual('>'); + })); + + it('should not align previous & next page link', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pager = shadowRoot.querySelector('pager > ul'); + + expect(getPaginationEl(pager, 0)).not.toHaveClass('previous'); + expect(getPaginationEl(pager, 1)).not.toHaveClass('next'); + })); + + it('changes "previous" & "next" text from interpolated attributes', compileComponent( + '', + {'total': 47, 'currentPage': 3, 'previousText': '<<', 'nextText': '>>'}, + (Scope scope, dom.HtmlElement shadowRoot) { + var pager = shadowRoot.querySelector('pager > ul'); + + + expect(getPaginationElText(pager, 0)).toEqual('<<'); + expect(getPaginationElText(pager, 1)).toEqual('>>'); + })); + }); + }); +} diff --git a/test/unit/pagination/pagination_component_test.dart b/test/unit/pagination/pagination_component_test.dart new file mode 100644 index 0000000..3d6a442 --- /dev/null +++ b/test/unit/pagination/pagination_component_test.dart @@ -0,0 +1,889 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. + +part of angular_ui_test; + +testPaginationComponent() { + describe("[PaginationComponent]", () { + TestBed _; + Scope scope; + + beforeEach(setUpInjector); + afterEach(tearDownInjector); + + beforeEach(() { + module((Module _) => _ + ..install(new PaginationModule()) + ); + return loadTemplates(['/pagination/pagination.html']); + }); + + /****************/ + void setTotalItems(scope, value) { + scope.context['total'] = value; + } + + int getCurrentPage(scope) => scope.context['currentPage']; + + void setCurrentPage(scope, value) { + scope.context['currentPage'] = value; + } + + void updateCurrentPage(scope, value) { + scope.apply(() => setCurrentPage(scope, value)); + } + + void setItemsPerPage(scope, int value) { + scope.context['perPage'] = value; + } + + /*****************/ + String getHtml() { + return ''; + }; + + Map getScope() { + return {'total': 47, 'currentPage': 3}; + } + + /*********************/ + int getPaginationBarSize(pagination) { + return pagination.querySelectorAll('li').length; + } + + dom.Element getPaginationEl(pagination, index) { + return pagination.querySelectorAll('li').elementAt(index); + } + + String getPaginationElText(pagination, index) { + return getPaginationEl(pagination, index).firstChild.text; + } + + void clickPaginationEl(pagination, index) { + getPaginationEl(pagination, index).querySelector('a').click(); + } + + /****************/ + + it('has a "pagination" css class', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + expect(pagination).toHaveClass('pagination'); + })); + + it('contains num-pages + 2 li elements', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + expect(getPaginationBarSize(pagination)).toBe(7); + expect(getPaginationElText(pagination, 0)).toEqual('Previous'); + expect(getPaginationElText(pagination, 6)).toEqual('Next'); + })); + + it('has the number of the page as text in each page item', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + for (var i = 1; i <= 5; i++) { + expect(getPaginationElText(pagination, i)).toEqual('$i'); + } + })); + + it('sets the current page to be active', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + expect(getPaginationEl(pagination, getCurrentPage(scope))).toHaveClass('active'); + })); + + it('disables the "previous" link if current page is 1', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + updateCurrentPage(scope, 1); + expect(getPaginationEl(pagination, 0)).toHaveClass('disabled'); + })); + + it('disables the "next" link if current page is last', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + updateCurrentPage(scope, 5); + expect(getPaginationEl(pagination, 6)).toHaveClass('disabled'); + })); + + it('changes currentPage if a page link is clicked', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + clickPaginationEl(pagination, 2); + expect(getCurrentPage(scope)).toBe(2); + })); + + it('changes currentPage if the "previous" link is clicked', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + clickPaginationEl(pagination, 0); + expect(getCurrentPage(scope)).toBe(2); + })); + + it('changes currentPage if the "next" link is clicked', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + clickPaginationEl(pagination, 6); + expect(getCurrentPage(scope)).toBe(4); + })); + + it('does not change the current page on "previous" click if already at first page', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + updateCurrentPage(scope, 1); + clickPaginationEl(pagination, 0); + expect(getCurrentPage(scope)).toBe(1); + })); + + it('does not change the current page on "next" click if already at last page', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + updateCurrentPage(scope, 5); + clickPaginationEl(pagination, 6); + expect(getCurrentPage(scope)).toBe(5); + })); + + it('changes the number of pages when `total-items` changes', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + scope.apply(() => setTotalItems(scope, 78)); // 8 pages + + expect(getPaginationBarSize(pagination)).toBe(10); + expect(getPaginationElText(pagination, 0)).toEqual('Previous'); + expect(getPaginationElText(pagination, 9)).toEqual('Next'); + })); + + it('does not "break" when `total-items` is undefined', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + scope.apply(() => setTotalItems(scope, null)); + + expect(getPaginationBarSize(pagination)).toBe(3); // Previous, 1, Next + expect(getPaginationEl(pagination, 0)).toHaveClass('disabled'); + expect(getPaginationEl(pagination, 1)).toHaveClass('active'); + expect(getPaginationEl(pagination, 2)).toHaveClass('disabled'); + })); + + it('does not "break" when `total-items` is negative', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + scope.apply(() => setTotalItems(scope, -1)); + + expect(getPaginationBarSize(pagination)).toBe(3); // Previous, 1, Next + expect(getPaginationEl(pagination, 0)).toHaveClass('disabled'); + expect(getPaginationEl(pagination, 1)).toHaveClass('active'); + expect(getPaginationEl(pagination, 2)).toHaveClass('disabled'); + })); + + it('does not change the current page when `total-items` changes but is valid', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + scope.apply(() { + setCurrentPage(scope, 1); + setTotalItems(scope, 18); + }); // 2 pages + + expect(getCurrentPage(scope)).toBe(1); + })); + describe('`items-per-page`', () { + + String getHtml() { + return ''; + }; + + Map getScope() { + return {'total': 47, 'perPage': 5, 'selectedPage': 3}; + } + + it('changes the number of pages', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + expect(getPaginationBarSize(pagination)).toBe(12); + expect(getPaginationElText(pagination, 0)).toEqual('Previous'); + expect(getPaginationElText(pagination, 11)).toEqual('Next'); + })); + + it('changes the number of pages when changes', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + scope.apply(() => setItemsPerPage(scope, 20)); + + expect(getPaginationBarSize(pagination)).toBe(5); + expect(getPaginationElText(pagination, 0)).toEqual('Previous'); + expect(getPaginationElText(pagination, 4)).toEqual('Next'); + })); + + it('selects the last page when current page is too big', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + scope.apply(() => setItemsPerPage(scope, 30)); + + expect(scope.context['selectedPage']).toBe(2); + expect(getPaginationBarSize(pagination)).toBe(4); + expect(getPaginationElText(pagination, 0)).toEqual('Previous'); + expect(getPaginationElText(pagination, 3)).toEqual('Next'); + })); + + it('displays a single page when it is negative', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + scope.apply(() => setItemsPerPage(scope, -1)); + + expect(getPaginationBarSize(pagination)).toBe(3); + expect(getPaginationElText(pagination, 0)).toEqual('Previous'); + expect(getPaginationElText(pagination, 1)).toEqual('1'); + expect(getPaginationElText(pagination, 2)).toEqual('Next'); + })); + }); + + describe('executes `on-select-page` expression', () { + + String getHtml() { + return ''; + }; + + Map getScope() { + return {'total': 47, 'currentPage': 3, 'selectPageHandler': guinness.createSpy('selectPageHandler')}; + } + + it('when an element is clicked', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + clickPaginationEl(pagination, 2); + expect(scope.context['selectPageHandler']).toHaveBeenCalled(); + })); + }); + + describe('when `page` is not a number', () { + it('handles numerical string', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + updateCurrentPage(scope, '2'); + expect(getPaginationEl(pagination, 2)).toHaveClass('active'); + + updateCurrentPage(scope, '04'); + expect(getPaginationEl(pagination, 4)).toHaveClass('active'); + })); + + it('defaults to 1 if non-numeric', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + updateCurrentPage(scope, 'pizza'); + expect(getPaginationEl(pagination, 1)).toHaveClass('active'); + })); + }); + + describe('with `max-size` option', (){ + + String getHtml() { + return ''; + }; + + Map getScope() { + return {'total': 98, 'currentPage': 3, 'maxSize': 5}; + } + + it('contains maxsize + 2 li elements', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + expect(getPaginationBarSize(pagination)).toBe(7); + expect(getPaginationElText(pagination, 0)).toEqual('Previous'); + expect(getPaginationElText(pagination, 6)).toEqual('Next'); + })); + + it('shows the page number even if it can\'t be shown in the middle', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + updateCurrentPage(scope, 1); + expect(getPaginationEl(pagination, 1)).toHaveClass('active'); + + updateCurrentPage(scope, 10); + expect(getPaginationEl(pagination, 5)).toHaveClass('active'); + })); + + it('shows the page number in middle after the next link is clicked', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + updateCurrentPage(scope, 6); + clickPaginationEl(pagination, 6); + + expect(getCurrentPage(scope)).toBe(7); + expect(getPaginationEl(pagination, 3)).toHaveClass('active'); + expect(getPaginationElText(pagination, 3)).toEqual(getCurrentPage(scope).toString()); + })); + + it('shows the page number in middle after the prev link is clicked', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + updateCurrentPage(scope, 7); + clickPaginationEl(pagination, 0); + + expect(getCurrentPage(scope)).toBe(6); + expect(getPaginationEl(pagination, 3)).toHaveClass('active'); + expect(getPaginationElText(pagination, 3)).toEqual(getCurrentPage(scope).toString()); + })); + + it('changes pagination bar size when max-size value changed', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + scope.apply(() => scope.context['maxSize'] = 7); + expect(getPaginationBarSize(pagination)).toBe(9); + })); + + it('sets the pagination bar size to num-pages, if max-size is greater than num-pages', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + scope.apply(() => scope.context['maxSize'] = 15); + expect(getPaginationBarSize(pagination)).toBe(12); + })); + + it('should not change value of max-size expression, if max-size is greater than num-pages', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + scope.apply(() => scope.context['maxSize'] = 15); + expect(scope.context['maxSize']).toBe(15); + })); + + it('should not display page numbers, if max-size is zero', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + scope.apply(() => scope.context['maxSize'] = 0); + expect(getPaginationBarSize(pagination)).toBe(2); + expect(getPaginationElText(pagination, 0)).toEqual('Previous'); + expect(getPaginationElText(pagination, 1)).toEqual('Next'); + })); + }); + + describe('with `max-size` option & no `rotate`', (){ + + String getHtml() { + return ''; + }; + + Map getScope() { + return {'total': 115, 'currentPage': 7, 'maxSize': 5, 'rotate': false}; + } + + it('contains maxsize + 4 elements', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + expect(getPaginationBarSize(pagination)).toBe(9); + expect(getPaginationElText(pagination, 0)).toEqual('Previous'); + expect(getPaginationElText(pagination, 1)).toEqual('...'); + expect(getPaginationElText(pagination, 2)).toEqual('6'); + expect(getPaginationElText(pagination, 6)).toEqual('10'); + expect(getPaginationElText(pagination, 7)).toEqual('...'); + expect(getPaginationElText(pagination, 8)).toEqual('Next'); + })); + + it('shows only the next ellipsis element on first page set', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + updateCurrentPage(scope, 3); + expect(getPaginationElText(pagination, 1)).toEqual('1'); + expect(getPaginationElText(pagination, 5)).toEqual('5'); + expect(getPaginationElText(pagination, 6)).toEqual('...'); + })); + + it('shows only the previous ellipsis element on last page set', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + updateCurrentPage(scope, 12); + expect(getPaginationElText(pagination, 1)).toEqual('...'); + expect(getPaginationElText(pagination, 2)).toEqual('11'); + expect(getPaginationElText(pagination, 3)).toEqual('12'); + })); + + it('moves to the previous set when first ellipsis is clicked', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + expect(getPaginationElText(pagination, 1)).toEqual('...'); + + clickPaginationEl(pagination, 1); + + expect(getCurrentPage(scope)).toEqual(5); + expect(getPaginationEl(pagination, 5)).toHaveClass('active'); + })); + + it('moves to the next set when last ellipsis is clicked', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + expect(getPaginationElText(pagination, 7)).toEqual('...'); + + clickPaginationEl(pagination, 7); + + expect(getCurrentPage(scope)).toEqual(11); + expect(getPaginationEl(pagination, 2)).toHaveClass('active'); + })); + + it('should not display page numbers, if max-size is zero', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + scope.apply(() => scope.context['maxSize'] = 0); + expect(getPaginationBarSize(pagination)).toBe(2); + expect(getPaginationElText(pagination, 0)).toEqual('Previous'); + expect(getPaginationElText(pagination, 1)).toEqual('Next'); + })); + }); + + describe('pagination directive with `boundary-links`', (){ + String getHtml() { + return ''; + }; + + it('contains num-pages + 4 li elements', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + expect(getPaginationBarSize(pagination)).toBe(9); + expect(getPaginationElText(pagination, 0)).toEqual('First'); + expect(getPaginationElText(pagination, 1)).toEqual('Previous'); + expect(getPaginationElText(pagination, 7)).toEqual('Next'); + expect(getPaginationElText(pagination, 8)).toEqual('Last'); + })); + + it('has first and last li elements visible', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + expect(getPaginationEl(pagination, 0).style.display).not.toEqual('none'); + expect(getPaginationEl(pagination, 8).style.display).not.toEqual('none'); + })); + + it('disables the "first" & "previous" link if current page is 1', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + updateCurrentPage(scope, 1); + + expect(getPaginationEl(pagination, 0)).toHaveClass('disabled'); + expect(getPaginationEl(pagination, 1)).toHaveClass('disabled'); + })); + + it('disables the "last" & "next" link if current page is num-pages', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + updateCurrentPage(scope, 5); + + expect(getPaginationEl(pagination, 7)).toHaveClass('disabled'); + expect(getPaginationEl(pagination, 8)).toHaveClass('disabled'); + })); + + it('changes currentPage if the "first" link is clicked', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + clickPaginationEl(pagination, 0); + expect(getCurrentPage(scope)).toBe(1); + })); + + it('changes currentPage if the "last" link is clicked', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + clickPaginationEl(pagination, 8); + expect(getCurrentPage(scope)).toBe(5); + })); + + it('does not change the current page on "first" click if already at first page', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + updateCurrentPage(scope, 1); + clickPaginationEl(pagination, 0); + expect(getCurrentPage(scope)).toBe(1); + })); + + it('does not change the current page on "last" click if already at last page', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + updateCurrentPage(scope, 5); + clickPaginationEl(pagination, 8); + expect(getCurrentPage(scope)).toBe(5); + })); + + it('changes "first" & "last" text from attributes', compileComponent( + '', + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + expect(getPaginationElText(pagination, 0)).toEqual('<<<'); + expect(getPaginationElText(pagination, 8)).toEqual('>>>'); + })); + + it('changes "previous" & "next" text from attributes', compileComponent( + '', + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + expect(getPaginationElText(pagination, 1)).toEqual('<<'); + expect(getPaginationElText(pagination, 7)).toEqual('>>'); + })); + + it('changes "first" & "last" text from interpolated attributes', compileComponent( + '', + {'total': 47, 'currentPage': 3, 'myFirstText': '<<<', 'myNextText': '>>>'}, + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + expect(getPaginationElText(pagination, 0)).toEqual('<<<'); + expect(getPaginationElText(pagination, 8)).toEqual('>>>'); + })); + + it('changes "previous" & "next" text from interpolated attributes', compileComponent( + '', + {'total': 47, 'currentPage': 3, 'previousText': '<<', 'nextText': '>>'}, + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + expect(getPaginationElText(pagination, 1)).toEqual('<<'); + expect(getPaginationElText(pagination, 7)).toEqual('>>'); + })); + }); + + describe('pagination directive with just number links', (){ + + String getHtml() { + return ''; + }; + + it('contains num-pages li elements', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + expect(getPaginationBarSize(pagination)).toBe(5); + expect(getPaginationElText(pagination, 0)).toEqual('1'); + expect(getPaginationElText(pagination, 4)).toEqual('5'); + })); + + it('has the number of the page as text in each page item', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + for(var i = 0; i < 5; i++) { + expect(getPaginationElText(pagination, i)).toEqual((i+1).toString()); + } + })); + + it('sets the current page to be active', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + expect(getPaginationEl(pagination, 2)).toHaveClass('active'); + })); + + it('does not disable the "1" link if current page is 1',compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + updateCurrentPage(scope, 1); + + expect(getPaginationEl(pagination, 0)).not.toHaveClass('disabled'); + expect(getPaginationEl(pagination, 0)).toHaveClass('active'); + })); + + it('does not disable the "last" link if current page is last page', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + updateCurrentPage(scope, 5); + + expect(getPaginationEl(pagination, 4)).not.toHaveClass('disabled'); + expect(getPaginationEl(pagination, 4)).toHaveClass('active'); + })); + + it('changes currentPage if a page link is clicked', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + clickPaginationEl(pagination, 1); + + expect(getCurrentPage(scope)).toBe(2); + })); + + it('changes the number of items when total items changes', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + scope.apply(()=> setTotalItems(scope, 73)); // 8 pages + + expect(getPaginationBarSize(pagination)).toBe(8); + expect(getPaginationElText(pagination, 0)).toEqual('1'); + expect(getPaginationElText(pagination, 7)).toEqual('8'); + })); + }); + + describe('with just boundary & number links', (){ + + String getHtml() { + return ''; + }; + + Map getScope() { + return {'total': 47, 'currentPage': 3, 'directions': false}; + } + + it('contains number of pages + 2 li elements', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + expect(getPaginationBarSize(pagination)).toBe(7); + expect(getPaginationElText(pagination, 0)).toEqual('First'); + expect(getPaginationElText(pagination, 1)).toEqual('1'); + expect(getPaginationElText(pagination, 5)).toEqual('5'); + expect(getPaginationElText(pagination, 6)).toEqual('Last'); + })); + + it('disables the "first" & activates "1" link if current page is 1', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + updateCurrentPage(scope, 1); + + expect(getPaginationEl(pagination, 0)).toHaveClass('disabled'); + expect(getPaginationEl(pagination, 1)).not.toHaveClass('disabled'); + expect(getPaginationEl(pagination, 1)).toHaveClass('active'); + })); + + it('disables the "last" & "next" link if current page is num-pages', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + updateCurrentPage(scope, 5); + + expect(getPaginationEl(pagination, 5)).toHaveClass('active'); + expect(getPaginationEl(pagination, 5)).not.toHaveClass('disabled'); + expect(getPaginationEl(pagination, 6)).toHaveClass('disabled'); + })); + }); + + describe('`num-pages`', (){ + + String getHtml() { + return ''; + }; + + Map getScope() { + return {'total': 47, 'currentPage': 3, 'numpg': null}; + } + + it('disables the "last" & "next" link if current page is num-pages', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + + expect(scope.context['numpg']).toBe(5); + })); + + it('changes when total number of pages change', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + + scope.apply(()=>setTotalItems(scope, 73)); // 8 pages + expect(scope.context['numpg']).toBe(8); + })); + + it('shows minimun one page if total items are not defined and does not break binding', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + + scope.apply(()=>setTotalItems(scope, null)); + expect(scope.context['numpg']).toBe(1); + + scope.apply(()=>setTotalItems(scope, 73)); // 8 pages + expect(scope.context['numpg']).toBe(8); + })); + }); + + describe('setting `paginationConfig`', (){ + + it('shows minimun one page if total items are not defined and does not break binding', async(inject((PaginationConfig paginationConfig){ + paginationConfig.boundaryLinks = true; + paginationConfig.directionLinks = true; + paginationConfig.firstText = 'FI'; + paginationConfig.previousText = 'PR'; + paginationConfig.nextText = 'NE'; + paginationConfig.lastText = 'LA'; + + compileComponent(getHtml(), getScope(), (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + expect(getPaginationElText(pagination, 0)).toEqual('FI'); + expect(getPaginationElText(pagination, 1)).toEqual('PR'); + expect(getPaginationElText(pagination, 7)).toEqual('NE'); + expect(getPaginationElText(pagination, 8)).toEqual('LA'); + }); + }))); + + it('contains number of pages + 2 li elements', async(inject((PaginationConfig paginationConfig){ + paginationConfig.itemsPerPage = 5; + compileComponent(getHtml(), getScope(), (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + expect(getPaginationBarSize(pagination)).toBe(12); + }); + }))); + + it('should take maxSize defaults into account', async(inject((PaginationConfig paginationConfig){ + paginationConfig.maxSize = 2; + compileComponent(getHtml(), getScope(), (Scope scope, dom.HtmlElement shadowRoot) { + var pagination = shadowRoot.querySelector('pagination > ul'); + + expect(getPaginationBarSize(pagination)).toBe(4); + }); + }))); + }); + }); +} diff --git a/test/unit/popover/popover_component_test.dart b/test/unit/popover/popover_component_test.dart new file mode 100644 index 0000000..f6a9f4b --- /dev/null +++ b/test/unit/popover/popover_component_test.dart @@ -0,0 +1,75 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. + +part of angular_ui_test; + +testPopoverComponent() { + describe("[PopoverComponent]", () { + TestBed _; + Scope scope; + Timeout timeout; + + beforeEach(setUpInjector); + afterEach(tearDownInjector); + + beforeEach(() { + module((Module _) => _ + ..install(new TimeoutModule()) + ..install(new PositionModule()) + ..install(new PopoverModule()) + ); + inject((TestBed tb) { _ = tb; }); + inject((Timeout t) { timeout = t; }); + }); + + String getHtml() { + return 'Selector Text'; + }; + + Popover getPopover(dom.Element elm) { + return ngProbe(elm).directives.firstWhere((e) => e is Popover); + } + + it('should not be open initially', compileComponent( + getHtml(), + {}, + (Scope scope, dom.HtmlElement shadowRoot) { + var elm = shadowRoot.querySelector('span'); + var popover = getPopover(elm); + + expect(popover.tt_isOpen).toBe(false); + + // We can only test *that* the popover-popup element wasn't created as the + // implementation is templated and replaced. + expect(shadowRoot.children.length ).toBe(1); + })); + + it('should open on click', compileComponent( + getHtml(), + {}, + (Scope scope, dom.HtmlElement shadowRoot) { + var elm = shadowRoot.querySelector('span'); + var popover = getPopover(elm); + + _.triggerEvent(elm, 'click'); + expect(popover.tt_isOpen).toBe(true); + + // We can only test *that* the tooltip-popup element was created as the + // implementation is templated and replaced. + expect(shadowRoot.children.length).toBe( 2 ); + })); + + it('should close on second click', compileComponent( + getHtml(), + {}, + (Scope scope, dom.HtmlElement shadowRoot) { + var elm = shadowRoot.querySelector('span'); + var popover = getPopover(elm); + + _.triggerEvent(elm, 'click'); + _.triggerEvent(elm, 'click'); + expect(popover.tt_isOpen).toBe(false); + })); + }); +} diff --git a/test/unit/progressbar/progressbar_component_test.dart b/test/unit/progressbar/progressbar_component_test.dart new file mode 100644 index 0000000..95f0099 --- /dev/null +++ b/test/unit/progressbar/progressbar_component_test.dart @@ -0,0 +1,299 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. + +part of angular_ui_test; + +testProgressbarComponent() { + describe("[ProgressbarComponent]", () { + TestBed _; + Scope scope; + + beforeEach(setUpInjector); + + beforeEach(() { + module((Module _) => _ + ..install(new ProgressbarModule()) + ); + inject((TestBed tb) { _ = tb; }); + //return loadTemplates(['/progressbar/bar.html', '/progressbar/progressbar.html', '/progressbar/stackedProgress.html']); + }); + + afterEach(tearDownInjector); + + String getHtml() { + return '{{value}} %'; + }; + + Map getScope() { + return {'value': 22}; + } + + var BAR_CLASS = 'progress-bar'; + + dom.Element getProgressbar(dom.Element element, [indx = 0]) => ngQuery(element, '.progress-bar')[indx]; + dom.Element getBar(dom.Element element, [indx = 0]) => ngQuery(element, 'bar')[indx]; + + it('has a "progress" css class', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var shadowElement = shadowRoot.querySelector('progressbar').children.first; + clockTick(); + + expect(shadowElement).toHaveClass('progress'); + })); + + it('contains one child element with "bar" css class', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var shadowElement = shadowRoot.querySelector('progressbar').children.first; + clockTick(); + + expect(shadowElement.children.length).toBe(1); + expect(getProgressbar(shadowElement)).toHaveClass(BAR_CLASS); + })); + + it('has a "bar" element with expected width', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var shadowElement = shadowRoot.querySelector('progressbar').children.first; + clockTick(); + + expect(getProgressbar(shadowElement).style.width).toEqual('22%'); + })); + + it('transcludes "bar" text', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var shadowElement = shadowRoot.querySelector('progressbar').children.first; + clockTick(); + + expect(shadowElement.text).toContain('22 %'); + })); + +/* it('it should be possible to add additional classes', compileComponent( + '', + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var shadowElement = shadowRoot.querySelector('stackedprogress').children.first; + clockTick(); + clockTick(); + + expect(shadowElement).toHaveClass('progress-striped'); + expect(shadowElement).toHaveClass('active'); + expect(getBar(shadowElement, 0)).toHaveClass('pizza'); + }));*/ + + describe('"max" attribute', () { + String getHtml() { + return '{{value}}/{{max}}'; + }; + + Map getScope() { + return {'value': 22, 'max': 200}; + } + + it('adjusts the "bar" width', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var shadowElement = shadowRoot.querySelector('progressbar').children.first; + clockTick(); + + expect(getProgressbar(shadowElement).style.width).toEqual('11%'); + })); + + it('adjusts the "bar" width when value changes', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var shadowElement = shadowRoot.querySelector('progressbar').children.first; + clockTick(); + + scope.context['value'] = 60; + digest(); + expect(getProgressbar(shadowElement).style.width).toEqual('30%'); + + scope.context['value'] += 12; + digest(); + expect(getProgressbar(shadowElement).style.width).toEqual('36%'); + + scope.context['value'] = 0; + digest(); + expect(getProgressbar(shadowElement).style.width).toEqual('0%'); + })); + + it('transcludes "bar" text', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var shadowElement = shadowRoot.querySelector('progressbar').children.first; + clockTick(); + + expect(shadowElement.text).toContain('22/200'); + })); + }); + + describe('"type" attribute', () { + + String getHtml() { + return 'test'; + }; + + Map getScope() { + return {'value': 22, 'type': 'success'}; + } + + it('should use correct classes', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var shadowElement = shadowRoot.querySelector('progressbar').children.first; + clockTick(); + + expect(getProgressbar(shadowElement)).toHaveClass(BAR_CLASS); + expect(getProgressbar(shadowElement)).toHaveClass(BAR_CLASS + '-success'); + })); + + it('should change classes if type changed', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var shadowElement = shadowRoot.querySelector('progressbar').children.first; + clockTick(); + + scope.context['type'] = 'warning'; + scope.context['value'] += 1; + digest(); + + expect(getProgressbar(shadowElement)).toHaveClass(BAR_CLASS); + expect(getProgressbar(shadowElement)).not.toHaveClass(BAR_CLASS + '-success'); + expect(getProgressbar(shadowElement)).toHaveClass(BAR_CLASS + '-warning'); + })); + }); + + describe('stacked', () { + + String getHtml() { + return '{{o.value}}'; + }; + + Map getScope() { + return {'value': 22, + 'objects': [ + { 'value': 10, 'type': 'success' }, + { 'value': 50, 'type': 'warning' }, + { 'value': 20, 'type': 'danger' } + ]}; + } + + it('contains the right number of bars', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var shadowElement = shadowRoot.querySelector('stackedprogress').children.first; + clockTick(); + clockTick(); + clockTick(); + clockTick(); + + expect(shadowElement.querySelectorAll('bar').length).toBe(3); + for (var i = 0; i < 3; i++) { + expect(getProgressbar(shadowElement, i)).toHaveClass(BAR_CLASS); + } + })); + + it('renders each bar with the appropriate width', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var shadowElement = shadowRoot.querySelector('stackedprogress').children.first; + clockTick(); + clockTick(); + clockTick(); + clockTick(); + + expect(getProgressbar(shadowElement, 0).style.width).toEqual('10%'); + expect(getProgressbar(shadowElement, 1).style.width).toEqual('50%'); + expect(getProgressbar(shadowElement, 2).style.width).toEqual('20%'); + })); + + it('uses correct classes', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var shadowElement = shadowRoot.querySelector('stackedprogress').children.first; + clockTick(); + clockTick(); + clockTick(); + clockTick(); + + expect(getProgressbar(shadowElement, 0)).toHaveClass(BAR_CLASS + '-success'); + expect(getProgressbar(shadowElement, 0)).not.toHaveClass(BAR_CLASS + '-warning'); + + expect(getProgressbar(shadowElement, 1)).not.toHaveClass(BAR_CLASS + '-success'); + expect(getProgressbar(shadowElement, 1)).toHaveClass(BAR_CLASS + '-warning'); + + expect(getProgressbar(shadowElement, 2)).not.toHaveClass(BAR_CLASS + '-success'); + expect(getProgressbar(shadowElement, 2)).not.toHaveClass(BAR_CLASS + '-warning'); + })); + +/* it('should change classes if type changed', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var shadowElement = shadowRoot.querySelector('stackedprogress').children.first; + clockTick(); + clockTick(); + clockTick(); + clockTick(); + + scope.context['objects'][0]['type'] = 'warning'; + scope.context['objects'][1]['type'] = ''; + scope.context['objects'][2]['type'] = 'info'; + + digest(); + + expect(getProgressbar(shadowElement, 0)).not.toHaveClass(BAR_CLASS + '-success'); + expect(getProgressbar(shadowElement, 0)).toHaveClass(BAR_CLASS + '-warning'); + + expect(getProgressbar(shadowElement, 1)).not.toHaveClass(BAR_CLASS + '-success'); + expect(getProgressbar(shadowElement, 1)).not.toHaveClass(BAR_CLASS + '-warning'); + + expect(getProgressbar(shadowElement, 2)).toHaveClass(BAR_CLASS + '-info'); + expect(getProgressbar(shadowElement, 2)).not.toHaveClass(BAR_CLASS + '-success'); + expect(getProgressbar(shadowElement, 2)).not.toHaveClass(BAR_CLASS + '-warning'); + }));*/ + + it('should change classes if type changed', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + var shadowElement = shadowRoot.querySelector('stackedprogress').children.first; + clockTick(); + clockTick(); + clockTick(); + clockTick(); + + scope.context['objects'][0]['type'] = 'info'; + scope.context['objects'][0]['value'] = 70; + scope.context['objects'].removeAt(1); + scope.context['objects'].removeAt(1); + + digest(); + clockTick(); + + expect(shadowElement.querySelectorAll('bar').length).toBe(1); + + expect(getProgressbar(shadowElement, 0)).toHaveClass(BAR_CLASS + '-info'); + expect(getProgressbar(shadowElement, 0)).not.toHaveClass(BAR_CLASS + '-success'); + expect(getProgressbar(shadowElement, 0)).not.toHaveClass(BAR_CLASS + '-warning'); + })); + + }); + }); +} diff --git a/test/unit/rating/rating_component_test.dart b/test/unit/rating/rating_component_test.dart new file mode 100644 index 0000000..a234a69 --- /dev/null +++ b/test/unit/rating/rating_component_test.dart @@ -0,0 +1,269 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. + +part of angular_ui_test; + +testRatingComponent() { + describe("[RatingComponent]", () { + TestBed _; + Scope scope; + + beforeEach(setUpInjector); + afterEach(tearDownInjector); + + beforeEach(() { + module((Module _) => _ + ..install(new RatingModule()) + ); + inject((TestBed t) => _ = t); + return loadTemplates(['rating/rating.html']); + }); + + dom.Element getSpan(dom.Element element) { + return ngQuery(element, 'span')[0]; + } + + List getStars(dom.Element element) { + return ngQuery(element, 'i'); + } + + dom.Element getStar(dom.Element element, int number) { + return getStars(element)[number - 1]; + } + + List getState(dom.Element element, [String classOn = null, String classOff = null]) { + var stars = getStars(element); + var state = []; + for (var i = 0, n = stars.length; i < n; i++) { + state.add((stars[i].classes.contains(classOn != null ? classOn : 'glyphicon-star') && !stars[i].classes.contains(classOff != null ? classOff : 'glyphicon-star-empty')) ); + } + return state; + } + + it('contains the default number of icons', compileComponent( + '', + {'rate': 3}, + (Scope scope, dom.HtmlElement shadowRoot) { + final rating = shadowRoot.querySelector('rating'); + + expect(getStars(rating).length).toBe(5); + })); + + it('initializes the default star icons as selected', compileComponent( + '', + {'rate': 3}, + (Scope scope, dom.HtmlElement shadowRoot) { + final rating = shadowRoot.querySelector('rating'); + + expect(getState(rating)).toEqual([true, true, true, false, false]); + })); + + it('handles correctly the click event', compileComponent( + '', + {'rate': 3}, + (Scope scope, dom.HtmlElement shadowRoot) { + final rating = shadowRoot.querySelector('rating'); + + getStar(rating, 2).click(); + scope.apply(); + expect(getState(rating)).toEqual([true, true, false, false, false]); + expect(scope.context['rate']).toBe(2); + + getStar(rating, 5).click(); + scope.apply(); + expect(getState(rating)).toEqual([true, true, true, true, true]); + expect(scope.context['rate']).toBe(5); + })); + + it('handles correctly the hover event', compileComponent( + '', + {'rate': 3}, + (Scope scope, dom.HtmlElement shadowRoot) { + final rating = shadowRoot.querySelector('rating'); + + _.triggerEvent(getStar(rating, 2), 'mouseover'); + expect(getState(rating)).toEqual([true, true, false, false, false]); + expect(scope.context['rate']).toBe(3); + + _.triggerEvent(getStar(rating, 5), 'mouseenter'); + expect(getState(rating)).toEqual([true, true, true, true, true]); + expect(scope.context['rate']).toBe(3); + + dom.Element span = getSpan(rating); + _.triggerEvent(span, 'mouseleave'); + expect(getState(rating)).toEqual([true, true, true, false, false]); + expect(scope.context['rate']).toBe(3); + })); + + //*************** + + it('changes the number of selected icons when value changes', compileComponent( + '', + {'rate': 3}, + (Scope scope, dom.HtmlElement shadowRoot) { + final rating = shadowRoot.querySelector('rating'); + + scope.context['rate'] = 2; + scope.apply(); + + expect(getState(rating)).toEqual([true, true, false, false, false]); + })); + + it('shows different number of icons when `max` attribute is set', compileComponent( + '', + {'rate': 3}, + (Scope scope, dom.HtmlElement shadowRoot) { + final rating = shadowRoot.querySelector('rating'); + + expect(getStars(rating).length).toBe(7); + })); + + it('shows different number of icons when `max` attribute is from scope variable', compileComponent( + '', + {'rate': 3, 'max': 15}, + (Scope scope, dom.HtmlElement shadowRoot) { + final rating = shadowRoot.querySelector('rating'); + + expect(getStars(rating).length).toBe(15); + })); + + it('handles readonly attribute', compileComponent( + '', + {'rate': 3, 'isReadonly': true}, + (Scope scope, dom.HtmlElement shadowRoot) { + final rating = shadowRoot.querySelector('rating'); + + expect(getState(rating)).toEqual([true, true, true, false, false]); + + var star5 = getStar(rating, 5); + _.triggerEvent(star5, 'mouseover'); + expect(getState(rating)).toEqual([true, true, true, false, false]); + + scope.context['isReadonly'] = false; + scope.apply(); + + _.triggerEvent(star5, 'mouseover'); + expect(getState(rating)).toEqual([true, true, true, true, true]); + })); + + it('should fire onHover', compileComponent( + '', + {'rate': 3, 'hoveringOver': guinness.createSpy('hoveringOver')}, + (Scope scope, dom.HtmlElement shadowRoot) { + final rating = shadowRoot.querySelector('rating'); + + _.triggerEvent(getStar(rating, 3), 'mouseover'); + expect(scope.context['hoveringOver']).toHaveBeenCalledWith(3); + })); + + it('should fire onLeave', compileComponent( + '', + {'rate': 3, 'leaving': guinness.createSpy('leaving')}, + (Scope scope, dom.HtmlElement shadowRoot) { + final rating = shadowRoot.querySelector('rating'); + + dom.Element span = getSpan(rating); + _.triggerEvent(span, 'mouseleave'); + expect(scope.context['leaving']).toHaveBeenCalled(); + })); + + describe('custom states', () { + it('changes the default icons', compileComponent( + '', + {'rate': 3, 'classOn': 'icon-ok-sign', 'classOff': 'icon-ok-circle'}, + (Scope scope, dom.HtmlElement shadowRoot) { + final rating = shadowRoot.querySelector('rating'); + + expect(getState(rating, scope.context['classOn'], scope.context['classOff'])).toEqual([true, true, true, false, false]); + })); + }); + + describe('`rating-states`', () { + + String getHtml() => ''; + Map getScope() => {'rate': 3, 'states': [ + {'stateOn': 'sign', 'stateOff': 'circle'}, + {'stateOn': 'heart', 'stateOff': 'ban'}, + {'stateOn': 'heart'}, + {'stateOff': 'off'} + ]}; + + it('changes the default icons', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + final rating = shadowRoot.querySelector('rating'); + + expect(getStars(rating).length).toBe(scope.context['states'].length); + })); + + it('handles each icon', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + final rating = shadowRoot.querySelector('rating'); + + var stars = getStars(rating); + + for (var i = 0; i < stars.length; i++) { + var star = stars[i]; + Map state = scope.context['states'][i]; + var isOn = i < scope.context['rate']; + + expect(star.classes.contains(state['stateOn'])).toBe(isOn); + expect(star.classes.contains(state['stateOff'])).toBe(!isOn); + } + })); + }); + + describe('setting ratingConfig', () { + RatingConfig originalConfig; + beforeEach(inject((RatingConfig ratingConfig) { + // + originalConfig = new RatingConfig() + ..max = ratingConfig.max + ..stateOn = ratingConfig.stateOn + ..stateOff = ratingConfig.stateOff; + // + ratingConfig.max = 10; + ratingConfig.stateOn = 'on'; + ratingConfig.stateOff = 'off'; + })); + + afterEach(inject((RatingConfig ratingConfig) { + // return it to the original state + ratingConfig + ..max = originalConfig.max + ..stateOn = originalConfig.stateOn + ..stateOff = originalConfig.stateOff; + })); + + String getHtml() => ''; + + it('should change number of icon elements', compileComponent( + getHtml(), + {'rate': 3}, + (Scope scope, dom.HtmlElement shadowRoot) { + final rating = shadowRoot.querySelector('rating'); + + scope.context['rate'] = 5; + digest(); + + expect(getStars(rating).length).toBe(10); + })); + + it('should change icon states', compileComponent( + getHtml(), + {'rate': 3}, + (Scope scope, dom.HtmlElement shadowRoot) { + final rating = shadowRoot.querySelector('rating'); + + scope.context['rate'] = 5; + digest(); + + expect(getState(rating, 'on', 'off')).toEqual([true, true, true, true, true, false, false, false, false, false]); + })); + }); + }); +} diff --git a/test/unit/tabs/tabs_component_test.dart b/test/unit/tabs/tabs_component_test.dart new file mode 100644 index 0000000..d0fc530 --- /dev/null +++ b/test/unit/tabs/tabs_component_test.dart @@ -0,0 +1,580 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. + +part of angular_ui_test; + +testTabsComponent() { + describe("[TabsComponent]", () { + TestBed _; + Scope scope; + + beforeEach(setUpInjector); + afterEach(tearDownInjector); + + beforeEach(() { + module((Module _) => _ + ..install(new TabsModule()) + ); + inject((TestBed t) => _ = t); + //return loadTemplates(['tabs/tab.html', 'tabs/tabset.html']); + }); + + getHtml() { + return ''' + + +
first content is {{first}}
+
+ + Second Tab {{second}} +
second content is {{second}}
+
+
'''; + } + + getScope() { + return { + 'first': '1', + 'second': '2', + 'actives': {}, + 'selectFirst': guinness.createSpy('first select listener'), + 'selectSecond': guinness.createSpy('second select listener'), + 'deselectFirst': guinness.createSpy('first deselect listener'), + 'deselectSecond': guinness.createSpy('second deselect listener') + }; + } + + it('should pass class and other attributes on to tab template', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + final tabset = shadowRoot.querySelector('tabset'); + + expect(tabset).toHaveClass('hello'); + expect(tabset.attributes['data-pizza']).toEqual('pepperoni'); + })); + + it('should create clickable titles', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + final titles = shadowRoot.querySelectorAll('ul.nav-tabs li'); + + expect(titles.length).toBe(2); + expect(ngQuery(titles[0] ,'a')[0].text).toEqual('First Tab 1'); + //It should put the tab-heading element into the 'a' title + expect(ngQuery(titles[1] ,'a')[0].text).toEqual('Second Tab 2'); + expect(ngQuery(titles[1] ,'tab-heading')[0].innerHtml ).toEqual('Second Tab 2'); + })); + +// it('should bind tabs content and set first tab active', compileComponent( +// getHtml(), +// getScope(), +// (Scope scope, dom.HtmlElement shadowRoot) { +// final contents = shadowRoot.querySelectorAll('.tab-pane'); +// +// expect(contents.length).toBe(1); +// expect(contents[0]).toHaveClass('active'); +// expect(ngQuery(shadowRoot ,'#tab-content')[0].text).toEqual('first content is 1'); +// })); +// +// it('should change active on click', compileComponent( +// getHtml(), +// getScope(), +// (Scope scope, dom.HtmlElement shadowRoot) { +// final titles = shadowRoot.querySelectorAll('ul.nav-tabs li'); +// final contents = shadowRoot.querySelectorAll('.tab-pane'); +// +// ngQuery(titles[1] , 'a')[0].click(); +// microLeap(); +// digest(); +// expect(contents[0]).toHaveClass('active'); +// expect(titles[0]).not.toHaveClass('active'); +// expect(titles[1]).toHaveClass('active'); +// })); +// +// it('should call select callback on select', compileComponent( +// getHtml(), +// getScope(), +// (Scope scope, dom.HtmlElement shadowRoot) { +// final titles = shadowRoot.querySelectorAll('ul.nav-tabs li'); +// +// ngQuery(titles[1] , 'a')[0].click(); +// expect(scope.context['selectSecond']).toHaveBeenCalled(); +// ngQuery(titles[0] , 'a')[0].click(); +// expect(scope.context['selectFirst']).toHaveBeenCalled(); +// })); +// +// it('should call deselect callback on deselect', compileComponent( +// getHtml(), +// getScope(), +// (Scope scope, dom.HtmlElement shadowRoot) { +// final titles = shadowRoot.querySelectorAll('ul.nav-tabs li'); +// +// expect(scope.context['deselectSecond']).not.toHaveBeenCalled(); +// ngQuery(titles[1] , 'a')[0].click(); +// ngQuery(titles[0] , 'a')[0].click(); +// expect(scope.context['deselectSecond']).toHaveBeenCalled(); +// ngQuery(titles[0] , 'a')[0].click(); +// expect(scope.context['deselectFirst']).toHaveBeenCalled(); +// })); +// + describe('basics with initial active tab', () { + + Map makeTab([active = false]) { + return { + 'active': active, + 'select': guinness.createSpy() + }; + } + + getHtml() { + return ''' + + + + + + + + + +'''; + } + + getScope() { + return { + 'tabs': [ makeTab(), makeTab(), makeTab(true), makeTab() ] + }; + } + + expectTabActive(dom.Element shadowRoot, Scope scope, Map activeTab) { + var _titles = shadowRoot.querySelectorAll('ul.nav-tabs li'); + var tabs = shadowRoot.querySelectorAll('tab'); + + for (var i = 0; i < scope.context['tabs'].length; i++) { + Map tab = scope.context['tabs'][i]; + if (activeTab == tab) { + expect(tab['active']).toBe(true); + //It should only call select ONCE for each select + expect(tab['select']).toHaveBeenCalled(); + expect(_titles[i]).toHaveClass('active'); + expect(tabs[i].querySelector('.tab-pane')).toHaveClass('active'); + } else { + expect(tab['active']).toBe(false); + expect(_titles[i]).not.toHaveClass('active'); + } + } + } + + it('should make tab titles and set active tab active', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + final titles = shadowRoot.querySelectorAll('ul.nav-tabs li'); + + expect(titles.length).toBe(scope.context['tabs'].length); + expectTabActive(shadowRoot, scope, scope.context['tabs'][2]); + })); + }); + + describe('tab callback order', () { + + List execOrder; + + getHtml() { + return ''' + + + +'''; + } + + getScope() { + return { + 'active': true, + 'execute': (id) => execOrder.add(id) + }; + } + + beforeEach(() { + execOrder = []; + }); + + it('should call select for the first tab', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + + expect(execOrder).toEqual(['deselect1', 'select1']); + })); + + it('should call deselect, then select', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + final titles = shadowRoot.querySelectorAll('ul.nav-tabs li'); + execOrder = []; + + // Select second tab + titles[1].querySelector('a').click(); + expect(execOrder).toEqual([ 'deselect1', 'select2' ]); + + execOrder = []; + + // Select again first tab + titles[0].querySelector('a').click(); + expect(execOrder).toEqual([ 'deselect2', 'select1' ]); + })); + }); + + describe('ng-repeat', () { + + Map makeTab([active = false]) { + return { + 'active': active, + 'select': guinness.createSpy() + }; + } + + getHtml() { + return r''' + + + heading {{index}} + content {{$index}} + +'''; + } + + getScope() { + return { + 'tabs': [ makeTab(), makeTab(), makeTab(true), makeTab() ] + }; + } + + expectTabActive(dom.Element shadowRoot, Scope scope, Map activeTab) { + var _titles = shadowRoot.querySelectorAll('ul.nav-tabs li'); + var tabs = shadowRoot.querySelectorAll('tab'); // div.tab-content div.tab-pane + + for (var i = 0; i < scope.context['tabs'].length; i++) { + Map tab = scope.context['tabs'][i]; + if (activeTab == tab) { + expect(tab['active']).toBe(true); + // It should only call select ONCE for each select + expect(tab['select']).toHaveBeenCalled(); + expect(_titles[i]).toHaveClass('active'); + expect(tabs[i].querySelector('.tab-pane')).toHaveClass('active'); + } else { + expect(tab['active']).toBe(false); + expect(_titles[i]).not.toHaveClass('active'); + } + } + } + + it('should make tab titles and set active tab active', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + // Extra cycle to draw tab content + microLeap(); + digest(); + + var titles = shadowRoot.querySelectorAll('ul.nav-tabs li'); + + expect(titles.length).toBe(scope.context['tabs'].length); + expectTabActive(shadowRoot, scope, scope.context['tabs'][2]); + })); + + it('should switch active when clicking', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + // Extra cycle to draw tab content + microLeap(); + digest(); + + var titles = shadowRoot.querySelectorAll('ul.nav-tabs li'); + + titles[3].querySelector('a').click(); + digest(); + + expectTabActive(shadowRoot, scope, scope.context['tabs'][3]); + })); + + it('should switch active when setting active=true', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + // Extra cycle to draw tab content + microLeap(); + digest(); + + scope.apply('tabs[2].active = true'); + expectTabActive(shadowRoot, scope, scope.context['tabs'][2]); + })); + + it('should deselect all when no tabs are active', compileComponent( + getHtml(), + getScope(), + (Scope scope, dom.HtmlElement shadowRoot) { + // Extra cycle to draw tab content + microLeap(); + digest(); + + for (var i = 0; i < scope.context['tabs'].length; i++) { + Map tab = scope.context['tabs'][i]; + tab['active'] = false; + } + microLeap(); + digest(); + + expectTabActive(shadowRoot, scope, null); + + final contents = shadowRoot.querySelectorAll('.tab-pane'); + expect(contents.where((dom.Element el) => el.classes.contains('active')).toList().length).toBe(0); + + scope.context['tabs'][2]['active'] = true; + microLeap(); + digest(); + + expectTabActive(shadowRoot, scope, scope.context['tabs'][2]); + })); + }); + + describe('advanced tab-heading element', () { +// +// getHtml() { +// return r''' +// +// +// +// +// +// 1 +//
2
+//
3
+//
'''; +// } +// +// getScope() { +// return { +// 'myHtml': 'hello there!', +// 'value': true +// }; +// } +// +// heading(dom.Element elm) => elm.querySelector('ul li a').children; +// +// it('should create a heading bound to myHtml', compileComponent( +// getHtml(), +// getScope(), +// (Scope scope, dom.HtmlElement shadowRoot) { +// // Extra cycle to draw tab content +// microLeap(); +// digest(); +// microLeap(); +// digest(); +// +// print(shadowRoot.outerHtml); +// //expect(heading().eq(0).html()).toBe('hello, there!'); +// })); +// +// it('should hide and show the heading depending on value', function() { +// expect(heading().eq(0)).not.toBeHidden(); +// scope.$apply('value = false'); +// expect(heading().eq(0)).toBeHidden(); +// scope.$apply('value = true'); +// expect(heading().eq(0)).not.toBeHidden(); +// }); +// +// it('should have a tab-heading no matter what syntax was used', function() { +// expect(heading().eq(1).text()).toBe('1'); +// expect(heading().eq(2).text()).toBe('2'); +// expect(heading().eq(3).text()).toBe('3'); +// }); + }); + + describe('tabset component', () { + mockTab(TabsetComponent tabSet, [isActive = false]) { + return new TabComponent(new dom.DivElement(), tabSet) + ..active = isActive + ..onSelectCallback = null + ..onDeselectCallback = null; + } + + TabsetComponent cmp; + + beforeEach(() { + inject((TabsetComponent v) => cmp = v); + }); + + describe('select', () { + it('should mark given tab selected', () { + var tab = mockTab(cmp); + + cmp.select(tab); + expect(tab.active).toBe(true); + }); + + it('should deselect other tabs', () { + var tab1 = mockTab(cmp), tab2 = mockTab(cmp), tab3 = mockTab(cmp); + + cmp.addTab(tab1); + cmp.addTab(tab2); + cmp.addTab(tab3); + + cmp.select(tab1); + expect(tab1.active).toBe(true); + expect(tab2.active).toBe(false); + expect(tab3.active).toBe(false); + + cmp.select(tab2); + expect(tab1.active).toBe(false); + expect(tab2.active).toBe(true); + expect(tab3.active).toBe(false); + + cmp.select(tab3); + expect(tab1.active).toBe(false); + expect(tab2.active).toBe(false); + expect(tab3.active).toBe(true); + }); + }); + + describe('addTab', () { + + it('should append tab', () { + var tab1 = mockTab(cmp), tab2 = mockTab(cmp); + + expect(cmp.tabs).toEqual([tab1, tab2]); + }); + + +// it('should select the first one', () { +// var tab1 = mockTab(cmp), tab2 = mockTab(cmp); +// +// expect(tab1.active).toBe(true); +// }); + +// it('should select a tab added that\'s already active', () { +// var tab1 = mockTab(cmp), tab2 = mockTab(cmp, true); +// expect(tab1.active).toBe(true); +// +// cmp.addTab(tab2); +// expect(tab1.active).toBe(false); +// expect(tab2.active).toBe(true); +// }); + }); + }); + +// describe('remove', () { +// getHtml() { +// return '''Hellocontent {{i}}'''; +// } +// +// getScope() { +// return {}; +// } +// +// expectTitles(dom.HtmlElement shadowRoot, titlesArray) { +// var t = shadowRoot.querySelectorAll('ul.nav-tabs li'); +// expect(t.length).toEqual(titlesArray.length); +// for (var i=0; i', +// ' ', +// ' heading {{index}}', +// ' content {{$index}}', +// ' ', +// '' +// ].join('\n'))(scope); +// scope.$apply(); +// +// // The first tab is selected the during the initial $digest. +// expect(selectList.length).toEqual(1); +// +// // Destroy the tabs - we should not trigger selection/deselection any more. +// scope.$destroy(); +// expect(selectList.length).toEqual(1); +// expect(deselectList.length).toEqual(0); +// })); +// }); + }); +} \ No newline at end of file diff --git a/test/unit/timepicker/timepicker_component_test.dart b/test/unit/timepicker/timepicker_component_test.dart new file mode 100644 index 0000000..d97fe87 --- /dev/null +++ b/test/unit/timepicker/timepicker_component_test.dart @@ -0,0 +1,1064 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. + +part of angular_ui_test; + +testTimepickerComponent() { + DateTime newTime(hours, minutes) { + var time = new DateTime.now(); + return new DateTime(time.year, time.month, time.day, hours, minutes, time.second, time.millisecond); + } + + describe("[TimepickerComponent]", () { + + beforeEach(setUpInjector); + afterEach(tearDownInjector); + + beforeEach(() { + module((Module _) => _ + ..install(new TimepickerModule()) + ); +// return loadTemplates(['/timepicker/timepicker.html']); + }); + + String getHtml({String extra:''}) { + return r''; + }; + + Map getScopeContent() { + return {'time': newTime(14, 40)}; + }; + + List getModelState(Scope scope) { + return [scope.context['time'].hour, scope.context['time'].minute]; + } + + dom.Element getArrow(timepicker, isUp, tdIndex) { + var el = ngQuery(timepicker, 'tr')[(isUp) ? 0 : 2]; + el = ngQuery(el, 'td')[tdIndex]; + return ngQuery(el, 'a')[0]; + } + + dom.Element getHoursButton(timepicker, isUp) { + return getArrow(timepicker, isUp, 0); + } + + dom.Element getMinutesButton(timepicker, isUp) { + return getArrow(timepicker, isUp, 2); + } + + dom.ButtonElement getMeridianButton(timepicker) { + return ngQuery(timepicker, 'button')[0]; + } + + void doClick(Scope scope, button, [int n = 1]) { + for (var i = 0, max = n; i < max; i++) { + button.click(); + microLeap(); + digest(); + } + } + + dom.Event wheelThatMouse(delta) { + var e = new dom.WheelEvent('mousewheel', deltaX:delta); + return e; + } + + dom.Event wheelThatOtherMouse(delta) { + var e = new dom.WheelEvent('wheel', deltaY:delta); + return e; + } + + List getTimeState(timepicker, [withoutMeridian = false]) { + List inputs = ngQuery(timepicker, 'input'); + + var state = []; + for (var i = 0; i < 2; i ++) { + state.add(inputs[i].value); + } + if (!withoutMeridian) { + state.add(getMeridianButton(timepicker).text); + } + return state; + } + + //***************** + + it('contains three row & three input elements', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final timepicker = shadowRoot.querySelector('timepicker'); + + expect(ngQuery(timepicker, 'tr').length).toBe(3); + expect(ngQuery(timepicker, 'input').length).toBe(2); + expect(ngQuery(timepicker, 'button').length).toBe(1); + })); + + it('has initially the correct time & meridian', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final timepicker = shadowRoot.querySelector('timepicker'); + + expect(getTimeState(timepicker)).toEqual(['02', '40', 'PM']); + expect(getModelState(scope)).toEqual([14, 40]); + })); + +// it('has `selected` current time when model is initially cleared', compileComponent( +// getHtml(), +// {'time': null}, +// (Scope scope, dom.HtmlElement shadowRoot) { +// final timepicker = shadowRoot.querySelector('timepicker'); +// +// expect(scope.context['time']).toBe(null); +// expect(getTimeState(timepicker)).not.toEqual(['', '', '']); +// })); + + it('changes inputs when model changes value', compileComponent( + getHtml(), + {'time': newTime(11, 50)}, + (Scope scope, dom.HtmlElement shadowRoot) { + final timepicker = shadowRoot.querySelector('timepicker'); + + expect(getTimeState(timepicker)).toEqual(['11', '50', 'AM']); + expect(getModelState(scope)).toEqual([11, 50]); + + scope.context['time'] = newTime(16, 40); + microLeap(); + scope.rootScope.apply(); + + expect(getTimeState(timepicker)).toEqual(['04', '40', 'PM']); + expect(getModelState(scope)).toEqual([16, 40]); + })); + + it('increases / decreases hours when arrows are clicked', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final timepicker = shadowRoot.querySelector('timepicker'); + + var up = getHoursButton(timepicker, true); + var down = getHoursButton(timepicker, false); + + doClick(scope, up); + expect(getTimeState(timepicker)).toEqual(['03', '40', 'PM']); + expect(getModelState(scope)).toEqual([15, 40]); + + doClick(scope, down); + expect(getTimeState(timepicker)).toEqual(['02', '40', 'PM']); + expect(getModelState(scope)).toEqual([14, 40]); + + doClick(scope, down); + expect(getTimeState(timepicker)).toEqual(['01', '40', 'PM']); + expect(getModelState(scope)).toEqual([13, 40]); + })); + + it('increase / decreases minutes by default step when arrows are clicked', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final timepicker = shadowRoot.querySelector('timepicker'); + + var up = getMinutesButton(timepicker, true); + var down = getMinutesButton(timepicker, false); + + doClick(scope, up); + expect(getTimeState(timepicker)).toEqual(['02', '41', 'PM']); + expect(getModelState(scope)).toEqual([14, 41]); + + doClick(scope, down); + expect(getTimeState(timepicker)).toEqual(['02', '40', 'PM']); + expect(getModelState(scope)).toEqual([14, 40]); + + doClick(scope, down); + expect(getTimeState(timepicker)).toEqual(['02', '39', 'PM']); + expect(getModelState(scope)).toEqual([14, 39]); + })); + + it('meridian button has correct type', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final timepicker = shadowRoot.querySelector('timepicker'); + + var button = getMeridianButton(timepicker); + expect(button.attributes['type']).toEqual('button'); + })); + + it('toggles meridian when button is clicked', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final timepicker = shadowRoot.querySelector('timepicker'); + + var button = getMeridianButton(timepicker); + + doClick(scope, button); + expect(getTimeState(timepicker)).toEqual(['02', '40', 'AM']); + expect(getModelState(scope)).toEqual([2, 40]); + + doClick(scope, button); + expect(getTimeState(timepicker)).toEqual(['02', '40', 'PM']); + expect(getModelState(scope)).toEqual([14, 40]); + + doClick(scope, button); + expect(getTimeState(timepicker)).toEqual(['02', '40', 'AM']); + expect(getModelState(scope)).toEqual([2, 40]); + })); + + it('has minutes "connected" to hours', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final timepicker = shadowRoot.querySelector('timepicker'); + + var up = getMinutesButton(timepicker, true); + var down = getMinutesButton(timepicker, false); + + doClick(scope, up, 10); + expect(getTimeState(timepicker)).toEqual(['02', '50', 'PM']); + expect(getModelState(scope)).toEqual([14, 50]); + + doClick(scope, up, 10); + expect(getTimeState(timepicker)).toEqual(['03', '00', 'PM']); + expect(getModelState(scope)).toEqual([15, 0]); + + doClick(scope, up, 10); + scope.rootScope.apply(); + expect(getTimeState(timepicker)).toEqual(['03', '10', 'PM']); + expect(getModelState(scope)).toEqual([15, 10]); + + doClick(scope, down, 10); + scope.rootScope.apply(); + expect(getTimeState(timepicker)).toEqual(['03', '00', 'PM']); + expect(getModelState(scope)).toEqual([15, 0]); + + doClick(scope, down, 10); + scope.rootScope.apply(); + expect(getTimeState(timepicker)).toEqual(['02', '50', 'PM']); + expect(getModelState(scope)).toEqual([14, 50]); + })); + + it('has hours "connected" to meridian', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final timepicker = shadowRoot.querySelector('timepicker'); + + var up = getHoursButton(timepicker, true); + var down = getHoursButton(timepicker, false); + + // AM -> PM + scope.context['time'] = newTime(11, 0); + scope.rootScope.apply(); + expect(getTimeState(timepicker)).toEqual(['11', '00', 'AM']); + expect(getModelState(scope)).toEqual([11, 0]); + + doClick(scope, up); + expect(getTimeState(timepicker)).toEqual(['12', '00', 'PM']); + expect(getModelState(scope)).toEqual([12, 0]); + + doClick(scope, up); + expect(getTimeState(timepicker)).toEqual(['01', '00', 'PM']); + expect(getModelState(scope)).toEqual([13, 0]); + + doClick(scope, down); + expect(getTimeState(timepicker)).toEqual(['12', '00', 'PM']); + expect(getModelState(scope)).toEqual([12, 0]); + + doClick(scope, down); + expect(getTimeState(timepicker)).toEqual(['11', '00', 'AM']); + expect(getModelState(scope)).toEqual([11, 0]); + + // PM -> AM + scope.context['time'] = newTime(23, 0); + scope.rootScope.apply(); + expect(getTimeState(timepicker)).toEqual(['11', '00', 'PM']); + expect(getModelState(scope)).toEqual([23, 0]); + + doClick(scope, up); + expect(getTimeState(timepicker)).toEqual(['12', '00', 'AM']); + expect(getModelState(scope)).toEqual([0, 0]); + + doClick(scope, up); + expect(getTimeState(timepicker)).toEqual(['01', '00', 'AM']); + expect(getModelState(scope)).toEqual([01, 0]); + + doClick(scope, down); + expect(getTimeState(timepicker)).toEqual(['12', '00', 'AM']); + expect(getModelState(scope)).toEqual([0, 0]); + + doClick(scope, down); + expect(getTimeState(timepicker)).toEqual(['11', '00', 'PM']); + expect(getModelState(scope)).toEqual([23, 0]); + })); + + + it('changes only the time part when hours change', compileComponent( + getHtml(), + {'time': newTime(23, 50)}, + (Scope scope, dom.HtmlElement shadowRoot) { + final timepicker = shadowRoot.querySelector('timepicker'); + + var date = scope.context['time'].day; + var up = getHoursButton(timepicker, true); + doClick(scope, up); + + expect(getTimeState(timepicker)).toEqual(['12', '50', 'AM']); + expect(getModelState(scope)).toEqual([0, 50]); + expect(date).toEqual(scope.context['time'].day); + })); + +// it('changes only the time part when minutes change', compileComponent( +// getHtml(extra:'minute-step="15"'), +// {'time': newTime(0, 0)}, +// (Scope scope, dom.HtmlElement shadowRoot) { +// final timepicker = shadowRoot.querySelector('timepicker'); +// +// var date = scope.context['time'].day; +// var up = getMinutesButton(timepicker, true); +// doClick(scope, up, 2); +// expect(getTimeState(timepicker)).toEqual(['12', '30', 'AM']); +// expect(getModelState(scope)).toEqual([0, 30]); +// expect(date).toEqual(scope.context['time'].day); +// +// var down = getMinutesButton(timepicker, false); +// doClick(scope, down, 2); +// expect(getTimeState(timepicker)).toEqual(['12', '00', 'AM']); +// expect(getModelState(scope)).toEqual([0, 0]); +// expect(date).toEqual(scope.context['time'].day); +// +// doClick(scope, down, 2); +// expect(getTimeState(timepicker)).toEqual(['11', '30', 'PM']); +// expect(getModelState(scope)).toEqual([23, 30]); +// expect(date).toEqual(scope.context['time'].day); +// })); + + it('responds properly on "mousewheel" events', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final timepicker = shadowRoot.querySelector('timepicker'); + + var inputs = ngQuery(timepicker, 'input'); + var hoursEl = inputs[0], minutesEl = inputs[1]; + var upMouseWheelEvent = wheelThatMouse(1); + var downMouseWheelEvent = wheelThatMouse(-1); + + expect(getTimeState(timepicker)).toEqual(['02', '40', 'PM']); + expect(getModelState(scope)).toEqual([14, 40]); + + // Hours UP + hoursEl.dispatchEvent( upMouseWheelEvent ); + scope.rootScope.apply(); + expect(getTimeState(timepicker)).toEqual(['03', '40', 'PM']); + expect(getModelState(scope)).toEqual([15, 40]); + // Hours UP + hoursEl.dispatchEvent( upMouseWheelEvent ); + scope.rootScope.apply(); + expect(getTimeState(timepicker)).toEqual(['04', '40', 'PM']); + expect(getModelState(scope)).toEqual([16, 40]); + // Minutes UP + minutesEl.dispatchEvent( upMouseWheelEvent ); + scope.rootScope.apply(); + expect(getTimeState(timepicker)).toEqual(['04', '41', 'PM']); + expect(getModelState(scope)).toEqual([16, 41]); + // Minutes UP + minutesEl.dispatchEvent( upMouseWheelEvent ); + scope.rootScope.apply(); + expect(getTimeState(timepicker)).toEqual(['04', '42', 'PM']); + expect(getModelState(scope)).toEqual([16, 42]); + + // Minutes DOWN + minutesEl.dispatchEvent( downMouseWheelEvent ); + scope.rootScope.apply(); + expect(getTimeState(timepicker)).toEqual(['04', '41', 'PM']); + expect(getModelState(scope)).toEqual([16, 41]); + // Minutes DOWN + minutesEl.dispatchEvent( downMouseWheelEvent ); + scope.rootScope.apply(); + expect(getTimeState(timepicker)).toEqual(['04', '40', 'PM']); + expect(getModelState(scope)).toEqual([16, 40]); + // Hours DOWN + hoursEl.dispatchEvent( downMouseWheelEvent ); + scope.rootScope.apply(); + expect(getTimeState(timepicker)).toEqual(['03', '40', 'PM']); + expect(getModelState(scope)).toEqual([15, 40]); + // Hours DOWN + hoursEl.dispatchEvent( downMouseWheelEvent ); + scope.rootScope.apply(); + expect(getTimeState(timepicker)).toEqual(['02', '40', 'PM']); + expect(getModelState(scope)).toEqual([14, 40]); + })); + + it('responds properly on "wheel" events', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final timepicker = shadowRoot.querySelector('timepicker'); + + var inputs = ngQuery(timepicker, 'input'); + var hoursEl = inputs[0], minutesEl = inputs[1]; + var upMouseWheelEvent = wheelThatOtherMouse(-1); + var downMouseWheelEvent = wheelThatOtherMouse(1); + + expect(getTimeState(timepicker)).toEqual(['02', '40', 'PM']); + expect(getModelState(scope)).toEqual([14, 40]); + + // UP + hoursEl.dispatchEvent( upMouseWheelEvent ); + scope.rootScope.apply(); + expect(getTimeState(timepicker)).toEqual(['03', '40', 'PM']); + expect(getModelState(scope)).toEqual([15, 40]); + + hoursEl.dispatchEvent( upMouseWheelEvent ); + scope.rootScope.apply(); + expect(getTimeState(timepicker)).toEqual(['04', '40', 'PM']); + expect(getModelState(scope)).toEqual([16, 40]); + + minutesEl.dispatchEvent( upMouseWheelEvent ); + scope.rootScope.apply(); + expect(getTimeState(timepicker)).toEqual(['04', '41', 'PM']); + expect(getModelState(scope)).toEqual([16, 41]); + + minutesEl.dispatchEvent( upMouseWheelEvent ); + scope.rootScope.apply(); + expect(getTimeState(timepicker)).toEqual(['04', '42', 'PM']); + expect(getModelState(scope)).toEqual([16, 42]); + + // DOWN + minutesEl.dispatchEvent( downMouseWheelEvent ); + scope.rootScope.apply(); + expect(getTimeState(timepicker)).toEqual(['04', '41', 'PM']); + expect(getModelState(scope)).toEqual([16, 41]); + + minutesEl.dispatchEvent( downMouseWheelEvent ); + scope.rootScope.apply(); + expect(getTimeState(timepicker)).toEqual(['04', '40', 'PM']); + expect(getModelState(scope)).toEqual([16, 40]); + + hoursEl.dispatchEvent( downMouseWheelEvent ); + scope.rootScope.apply(); + expect(getTimeState(timepicker)).toEqual(['03', '40', 'PM']); + expect(getModelState(scope)).toEqual([15, 40]); + + hoursEl.dispatchEvent( downMouseWheelEvent ); + scope.rootScope.apply(); + expect(getTimeState(timepicker)).toEqual(['02', '40', 'PM']); + expect(getModelState(scope)).toEqual([14, 40]); + })); + + describe('attributes', () { + + String getHtml() { + return r''; + }; + + Map getScopeContent() { + return {'time': newTime(14, 0), 'hstep': 2, 'mstep': 30}; + }; + + it('increases / decreases hours by configurable step', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final timepicker = shadowRoot.querySelector('timepicker'); + + var up = getHoursButton(timepicker, true); + var down = getHoursButton(timepicker, false); + + expect(getTimeState(timepicker)).toEqual(['02', '00', 'PM']); + expect(getModelState(scope)).toEqual([14, 0]); + + doClick(scope, up); + expect(getTimeState(timepicker)).toEqual(['04', '00', 'PM']); + expect(getModelState(scope)).toEqual([16, 0]); + + doClick(scope, down); + expect(getTimeState(timepicker)).toEqual(['02', '00', 'PM']); + expect(getModelState(scope)).toEqual([14, 0]); + + doClick(scope, down); + expect(getTimeState(timepicker)).toEqual(['12', '00', 'PM']); + expect(getModelState(scope)).toEqual([12, 0]); + + // Change step + scope.context['hstep'] = 3; + scope.rootScope.apply(); + + doClick(scope, up); + expect(getTimeState(timepicker)).toEqual(['03', '00', 'PM']); + expect(getModelState(scope)).toEqual([15, 0]); + + doClick(scope, down); + expect(getTimeState(timepicker)).toEqual(['12', '00', 'PM']); + expect(getModelState(scope)).toEqual([12, 0]); + })); + + it('increases / decreases minutes by configurable step', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final timepicker = shadowRoot.querySelector('timepicker'); + + var up = getMinutesButton(timepicker, true); + var down = getMinutesButton(timepicker, false); + + doClick(scope, up); + expect(getTimeState(timepicker)).toEqual(['02', '30', 'PM']); + expect(getModelState(scope)).toEqual([14, 30]); + + doClick(scope, up); + expect(getTimeState(timepicker)).toEqual(['03', '00', 'PM']); + expect(getModelState(scope)).toEqual([15, 0]); + + doClick(scope, down); + expect(getTimeState(timepicker)).toEqual(['02', '30', 'PM']); + expect(getModelState(scope)).toEqual([14, 30]); + + doClick(scope, down); + expect(getTimeState(timepicker)).toEqual(['02', '00', 'PM']); + expect(getModelState(scope)).toEqual([14, 0]); + + // Change step + scope.context['mstep'] = 15; + scope.rootScope.apply(); + + doClick(scope, up); + expect(getTimeState(timepicker)).toEqual(['02', '15', 'PM']); + expect(getModelState(scope)).toEqual([14, 15]); + + doClick(scope, down); + expect(getTimeState(timepicker)).toEqual(['02', '00', 'PM']); + expect(getModelState(scope)).toEqual([14, 0]); + + doClick(scope, down); + expect(getTimeState(timepicker)).toEqual(['01', '45', 'PM']); + expect(getModelState(scope)).toEqual([13, 45]); + })); + + it('responds properly on "mousewheel" events with configurable steps', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final timepicker = shadowRoot.querySelector('timepicker'); + + var inputs = ngQuery(timepicker, 'input'); + var hoursEl = inputs[0], minutesEl = inputs[1]; + var upMouseWheelEvent = wheelThatMouse(1); + var downMouseWheelEvent = wheelThatMouse(-1); + + expect(getTimeState(timepicker)).toEqual(['02', '00', 'PM']); + expect(getModelState(scope)).toEqual([14, 0]); + + // UP + hoursEl.dispatchEvent( upMouseWheelEvent ); + scope.rootScope.apply(); + expect(getTimeState(timepicker)).toEqual(['04', '00', 'PM']); + expect(getModelState(scope)).toEqual([16, 0]); + + minutesEl.dispatchEvent( upMouseWheelEvent ); + scope.rootScope.apply(); + expect(getTimeState(timepicker)).toEqual(['04', '30', 'PM']); + expect(getModelState(scope)).toEqual([16, 30]); + + // DOWN + minutesEl.dispatchEvent( downMouseWheelEvent ); + scope.rootScope.apply(); + expect(getTimeState(timepicker)).toEqual(['04', '00', 'PM']); + expect(getModelState(scope)).toEqual([16, 0]); + + hoursEl.dispatchEvent( downMouseWheelEvent ); + scope.rootScope.apply(); + expect(getTimeState(timepicker)).toEqual(['02', '00', 'PM']); + expect(getModelState(scope)).toEqual([14, 0]); + })); + + it('responds properly on "wheel" events with configurable steps', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final timepicker = shadowRoot.querySelector('timepicker'); + + var inputs = ngQuery(timepicker, 'input'); + var hoursEl = inputs[0], minutesEl = inputs[1]; + var upMouseWheelEvent = wheelThatOtherMouse(-1); + var downMouseWheelEvent = wheelThatOtherMouse(1); + + expect(getTimeState(timepicker)).toEqual(['02', '00', 'PM']); + expect(getModelState(scope)).toEqual([14, 0]); + + // UP + hoursEl.dispatchEvent( upMouseWheelEvent ); + scope.rootScope.apply(); + expect(getTimeState(timepicker)).toEqual(['04', '00', 'PM']); + expect(getModelState(scope)).toEqual([16, 0]); + + minutesEl.dispatchEvent( upMouseWheelEvent ); + scope.rootScope.apply(); + expect(getTimeState(timepicker)).toEqual(['04', '30', 'PM']); + expect(getModelState(scope)).toEqual([16, 30]); + + // DOWN + minutesEl.dispatchEvent( downMouseWheelEvent ); + scope.rootScope.apply(); + expect(getTimeState(timepicker)).toEqual(['04', '00', 'PM']); + expect(getModelState(scope)).toEqual([16, 0]); + + hoursEl.dispatchEvent( downMouseWheelEvent ); + scope.rootScope.apply(); + expect(getTimeState(timepicker)).toEqual(['02', '00', 'PM']); + expect(getModelState(scope)).toEqual([14, 0]); + })); + + it('can handle strings as steps', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final timepicker = shadowRoot.querySelector('timepicker'); + + var upHours = getHoursButton(timepicker, true); + var upMinutes = getMinutesButton(timepicker, true); + + expect(getTimeState(timepicker)).toEqual(['02', '00', 'PM']); + expect(getModelState(scope)).toEqual([14, 0]); + + scope.context['hstep'] = '4'; + scope.context['mstep'] = '20'; + scope.rootScope.apply(); + + doClick(scope, upHours); + expect(getTimeState(timepicker)).toEqual(['06', '00', 'PM']); + expect(getModelState(scope)).toEqual([18, 0]); + + doClick(scope, upMinutes); + expect(getTimeState(timepicker)).toEqual(['06', '20', 'PM']); + expect(getModelState(scope)).toEqual([18, 20]); + })); + }); + + describe('12 / 24 hour mode', () { + + String getHtml() { + return r''; + }; + + Map getScopeContent() { + return {'time': newTime(14, 10), 'meridian': false}; + }; + + dom.Element getMeridianTd(dom.Element timepicker) { + dom.Element res = ngQuery(timepicker, 'tr')[1].querySelectorAll('td')[3]; + return res; + } + + it('initially displays correct time when `show-meridian` is false', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final timepicker = shadowRoot.querySelector('timepicker'); + + expect(getTimeState(timepicker, true)).toEqual(['14', '10']); + expect(getModelState(scope)).toEqual([14, 10]); + //expect(getMeridianTd()).toBeHidden(); + expect(getMeridianTd(timepicker).classes.contains('ng-hide')).toBeTruthy(); + })); + + it('toggles correctly between different modes', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final timepicker = shadowRoot.querySelector('timepicker'); + + expect(getTimeState(timepicker, true)).toEqual(['14', '10']); + + scope.context['meridian'] = true; + scope.rootScope.apply(); + expect(getTimeState(timepicker)).toEqual(['02', '10', 'PM']); + expect(getModelState(scope)).toEqual([14, 10]); + //expect(getMeridianTd()).not.toBeHidden(); + expect(getMeridianTd(timepicker).classes.contains('ng-hide')).toBeFalsy(); + + scope.context['meridian'] = false; + scope.rootScope.apply(); + expect(getTimeState(timepicker, true)).toEqual(['14', '10']); + expect(getModelState(scope)).toEqual([14, 10]); + //expect(getMeridianTd()).toBeHidden(); + expect(getMeridianTd(timepicker).classes.contains('ng-hide')).toBeTruthy(); + })); + +// it('handles correctly initially empty model on parent timepicker', async(inject(() { +// dom.Element timepicker = createTimepicker(); +// +// scope.context['time'] = null; +// timepicker = $compile('')($rootScope); +// scope.rootScope.apply(); +// +// expect(scope.context['time']).toBe(null); +// })); + }); + + describe('`meridians` attribute', () { + + String getHtml() { + return r''; + }; + + Map getScopeContent() { + return {'time': newTime(14, 10), 'meridiansArray': ['am', 'pm']}; + }; + + it('displays correctly', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final timepicker = shadowRoot.querySelector('timepicker'); + + expect(getTimeState(timepicker)[2]).toEqual('pm'); + })); + + it('toggles correctly', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final timepicker = shadowRoot.querySelector('timepicker'); + + scope.context['time'] = newTime(2, 40); + scope.rootScope.apply(); + expect(getTimeState(timepicker)[2]).toEqual('am'); + })); + }); + + describe('setting timepickerConfig steps', () { + TimepickerConfig originalConfig; + + beforeEach(inject((TimepickerConfig tpConfig) { + originalConfig = tpConfig; + tpConfig + ..hourStep = 2 + ..minuteStep = 10 + ..showMeridian = false; + })); + + afterEach(inject((TimepickerConfig tpConfig) { + // return it to the original state + tpConfig + ..hourStep = originalConfig.hourStep + ..minuteStep = originalConfig.minuteStep + ..showMeridian = originalConfig.showMeridian; + })); + + String getHtml() { + return r''; + }; + + Map getScopeContent() { + return {'time': newTime(14, 40)}; + }; + + it('does not affect the initial value', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final timepicker = shadowRoot.querySelector('timepicker'); + + expect(getTimeState(timepicker, true)).toEqual(['14', '40']); + expect(getModelState(scope)).toEqual([14, 40]); + })); + + it('increases / decreases hours with configured step', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final timepicker = shadowRoot.querySelector('timepicker'); + + var up = getHoursButton(timepicker, true); + var down = getHoursButton(timepicker, false); + + doClick(scope, up, 2); + expect(getTimeState(timepicker, true)).toEqual(['18', '40']); + expect(getModelState(scope)).toEqual([18, 40]); + + doClick(scope, down, 3); + expect(getTimeState(timepicker, true)).toEqual(['12', '40']); + expect(getModelState(scope)).toEqual([12, 40]); + })); + + it('increases / decreases minutes with configured step', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final timepicker = shadowRoot.querySelector('timepicker'); + + var up = getMinutesButton(timepicker, true); + var down = getMinutesButton(timepicker, false); + + doClick(scope, up); + expect(getTimeState(timepicker, true)).toEqual(['14', '50']); + expect(getModelState(scope)).toEqual([14, 50]); + + doClick(scope, down, 3); + expect(getTimeState(timepicker, true)).toEqual(['14', '20']); + expect(getModelState(scope)).toEqual([14, 20]); + })); + }); + + describe('setting timepickerConfig meridian labels', () { + + TimepickerConfig originalConfig; + + beforeEach(inject((TimepickerConfig tpConfig) { + originalConfig = tpConfig; + tpConfig + ..meridians = ['π.μ.', 'μ.μ.'] + ..showMeridian = true; + })); + + afterEach(inject((TimepickerConfig tpConfig) { + // return it to the original state + tpConfig + ..meridians = originalConfig.meridians + ..showMeridian = originalConfig.showMeridian; + })); + + String getHtml() { + return r''; + }; + + Map getScopeContent() { + return {'time': newTime(14, 40)}; + }; + + it('displays correctly', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final timepicker = shadowRoot.querySelector('timepicker'); + + expect(getTimeState(timepicker)).toEqual(['02', '40', 'μ.μ.']); + expect(getModelState(scope)).toEqual([14, 40]); + })); + + it('toggles correctly', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final timepicker = shadowRoot.querySelector('timepicker'); + + scope.context['time'] = newTime(2, 40); + scope.rootScope.apply(); + + expect(getTimeState(timepicker)).toEqual(['02', '40', 'π.μ.']); + expect(getModelState(scope)).toEqual([2, 40]); + })); + }); + +// describe('user input validation', () { +// var changeInputValueTo; +// +// beforeEach(inject(($sniffer) { +// changeInputValueTo = (inputEl, value) { +// inputEl.val(value); +// inputEl.dispatchEvent($sniffer.hasEvent('input') ? 'input' : 'change'); +// scope.rootScope.apply(); +// }; +// })); +// +// getHoursInputEl() { +// return timepicker.find('input').eq(0); +// } +// +// getMinutesInputEl() { +// return timepicker.find('input').eq(1); +// } +// +// it('has initially the correct time & meridian', async(inject(() { +// dom.Element timepicker = createTimepicker(); +// +// expect(getTimeState(timepicker)).toEqual(['02', '40', 'PM']); +// expect(getModelState()).toEqual([14, 40]); +// })); +// +// it('updates hours & pads on input change & pads on blur', async(inject(() { +// dom.Element timepicker = createTimepicker(); +// +// var el = getHoursInputEl(); +// +// changeInputValueTo(el, 5); +// expect(getTimeState(timepicker)).toEqual(['5', '40', 'PM']); +// expect(getModelState()).toEqual([17, 40]); +// +// el.blur(); +// expect(getTimeState(timepicker)).toEqual(['05', '40', 'PM']); +// expect(getModelState()).toEqual([17, 40]); +// })); +// +// it('updates minutes & pads on input change & pads on blur', async(inject(() { +// dom.Element timepicker = createTimepicker(); +// +// var el = getMinutesInputEl(); +// +// changeInputValueTo(el, 9); +// expect(getTimeState(timepicker)).toEqual(['02', '9', 'PM']); +// expect(getModelState()).toEqual([14, 9]); +// +// el.blur(); +// expect(getTimeState(timepicker)).toEqual(['02', '09', 'PM']); +// expect(getModelState()).toEqual([14, 9]); +// })); +// +// it('clears model when input hours is invalid & alerts the UI', async(inject(() { +// dom.Element timepicker = createTimepicker(); +// +// var el = getHoursInputEl(); +// +// changeInputValueTo(el, 'pizza'); +// expect(scope.context['time']).toBe(null); +// expect(el.parent().hasClass('has-error')).toBe(true); +// expect(timepicker.hasClass('ng-invalid-time')).toBe(true); +// +// changeInputValueTo(el, 8); +// el.blur(); +// scope.rootScope.apply(); +// expect(getTimeState(timepicker)).toEqual(['08', '40', 'PM']); +// expect(getModelState()).toEqual([20, 40]); +// expect(el.parent().hasClass('has-error')).toBe(false); +// expect(timepicker.hasClass('ng-invalid-time')).toBe(false); +// })); +// +// it('clears model when input minutes is invalid & alerts the UI', async(inject(() { +// dom.Element timepicker = createTimepicker(); +// +// var el = getMinutesInputEl(); +// +// changeInputValueTo(el, 'pizza'); +// expect(scope.context['time']).toBe(null); +// expect(el.parent().hasClass('has-error')).toBe(true); +// expect(timepicker.hasClass('ng-invalid-time')).toBe(true); +// +// changeInputValueTo(el, 22); +// expect(getTimeState(timepicker)).toEqual(['02', '22', 'PM']); +// expect(getModelState()).toEqual([14, 22]); +// expect(el.parent().hasClass('has-error')).toBe(false); +// expect(timepicker.hasClass('ng-invalid-time')).toBe(false); +// })); +// +// it('handles 12/24H mode change', async(inject(() { +// scope.context['meridian'] = true; +//// timepicker = $compile('')($rootScope); +// dom.Element timepicker = _.compile('', scope:scope); +// +// microLeap(); +// scope.rootScope.apply(); +// +// var el = getHoursInputEl(); +// +// changeInputValueTo(el, '16'); +// expect(scope.context['time']).toBe(null); +// expect(el.parent().hasClass('has-error')).toBe(true); +// expect(timepicker.hasClass('ng-invalid-time')).toBe(true); +// +// scope.context['meridian'] = false; +// scope.rootScope.apply(); +// expect(getTimeState(timepicker, true)).toEqual(['16', '40']); +// expect(getModelState()).toEqual([16, 40]); +// expect(timepicker.hasClass('ng-invalid-time')).toBe(false); +// })); +// }); + + describe('when model is not a Date', () { + + it('should not be invalid when the model is null', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final timepicker = shadowRoot.querySelector('timepicker'); + + scope.context['time'] = null; + scope.rootScope.apply(); + expect(timepicker.classes.contains('ng-invalid-time')).toBeFalsy(); + })); + + it('should not be invalid when the model is undefined', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final timepicker = shadowRoot.querySelector('timepicker'); + + scope.context['time'] = null; + scope.rootScope.apply(); + expect(timepicker.classes.contains('ng-invalid-time')).toBeFalsy(); + })); + + it('should not be invalid when the model is a valid string date representation', compileComponent( + getHtml(), + getScopeContent(), + (Scope scope, dom.HtmlElement shadowRoot) { + final timepicker = shadowRoot.querySelector('timepicker'); + + //scope.context['time'] = 'September 30, 2010 15:30:00'; // 1969-07-20 20:18:00 + scope.context['time'] = '2010-09-30 15:30:00'; + scope.rootScope.apply(); + expect(timepicker.classes.contains('ng-invalid-time')).toBeFalsy(); + expect(getTimeState(timepicker)).toEqual(['03', '30', 'PM']); + })); + +// it('should be invalid when the model is not a valid string date representation', async(inject(() { +// dom.Element timepicker = createTimepicker(); +// +// scope.context['time'] = 'pizza'; +// scope.rootScope.apply(); +// expect(timepicker.classes.contains('ng-invalid-time')).toBeTruthy(); +// })); + +// it('should return valid when the model becomes valid', async(inject(() { +// dom.Element timepicker = createTimepicker(); +// +// scope.context['time'] = 'pizza'; +// scope.rootScope.apply(); +// //expect(timepicker.hasClass('ng-invalid-time')).toBe(true); +// expect(timepicker.classes.contains('ng-invalid-time')).toBeTruthy(); +// +// scope.context['time'] = new Date(); +// scope.rootScope.apply(); +// //expect(timepicker.hasClass('ng-invalid-time')).toBe(false); +// expect(timepicker.classes.contains('ng-invalid-time')).toBeFalse(); +// })); +// +// it('should return valid when the model is cleared', async(inject(() { +// dom.Element timepicker = createTimepicker(); +// +// scope.context['time'] = 'pizza'; +// scope.rootScope.apply(); +// expect(timepicker.hasClass('ng-invalid-time')).toBe(true); +// expect(timepicker.classes.contains('ng-invalid-time')).toBeTruthy(); +// +// scope.context['time'] = null; +// scope.rootScope.apply(); +// //expect(timepicker.hasClass('ng-invalid-time')).toBe(false); +// expect(timepicker.classes.contains('ng-invalid-time')).toBeFalse(); +// })); + }); + +// describe('use with `ng-required` directive', () { +// it('should be invalid initially', async(inject(() { +// dom.Element element = createTimepicker('ng-required="true"'); +// +// //expect(element.hasClass('ng-invalid')).toBe(true); +// expect(element.classes.contains('ng-invalid')).toBeTruthy(); +// }))); +// +// it('should be valid if model has been specified', async(inject(() { +// dom.Element element = createTimepicker(); +// +// _scope.context['time'] = new Date(); +// _scope.rootScope.apply(); +// //expect(element.hasClass('ng-invalid')).toBe(false); +// expect(element.classes.contains('ng-invalid')).toBeFalsy(); +// }))); +// }); + + }); +} \ No newline at end of file diff --git a/test/unit/tooltip/tooltip_component_test.dart b/test/unit/tooltip/tooltip_component_test.dart new file mode 100644 index 0000000..815aa81 --- /dev/null +++ b/test/unit/tooltip/tooltip_component_test.dart @@ -0,0 +1,330 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. + +part of angular_ui_test; + +testTooltipComponent() { + describe("[TooltipComponent]", () { + TestBed _; + Scope scope; + Timeout timeout; + + beforeEach(setUpInjector); + afterEach(tearDownInjector); + + beforeEach(() { + module((Module _) => _ + ..install(new TooltipModule()) + ); + inject((TestBed tb) { _ = tb; }); + inject((Timeout t) { timeout = t; }); + }); + + String getHtml() { + return 'Selector Text'; + }; + + Tooltip getTooltip(dom.Element elm) { + return ngProbe(elm).directives.firstWhere((e) => e is Tooltip); + } + + it('should not be open initially', compileComponent( + getHtml(), + {}, + (Scope scope, dom.HtmlElement shadowRoot) { + var elm = shadowRoot.querySelector('span'); + var tooltip = getTooltip(elm); + expect(tooltip.tt_isOpen).toBe(false); + + // We can only test *that* the tooltip-popup element wasn't created as the + // implementation is templated and replaced. + expect(shadowRoot.children.length ).toBe(1); + })); + + it('should open on mouseenter', compileComponent( + getHtml(), + {}, + (Scope scope, dom.HtmlElement shadowRoot) { + var elm = shadowRoot.querySelector('span'); + var tooltip = getTooltip(elm); + + _.triggerEvent(elm, 'mouseenter'); + expect(tooltip.tt_isOpen).toBe(true); + + // We can only test *that* the tooltip-popup element was created as the + // implementation is templated and replaced. + expect(shadowRoot.children.length).toBe( 2 ); + })); + + it('should close on mouseleave', compileComponent( + getHtml(), + {}, + (Scope scope, dom.HtmlElement shadowRoot) { + var elm = shadowRoot.querySelector('span'); + var tooltip = getTooltip(elm); + + _.triggerEvent(elm, 'mouseenter'); + _.triggerEvent(elm, 'mouseleave'); + expect(tooltip.tt_isOpen).toBe(false); + })); + + it('should not animate on animation set to false', compileComponent( + getHtml(), + {}, + (Scope scope, dom.HtmlElement shadowRoot) { + var elm = shadowRoot.querySelector('span'); + var tooltip = getTooltip(elm); + + expect(tooltip.tt_animation).toBe(false); + })); + + it('should have default placement of "top"', compileComponent( + getHtml(), + {}, + (Scope scope, dom.HtmlElement shadowRoot) { + var elm = shadowRoot.querySelector('span'); + var tooltip = getTooltip(elm); + + _.triggerEvent(elm, 'mouseenter'); + expect(tooltip.tt_placement).toEqual('top'); + })); + + it('should allow specification of placement', compileComponent( + 'Selector Text', + {}, + (Scope scope, dom.HtmlElement shadowRoot) { + var elm = shadowRoot.querySelector('span'); + var tooltip = getTooltip(elm); + + _.triggerEvent(elm, 'mouseenter'); + expect(tooltip.tt_placement).toEqual('bottom'); + })); + + it('should work inside an ngRepeat', compileComponent( + '
  • {{item.name}}
', + {'items':[{ 'name': 'One', 'tooltip': 'First Tooltip' }]}, + (Scope scope, dom.HtmlElement shadowRoot) { + var elm = shadowRoot.querySelector('ul'); + + dom.SpanElement tt = ngQuery(elm, 'li > span')[0]; + _.triggerEvent(tt, 'mouseenter'); + expect(tt.text).toEqual(scope.context['items'][0]['name']); + })); + +// it('should only have an isolate scope on the popup', async(inject(() { +// var ttScope; +// +// scope.tooltipMsg = 'Tooltip Text'; +// scope.alt = 'Alt Message'; +// +// elmBody = $compile( angular.element( +// '
Selector Text
' +// ) )( scope ); +// +// $compile( elmBody )( scope ); +// scope.$digest(); +// elm = elmBody.find( 'span' ); +// elmScope = elm.scope(); +// +// elm.trigger( 'mouseenter' ); +// expect( elm.attr( 'alt' ) ).toBe( scope.alt ); +// +// ttScope = angular.element( elmBody.children()[1] ).isolateScope(); +// expect( ttScope.placement ).toBe( 'top' ); +// expect( ttScope.content ).toBe( scope.tooltipMsg ); +// +// elm.trigger( 'mouseleave' ); +// +// //Isolate scope contents should be the same after hiding and showing again (issue 1191) +// elm.trigger( 'mouseenter' ); +// +// ttScope = angular.element( elmBody.children()[1] ).isolateScope(); +// expect( ttScope.placement ).toBe( 'top' ); +// expect( ttScope.content ).toBe( scope.tooltipMsg ); +// }))); + + it('should not show tooltips if there is nothing to show', compileComponent( + '
Selector Text
', + {}, + (Scope scope, dom.HtmlElement shadowRoot) { + var elm = shadowRoot.querySelector('span'); + + _.triggerEvent(elm, 'mouseenter'); + + expect(shadowRoot.children.length).toBe(1); + })); + +// it( 'should close the tooltip when its trigger element is destroyed', async(inject(() { +// Scope scope = compileElement(); +// +// _.triggerEvent(elm, 'mouseenter'); +// expect(scope.context['tt_isOpen']).toBe(true); +// +// elm.remove(); +// // Stypid Rootscope has empty destroy method. +// scope.destroy(); +// expect(elmBody.children.length).toBe(0); +// }))); + +// it('isolate scope on the popup should always be child of correct element scope', async(inject(() { +// Scope scope = compileElement(); +// +// var ttScope; +// _.triggerEvent(elm, 'mouseenter'); +// +// ttScope = angular.element( elmBody.children()[1] ).isolateScope(); +// expect( ttScope.$parent ).toBe( elmScope ); +// +// elm.trigger( 'mouseleave' ); +// +// // After leaving and coming back, the scope's parent should be the same +// elm.trigger( 'mouseenter' ); +// +// ttScope = angular.element( elmBody.children()[1] ).isolateScope(); +// expect( ttScope.$parent ).toBe( elmScope ); +// +// elm.trigger( 'mouseleave' ); +// }))); + + describe('with specified enable expression', () { + + it('should not open ', compileComponent( + 'Selector Text', + {}, + (Scope scope, dom.HtmlElement shadowRoot) { + var elm = shadowRoot.querySelector('span'); + var tooltip = getTooltip(elm); + + _.triggerEvent(elm, 'mouseenter'); + expect(tooltip.tt_isOpen).toBeFalsy(); + expect(shadowRoot.children.length).toBe(1); + })); + + it('should open', compileComponent( + 'Selector Text', + {}, + (Scope scope, dom.HtmlElement shadowRoot) { + var elm = shadowRoot.querySelector('span'); + var tooltip = getTooltip(elm); + + _.triggerEvent(elm, 'mouseenter'); + digest(); + expect(tooltip.tt_isOpen).toBeTruthy(); + expect(shadowRoot.children.length).toBe(2); + })); + }); + + describe('with specified popup delay', () { + + it('should open after timeout', compileComponent( + 'Selector Text', + {}, + (Scope scope, dom.HtmlElement shadowRoot) { + var elm = shadowRoot.querySelector('span'); + var tooltip = getTooltip(elm); + + _.triggerEvent(elm, 'mouseenter'); + expect(tooltip.tt_isOpen).toBe(false); + + timeout.flush(); + expect(tooltip.tt_isOpen).toBe(true); + + })); + + it('should not open if mouseleave before timeout', compileComponent( + 'Selector Text', + {}, + (Scope scope, dom.HtmlElement shadowRoot) { + var elm = shadowRoot.querySelector('span'); + var tooltip = getTooltip(elm); + + _.triggerEvent(elm, 'mouseenter'); + expect(tooltip.tt_isOpen).toBe(false); + + _.triggerEvent(elm, 'mouseleave'); + timeout.flush(); + expect(tooltip.tt_isOpen).toBe(false); + })); + + it('should use default popup delay if specified delay is not a number', compileComponent( + 'Selector Text', + {}, + (Scope scope, dom.HtmlElement shadowRoot) { + var elm = shadowRoot.querySelector('span'); + var tooltip = getTooltip(elm); + + _.triggerEvent(elm, 'mouseenter'); + expect(tooltip.tt_isOpen).toBe(true); + })); + }); + + describe( 'with a trigger attribute', () { + it( 'should use it to show but set the hide trigger based on the map for mapped triggers', compileComponent( + '', + {}, + (Scope scope, dom.HtmlElement shadowRoot) { + var elm = shadowRoot.querySelector('input'); + var tooltip = getTooltip(elm); + + expect(tooltip.tt_isOpen).toBeFalsy(); + + _.triggerEvent(elm, 'focus', 'FocusEvent'); + expect(tooltip.tt_isOpen).toBeTruthy(); + + _.triggerEvent(elm, 'blur', 'FocusEvent'); + expect(tooltip.tt_isOpen).toBeFalsy(); + })); + + it( 'should use it as both the show and hide triggers for unmapped triggers', compileComponent( + '', + {}, + (Scope scope, dom.HtmlElement shadowRoot) { + var elm = shadowRoot.querySelector('input'); + var tooltip = getTooltip(elm); + + expect(tooltip.tt_isOpen).toBeFalsy(); + _.triggerEvent(elm, 'fakeTriggerAttr', 'Event'); + expect(tooltip.tt_isOpen).toBeTruthy(); + _.triggerEvent(elm, 'fakeTriggerAttr', 'Event'); + expect(tooltip.tt_isOpen).toBeFalsy(); + })); + + it('should not share triggers among different element instances', compileComponent( + '', + {}, + (Scope scope, dom.HtmlElement shadowRoot) { + var elms = shadowRoot.querySelectorAll('input'); + expect(elms.length).toEqual(2); + + var tooltip0 = getTooltip(elms[0]); + var tooltip1 = getTooltip(elms[1]); + + _.triggerEvent(elms[1], 'click'); + expect(tooltip0.tt_isOpen).toBeFalsy(); + expect(tooltip1.tt_isOpen).toBeTruthy(); + })); + }); + + describe( 'with an append-to-body attribute', () { + it( 'should append to the body', compileComponent( + 'Selector Text', + {}, + (Scope scope, dom.HtmlElement shadowRoot) { + var elm = shadowRoot.firstChild; + var tooltip = getTooltip(elm); + + var bodyLength = dom.document.body.children.length; + _.triggerEvent(elm, 'mouseenter'); + + expect(tooltip.tt_isOpen).toBe(true); + expect(shadowRoot.children.length ).toBe(1); + expect(dom.document.body.children.length).toEqual(bodyLength + 1); + })); + }); + +// describe('cleanup', () { +// +// }); + }); +} diff --git a/test/unit/typeahead/typeahead_highlight_tests.dart b/test/unit/typeahead/typeahead_highlight_tests.dart new file mode 100644 index 0000000..33bf34e --- /dev/null +++ b/test/unit/typeahead/typeahead_highlight_tests.dart @@ -0,0 +1,55 @@ +part of angular_ui_test; + +void typeaheadHighlightFilterTests() { + + describe("[TypeaheadHighlightFilterComponent]", () { + + beforeEach(setUpInjector); + afterEach(tearDownInjector); + + TypeaheadHighlightFilter highlightFilter; + + beforeEach(module((Module module){ + module.install(new TypeaheadModule()); + + return (Injector injector) { + highlightFilter = injector.get(TypeaheadHighlightFilter); + }; + })); + + it('should higlight a match', async(inject((){ + expect(highlightFilter('before match after', 'match')).toEqual('before match after'); + }))); + + it('should higlight a match with mixed case', async(inject((){ + expect(highlightFilter('before MaTch after', 'match')).toEqual('before MaTch after'); + }))); + + it('should higlight all matches', async(inject((){ + expect(highlightFilter('before MaTch after match', 'match')).toEqual('before MaTch after match'); + }))); + + it('should do nothing if no match', async(inject((){ + expect(highlightFilter('before match after', 'nomatch')).toEqual('before match after'); + }))); + + it('should do nothing if no or empty query', async(inject((){ + expect(highlightFilter('before match after', '')).toEqual('before match after'); + expect(highlightFilter('before match after', null)).toEqual('before match after'); + }))); + + it('should work correctly for regexp reserved words', async(inject((){ + expect(highlightFilter('before (match after', '(match')).toEqual('before (match after'); + }))); + + it('should work correctly with numeric values', async(inject((){ + expect(highlightFilter('123', '2')).toEqual('123'); + }))); + + it('should highlight match results based on individual words', async(inject((){ + expect(highlightFilter('John Doe 123', 'John 123')).toEqual('John Doe 123'); + expect(highlightFilter('John - Doe - Jane - Doe', 'John Jane')).toEqual('John - Doe - Jane - Doe'); + }))); + + }); +} \ No newline at end of file diff --git a/test/unit/typeahead/typeahead_parser_tests.dart b/test/unit/typeahead/typeahead_parser_tests.dart new file mode 100644 index 0000000..91139e7 --- /dev/null +++ b/test/unit/typeahead/typeahead_parser_tests.dart @@ -0,0 +1,89 @@ +part of angular_ui_test; + +void typeaheadParserTests() { + + describe("[TypeaheadParserComponent]", () { + + beforeEach(setUpInjector); + afterEach(tearDownInjector); + + TypeaheadParser typeaheadParser; + Scope rootScope; + FormatterMap formatters; + + beforeEach(() { + module((Module _) => _ + ..install(new TypeaheadModule()) + ); + inject((TypeaheadParser t) => typeaheadParser = t); + inject((Scope s) => rootScope = s); + inject((FormatterMap f) => formatters = f); + }); + + dynamic evaluateExpression(expression, locals) { + return expression.eval(new ContextLocals(rootScope.context, locals), formatters); + } + + it('should parse the simplest array-based syntax', async(inject((){ + rootScope.context['states'] = ['Alabama', 'California', 'Delaware']; + var result = typeaheadParser.parse(r'state for state in states | filter:$viewValue'); + var itemName = result.itemName; + var locals = {r'$viewValue':'al'}; + expect(evaluateExpression(result.source, locals)).toEqual(['Alabama', 'California']); + + locals[itemName] = 'Alabama'; + expect(evaluateExpression(result.viewMapper, locals)).toEqual('Alabama'); + expect(evaluateExpression(result.modelMapper, locals)).toEqual('Alabama'); + }))); + + it('should parse the simplest function-based syntax', async(inject((Filter filter){ + rootScope.context['getStates'] = (viewValue) => filter(['Alabama', 'California', 'Delaware'], viewValue); + var result = typeaheadParser.parse(r'state for state in getStates($viewValue)'); + var itemName = result.itemName; + var locals = {r'$viewValue':'al'}; + expect(evaluateExpression(result.source, locals)).toEqual(['Alabama', 'California']); + + locals[itemName] = 'Alabama'; + expect(evaluateExpression(result.viewMapper, locals)).toEqual('Alabama'); + expect(evaluateExpression(result.modelMapper, locals)).toEqual('Alabama'); + }))); + + it('should allow to specify custom model mapping that is used as a label as well', async(inject((){ + rootScope.context['states'] = [{'code' : 'AL', 'name' : 'Alabama'}, {'code' : 'CA', 'name':'California'}, {'code' : 'DE', 'name':'Delaware'}]; + var result = typeaheadParser.parse(r'state.name for state in states | filter:$viewValue | orderBy:"name":true'); + var itemName = result.itemName; + expect(itemName).toEqual('state'); + var locals = {r'$viewValue':'al'}; + expect(evaluateExpression(result.source, locals)).toEqual([{'code' : 'CA', 'name':'California'}, {'code' : 'AL', 'name' : 'Alabama'}]); + + locals[itemName] = {'code':'AL', 'name':'Alabama'}; + expect(evaluateExpression(result.viewMapper, locals)).toEqual('Alabama'); + expect(evaluateExpression(result.modelMapper, locals)).toEqual('Alabama'); + }))); + + it('should allow to specify custom view and model mappers', async(inject((){ + rootScope.context['states'] = [{'code' : 'AL', 'name' : 'Alabama'}, {'code' : 'CA', 'name':'California'}, {'code' : 'DE', 'name':'Delaware'}]; + var result = typeaheadParser.parse(r'state.code as state.name + " ("+state.code+")" for state in states | filter:$viewValue | orderBy:"name":true'); + var itemName = result.itemName; + var locals = {r'$viewValue':'al'}; + expect(evaluateExpression(result.source, locals)).toEqual([{'code' : 'CA', 'name':'California'}, {'code' : 'AL', 'name' : 'Alabama'}]); + + locals[itemName] = {'code':'AL', 'name':'Alabama'}; + expect(evaluateExpression(result.viewMapper, locals)).toEqual('Alabama (AL)'); + expect(evaluateExpression(result.modelMapper, locals)).toEqual('AL'); + }))); + + it('should parse the multiline array-based syntax', async(inject((){ + rootScope.context['states'] = ['Alabama', 'California', 'Delaware']; + var result = typeaheadParser.parse(r'''state for state in states + | filter:$viewValue'''); + var itemName = result.itemName; + var locals = {r'$viewValue':'al'}; + expect(evaluateExpression(result.source, locals)).toEqual(['Alabama', 'California']); + + locals[itemName] = 'Alabama'; + expect(evaluateExpression(result.viewMapper, locals)).toEqual('Alabama'); + expect(evaluateExpression(result.modelMapper, locals)).toEqual('Alabama'); + }))); + }); +} \ No newline at end of file diff --git a/test/unit/typeahead/typeahead_popup_tests.dart b/test/unit/typeahead/typeahead_popup_tests.dart new file mode 100644 index 0000000..b458c6c --- /dev/null +++ b/test/unit/typeahead/typeahead_popup_tests.dart @@ -0,0 +1,128 @@ +part of angular_ui_test; + +void typeaheadPopupTests() { + + describe("[TypeaheadPopupComponent]", () { + + MockHttpBackend mockHttp; + TestBed _; + + beforeEach(setUpInjector); + afterEach(tearDownInjector); + + beforeEach(() { + module((Module _) => _ + ..install(new TypeaheadModule()) + ); + inject((MockHttpBackend http, TestBed testBed) { + mockHttp = http; + _ = testBed; + mockHttp.whenGET('packages/angular_ui/typeahead/typeahead-match.html').respond(''); + }); + //return loadTemplates(['/typeahead/typeahead-match.html', '/typeahead/typeahead-popup.html']); + }); + + getHtml() { + return ''; + } + + it('should render initial results', compileComponent( + getHtml(), + { + 'matches': [new Item('foo'), new Item('bar'), new Item('baz') ], + 'active': 1 + }, + (Scope scope, dom.HtmlElement shadowRoot) { + mockHttp.flush(); + microLeap(); + digest(); + + var liElems = shadowRoot.querySelectorAll("li"); + + expect(liElems.length).toBe(3); + expect(liElems.elementAt(0)).not.toHaveClass('active'); + expect(liElems.elementAt(1)).toHaveClass('active'); + expect(liElems.elementAt(2)).not.toHaveClass('active'); + })); + + it('should change active item on mouseenter', compileComponent( + getHtml(), + { + 'matches': [new Item('foo'), new Item('bar'), new Item('baz') ], + 'active': 1 + }, + (Scope scope, dom.HtmlElement shadowRoot) { + mockHttp.flush(); + microLeap(); + digest(); + + var liElems = shadowRoot.querySelectorAll("li"); + + expect(liElems.elementAt(1)).toHaveClass('active'); + expect(liElems.elementAt(2)).not.toHaveClass('active'); + + _.triggerEvent(liElems.elementAt(2), "mouseenter"); + + expect(liElems.elementAt(1)).not.toHaveClass('active'); + expect(liElems.elementAt(2)).toHaveClass('active'); + })); + + it('should select an item on mouse click', compileComponent( + getHtml(), + { + 'matches': [new Item('foo'), new Item('bar'), new Item('baz') ], + 'active': 1, + 'select': guinness.createSpy('select') + }, + (Scope scope, dom.HtmlElement shadowRoot) { + mockHttp.flush(); + microLeap(); + digest(); + + var liElems = shadowRoot.querySelectorAll("li"); + + liElems.elementAt(2).click(); + expect(scope.context['select']).toHaveBeenCalledWith(2); + })); + + it('should remove list if no matches', compileComponent( + getHtml(), + { + 'matches': [], + 'active': 1, + 'select': guinness.createSpy('select') + }, + (Scope scope, dom.HtmlElement shadowRoot) { + microLeap(); + digest(); + + var typeaheadPopup = shadowRoot.querySelector('typeahead-popup'); + expect(typeaheadPopup).toBeDefined(); + expect(typeaheadPopup.children.length).toBe(0); + })); + + it('should remove list if null matche list', compileComponent( + getHtml(), + { + 'matches': null, + 'active': 1, + 'select': guinness.createSpy('select') + }, + (Scope scope, dom.HtmlElement shadowRoot) { + microLeap(); + digest(); + + var typeaheadPopup = shadowRoot.querySelector('typeahead-popup'); + expect(typeaheadPopup).toBeDefined(); + expect(typeaheadPopup.children.length).toBe(0); + })); + }); +} + +class Item { + var id, label, model; + Item(this.id, [this.label = null]) { + if (label == null) label = id; + model = this; + } +} \ No newline at end of file diff --git a/test/unit/typeahead/typeahead_tests.dart b/test/unit/typeahead/typeahead_tests.dart new file mode 100644 index 0000000..ddb29ec --- /dev/null +++ b/test/unit/typeahead/typeahead_tests.dart @@ -0,0 +1,171 @@ +part of angular_ui_test; + +void typeaheadComponentTests() { + + beforeEach(setUpInjector); + afterEach(tearDownInjector); + + TestBed _; + MockHttpBackend mockHttp; + + beforeEach(() { + module((Module _) => _ + ..install(new TypeaheadModule()) + ); + inject((MockHttpBackend http, TestBed testBed) { + mockHttp = http; + _ = testBed; + mockHttp.whenGET('packages/angular_ui/typeahead/typeahead-match.html').respond(''); + mockHttp.whenGET('packages/angular_ui/typeahead/typeahead-popup.html').respond(r''' +'''); + }); + //return loadTemplates(['/typeahead/typeahead-match.html', '/typeahead/typeahead-popup.html']); + }); + + getHtml() { + return ''; + } + + dom.InputElement getInput(parentDiv) { + return parentDiv.querySelector('input'); + } + + dom.Element getDropDown(parentDiv) { + var typeaheadPopupContainer = parentDiv.querySelector('typeahead-popup'); + return typeaheadPopupContainer.shadowRoot.querySelector('ul.dropdown-menu'); + } + + changeInputValueTo(parentDiv, value) { + dom.InputElement input = getInput(parentDiv); + input.value = value; + input.dispatchEvent(new dom.Event('input')); + clockTick(); + } + + dom.Element findDropDown(dom.Element el) { + List els = ngQuery(el, 'ul.dropdown-menu'); + return els.length > 0 ? els[0] : null; + } + + List findMatches (dom.Element element) { + dom.Element el = findDropDown(element); + return el == null ? [] : ngQuery(el, 'li'); + } + + bool toBeClosed(dom.Element actual) { + dom.Element typeaheadEl = findDropDown(actual); + var message = 'Expected "$actual" to be closed.'; + return typeaheadEl != null && typeaheadEl.style.display == 'none' && findMatches(actual).length == 0; + } + + bool toBeOpenWithActive(dom.Element actual, int noOfMatches, int activeIdx) { + dom.Element typeaheadEl = findDropDown(actual); + List liEls = findMatches(actual); + + var message = 'Expected "$actual" to be opened.'; + return typeaheadEl != null && typeaheadEl.style.display == 'block' && liEls.length == noOfMatches && liEls[activeIdx].classes.contains('active'); + } + + describe('initial state and model changes', () { + +// it('should be closed by default', compileComponent( +// getHtml(), +// { +// 'source': ['foo', 'bar', 'baz'], +// 'states': [{'code': 'AL', 'name': 'Alaska'}, {'code': 'CL', 'name': 'California'}], +// 'result': {} +// }, +// (Scope scope, dom.HtmlElement shadowRoot) { +// mockHttp.flush(); +// microLeap(); +// digest(); +// +// expect(getDropDown(shadowRoot)).toBeNull(); +// })); + + it('should be closed by default', compileComponent( + getHtml(), + { + 'source': ['foo', 'bar', 'baz'], + 'states': [{'code': 'AL', 'name': 'Alaska'}, {'code': 'CL', 'name': 'California'}] + }, + (Scope scope, dom.HtmlElement shadowRoot) { + microLeap(); + digest(); + + expect(toBeClosed(shadowRoot)).toBeFalsy(); + })); + + it('should correctly render initial state if the "as" keyword is used', compileComponent( + '', + { + 'source': ['foo', 'bar', 'baz'], + 'states': [{'code': 'AL', 'name': 'Alaska'}, {'code': 'CL', 'name': 'California'}] + }, + (Scope scope, dom.HtmlElement shadowRoot) { + scope.context['result'] = scope.context['states'][0]; + microLeap(); + digest(); + + expect(getInput(shadowRoot).value).toEqual('Alaska'); + })); + + it('should default to bound model for initial rendering if there is not enough info to render label', compileComponent( + '', + { + 'source': ['foo', 'bar', 'baz'], + 'states': [{'code': 'AL', 'name': 'Alaska'}, {'code': 'CL', 'name': 'California'}] + }, + (Scope scope, dom.HtmlElement shadowRoot) { + scope.context['result'] = scope.context['states'][0]['code']; + microLeap(); + digest(); + + expect(getInput(shadowRoot).value).toEqual('AL'); + })); + + it('should not get open on model change', compileComponent( + '', + { + 'source': ['foo', 'bar', 'baz'], + 'states': [{'code': 'AL', 'name': 'Alaska'}, {'code': 'CL', 'name': 'California'}] + }, + (Scope scope, dom.HtmlElement shadowRoot) { + scope.apply(() => scope.context['result'] = 'foo'); + microLeap(); + digest(); + + expect(toBeClosed(shadowRoot)).toBeFalsy(); + })); + }); + + describe('basic functionality', () { + + it('should open and close typeahead based on matches', compileComponent( + r'', + { + 'source': ['foo', 'bar', 'baz'], + 'states': [{'code': 'AL', 'name': 'Alaska'}, {'code': 'CL', 'name': 'California'}] + }, + (Scope scope, dom.HtmlElement shadowRoot) { + scope.apply(() => scope.context['result'] = 'foo'); + microLeap(); + digest(); + + expect(getInput(shadowRoot).attributes['aria-expanded']).toEqual('false'); + expect(getInput(shadowRoot).attributes['aria-activedescendant']).toBeNull(); + + changeInputValueTo(shadowRoot, 'ba'); +// expect(toBeOpenWithActive(shadowRoot, 2, 0)).toBeTruthy(); +// +// changeInputValueTo(shadowRoot, ''); +// +// //expect(getDropDown()).toBeNotNull(); +// expect(toBeClosed(shadowRoot)).toBeFalsy(); + })); + }); +} \ No newline at end of file diff --git a/test/unit/utils/content_append_test.dart b/test/unit/utils/content_append_test.dart new file mode 100644 index 0000000..7267154 --- /dev/null +++ b/test/unit/utils/content_append_test.dart @@ -0,0 +1,49 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. + +part of angular_ui_test; + +testContentAppendComponent() { + describe("[ContentAppendComponent]", () { + TestBed _; + Scope scope; + + beforeEach(setUpInjector); + afterEach(tearDownInjector); + + beforeEach(() { + module((Module _) => _ + ..install(new ContentAppendModule()) + ); + }); + + String getHtml() { + return ''; + }; + + it('It should ignore null values', compileComponent( + getHtml(), + {'node': null}, + (Scope scope, dom.HtmlElement shadowRoot) { + expect(shadowRoot.children[0].childNodes.length).toBe(0); + })); + + it('It should append a String', compileComponent( + getHtml(), + {'node': 'Hello'}, + (Scope scope, dom.HtmlElement shadowRoot) { + scope.context['node'] = 'Hello'; + expect(shadowRoot.children[0].childNodes.length).toBe(1); + expect(shadowRoot.children[0].text).toEqual('Hello'); + })); + + it('It should append an Element', compileComponent( + getHtml(), + {'node': new dom.Element.html("
HelloFromInner
")}, + (Scope scope, dom.HtmlElement shadowRoot) { + expect(shadowRoot.children[0].children.length).toBe(1); + expect(shadowRoot.querySelector('#inner').text).toEqual('HelloFromInner'); + })); + }); +} diff --git a/test/unit/utils/position_test.dart b/test/unit/utils/position_test.dart new file mode 100644 index 0000000..5e4680f --- /dev/null +++ b/test/unit/utils/position_test.dart @@ -0,0 +1,25 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. + +part of angular_ui_test; + +testPosition() { + describe("[Position]", () { + TestBed _; + Scope scope; + + beforeEach(setUpInjector); + afterEach(tearDownInjector); + + beforeEach(() { + module((Module _) => _ + ..install(new PositionModule()) + ); + }); + + it('position must not be null', inject((Position position) { + expect(position).toBeNotNull(); + })); + }); +} diff --git a/test/tests/timeout_tests.dart b/test/unit/utils/timeout_test.dart similarity index 53% rename from test/tests/timeout_tests.dart rename to test/unit/utils/timeout_test.dart index 3c38ef4..0ddde01 100644 --- a/test/tests/timeout_tests.dart +++ b/test/unit/utils/timeout_test.dart @@ -1,29 +1,23 @@ -// Copyright (c) 2013 - 2014, akserg (Sergey Akopkokhyants) +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. // https://github.com/akserg/angular.dart.ui // All rights reserved. Please see the LICENSE.md file. -part of angular.ui.test; +part of angular_ui_test; -void timeoutTests() { - - - describe('Testing timeout:', () { +testTimeout() { + describe("[Timeout]", () { TestBed _; - Scope scope; - Timeout timeout; - + beforeEach(setUpInjector); - beforeEach(module((Module module) { - module.install(new TimeoutModule()); - })); - beforeEach(inject((TestBed tb) => _ = tb)); - beforeEach(inject((Scope s) => scope = s)); - beforeEach(inject((Timeout t) => timeout = t)); - - afterEach(tearDownInjector); - - it('should delegate functions to timeout', () { + + beforeEach(() { + module((Module _) => _ + ..install(new TimeoutModule()) + ); + }); + + it('should delegate functions to timeout', inject((Timeout timeout) { var counter = 0; timeout(() { counter++; @@ -33,58 +27,50 @@ void timeoutTests() { timeout.flush(); expect(counter).toBe(1); - }); + })); }); - describe('Testing timeout exceptiong handler:', () { + describe("[Timeout Exception Handler]", () { TestBed _; - Scope scope; - Timeout timeout; - TestExceptionHandler exceptionHandler; - + beforeEach(setUpInjector); - beforeEach(module((Module module) { - module.install(new TimeoutModule()); - module.type(ExceptionHandler, implementedBy:TestExceptionHandler); - })); - beforeEach(inject((TestBed tb) => _ = tb)); - beforeEach(inject((Scope s) => scope = s)); - beforeEach(inject((Timeout t) => timeout = t)); - beforeEach(inject((ExceptionHandler e) => exceptionHandler = e)); - - afterEach(tearDownInjector); - - it('should delegate exception to the exceptionHandler service', () { + + beforeEach(() { + module((Module _) => _ + ..install(new TimeoutModule()) + ..bind(ExceptionHandler, toImplementation:TestExceptionHandler) + ); + }); + + it('should delegate exception to the exceptionHandler service', inject((Timeout timeout, ExceptionHandler handler) { timeout(() { throw new Exception("Test Error"); }); + expect(handler).toBeAnInstanceOf(TestExceptionHandler); + TestExceptionHandler exceptionHandler = handler as TestExceptionHandler; expect(exceptionHandler.errors).toEqual([]); timeout.flush(); expect(exceptionHandler.errors.length).toEqual(1); - }); + })); }); - describe('Testing timeout cancel:', () { + describe("[Timeout cancel]", () { TestBed _; - Scope scope; - Timeout timeout; - + beforeEach(setUpInjector); - beforeEach(module((Module module) { - module.install(new TimeoutModule()); - })); - beforeEach(inject((TestBed tb) => _ = tb)); - beforeEach(inject((Scope s) => scope = s)); - beforeEach(inject((Timeout t) => timeout = t)); - - afterEach(tearDownInjector); - - it('should cancel tasks', () { - var task1 = jasmine.createSpy('task1'), - task2 = jasmine.createSpy('task2'), - task3 = jasmine.createSpy('task3'), + + beforeEach(() { + module((Module _) => _ + ..install(new TimeoutModule()) + ); + }); + + it('should cancel tasks', inject((Timeout timeout) { + var task1 = guinness.createSpy('task1'), + task2 = guinness.createSpy('task2'), + task3 = guinness.createSpy('task3'), promise1, promise3; promise1 = timeout(task1); @@ -98,11 +84,11 @@ void timeoutTests() { expect(task1).not.toHaveBeenCalled(); expect(task2).toHaveBeenCalledOnce(); expect(task3).not.toHaveBeenCalled(); - }); + })); - it('should return true if a task was successfully canceled', () { - var task1 = jasmine.createSpy('task1'), - task2 = jasmine.createSpy('task2'), + it('should return true if a task was successfully canceled', inject((Timeout timeout) { + var task1 = guinness.createSpy('task1'), + task2 = guinness.createSpy('task2'), promise1, promise2; promise1 = timeout(task1); @@ -111,15 +97,15 @@ void timeoutTests() { expect(timeout.cancel(promise1)).toBe(false); expect(timeout.cancel(promise2)).toBe(true); - }); + })); - it('should not throw a runtime exception when given an undefined promise', () { + it('should not throw a runtime exception when given an undefined promise', inject((Timeout timeout) { expect(timeout.cancel()).toBe(false); - }); + })); }); } -@NgInjectableService() +@Injectable() class TestExceptionHandler implements ExceptionHandler { var errors = []; @@ -134,3 +120,4 @@ class TestExceptionHandler implements ExceptionHandler { errors.add(error); } } + diff --git a/test/tests/transition_tests.dart b/test/unit/utils/transition_test.dart similarity index 57% rename from test/tests/transition_tests.dart rename to test/unit/utils/transition_test.dart index cd3ab3c..1282e35 100644 --- a/test/tests/transition_tests.dart +++ b/test/unit/utils/transition_test.dart @@ -1,49 +1,47 @@ -// Copyright (c) 2013 - 2014, akserg (Sergey Akopkokhyants) +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. // https://github.com/akserg/angular.dart.ui // All rights reserved. Please see the LICENSE.md file. -part of angular.ui.test; - -void transitionTests() { - - describe('transition', () { +part of angular_ui_test; +testTransition() { + describe("[Transition]", () { TestBed _; - Transition transition; - + Scope scope; + beforeEach(setUpInjector); - beforeEach(module((Module module) { - module.install(new TransitionModule()); - })); - beforeEach(inject((TestBed tb) => _ = tb)); - beforeEach(inject((Transition t) => transition = t)); - afterEach(tearDownInjector); - - it('returns our custom promise', () { + + beforeEach(() { + module((Module _) => _ + ..install(new TransitionModule()) + ); + }); + + it('returns our custom promise', inject((Transition transition) { var element = new dom.DivElement(); Future promise = transition(element, '').future; expect(promise.then).toBeNotNull(); expect(promise.catchError).toBeNotNull(); - }); - - it('changes the css if passed a string', () { + })); + + it('changes the css if passed a string', inject((Transition transition) { var element = new dom.DivElement(); transition(element, 'triggerClass').future.then((value) { expect(element.classes).toContain('triggerClass'); }); - }); - - it('changes the style if passed an object', () { + })); + + it('changes the style if passed an object', inject((Transition transition) { var element = new dom.DivElement(); var triggerStyle = { 'height': '11px' }; transition(element, triggerStyle).future.then((value) { expect(element.style.getPropertyValue('height')).toEqual('11px'); }); - }); - - it('calls the if passed', () { + })); + + it('calls the function if passed', inject((Transition transition) { var element = new dom.DivElement(); transition(element, (dom.Element e){ @@ -51,12 +49,6 @@ void transitionTests() { }).future.then((value) { expect(element.classes).toContain("test"); }); - }); - - describe('transitionEndEventName', () { - it('should be undefined', () { - expect(transition.transitionEndEventName).toBeNull(); - }); - }); + })); }); -} \ No newline at end of file +} diff --git a/upgrade.sh b/upgrade.sh new file mode 100755 index 0000000..ceb93ab --- /dev/null +++ b/upgrade.sh @@ -0,0 +1,3 @@ +#!/bin/bash +pub upgrade +run pub serve --verbose \ No newline at end of file diff --git a/web/accordion/accordion_demo.dart b/web/accordion/accordion_demo.dart new file mode 100644 index 0000000..d4bbcf2 --- /dev/null +++ b/web/accordion/accordion_demo.dart @@ -0,0 +1,28 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +part of angular.ui.demo; + +@Component( + selector: 'accordion-demo', + templateUrl: 'accordion/accordion_demo.html', + exportExpressions: const ["oneAtATime", "groups", "items", "isOpen"], + useShadowDom: false) +class AccordionDemo implements ScopeAware { + + Scope scope; + + bool oneAtATime = true; + bool isOpen = false; + List groups = [ + { + 'title': "Dynamic Group Header - 1", + 'content': "Dynamic Group Body - 1" + }, + { + 'title': "Dynamic Group Header - 2", + 'content': "Dynamic Group Body - 2" + } + ]; + List items = ['Item 1', 'Item 2', 'Item 3']; +} \ No newline at end of file diff --git a/web/accordion/accordion_demo.html b/web/accordion/accordion_demo.html new file mode 100644 index 0000000..94cf0ef --- /dev/null +++ b/web/accordion/accordion_demo.html @@ -0,0 +1,30 @@ + + +
+ + + + This content is straight in the template. + + + {{group.content}} + + +

The body of the accordion group grows to fit the contents

+
{{item}}
+
+ + + I can have markup, too! + + This is just some content to illustrate fancy headings. + +
+
\ No newline at end of file diff --git a/web/alert/alert_demo.dart b/web/alert/alert_demo.dart new file mode 100644 index 0000000..6236a88 --- /dev/null +++ b/web/alert/alert_demo.dart @@ -0,0 +1,37 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +part of angular.ui.demo; + +/** + * Alert component. + */ +@Component( + selector: 'alert-demo', + templateUrl: "alert/alert_demo.html", + useShadowDom: false) +class AlertDemo implements ScopeAware { + + Scope scope; + + List alerts = [ + new AlertItem(type:'danger', msg:'Oh snap! Change a few things up and try submitting again.'), + new AlertItem(type:'success', msg:'Well done! You successfully read this important alert message.') + ]; + + void addAlert() { + alerts.add(new AlertItem(msg:"Another alert!")); + } + + void closeAlert(index) { + alerts.removeAt(index); + } +} + +@Injectable() +class AlertItem { + var type; + var msg; + + AlertItem({String this.type:null, String this.msg:''}); +} \ No newline at end of file diff --git a/web/alert/alert_demo.html b/web/alert/alert_demo.html new file mode 100644 index 0000000..f67832a --- /dev/null +++ b/web/alert/alert_demo.html @@ -0,0 +1,12 @@ + + +
+ Immortal info + {{alert.msg}} + +
+ \ No newline at end of file diff --git a/web/buttons/buttons_demo.dart b/web/buttons/buttons_demo.dart new file mode 100644 index 0000000..e50af96 --- /dev/null +++ b/web/buttons/buttons_demo.dart @@ -0,0 +1,25 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +part of angular.ui.demo; + +/** + * Buttons demo component. + */ +@Component(selector: 'buttons-demo', + templateUrl: 'buttons/buttons_demo.html', + useShadowDom: false) +class ButtonsDemo implements ScopeAware { + + Scope scope; + + var singleModel = 1; + + var radioModel = 'Right'; + + var leftModel = false; + + var middleModel = true; + + var rightModel = false; +} \ No newline at end of file diff --git a/web/buttons/buttons_demo.html b/web/buttons/buttons_demo.html new file mode 100644 index 0000000..1143512 --- /dev/null +++ b/web/buttons/buttons_demo.html @@ -0,0 +1,29 @@ + + +
+

Single toggle

+
{{singleModel}}
+ + +

Checkbox

+
{{leftModel}} - {{middleModel}} - {{rightModel}}
+
+ + + +
+ +

Radio

+
{{radioModel}}
+
+ + + +
+
\ No newline at end of file diff --git a/web/carousel/carousel_demo.dart b/web/carousel/carousel_demo.dart new file mode 100644 index 0000000..f75a71e --- /dev/null +++ b/web/carousel/carousel_demo.dart @@ -0,0 +1,43 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +part of angular.ui.demo; + +@Component( + selector: 'carousel-demo', + templateUrl: 'carousel/carousel_demo.html', + useShadowDom: false) +class CarouselDemo implements ScopeAware { + + Scope scope; + + int _myInterval = 2; + int get myInterval { + return _myInterval; + } + set myInterval(value) { + try { + _myInterval = toInt(value); + } on Error catch(ex) { + _myInterval = 0; + } + } + + List> slides = []; + + CarouselDemo() { + + for (int i = 0; i < 4; i++) { + addSlide(); + } + } + + void addSlide() { + int newWidth = 600 + slides.length; + slides.add({ + 'image': 'http://placekitten.com/g/${newWidth}/300', + 'text': ['More','Extra','Lots of','Surplus'][slides.length % 4] + ' ' + + ['Cats', 'Kittys', 'Felines', 'Cutes'][slides.length % 4] + }); + } +} diff --git a/web/carousel/carousel_demo.html b/web/carousel/carousel_demo.html new file mode 100644 index 0000000..76a910a --- /dev/null +++ b/web/carousel/carousel_demo.html @@ -0,0 +1,29 @@ + + +
+
+ + + + + + +
+
+
+ +
+
+ Interval, in seconds: + + Enter a negative number to stop the interval. +
+
+
\ No newline at end of file diff --git a/web/collapse/collapse_demo.dart b/web/collapse/collapse_demo.dart new file mode 100644 index 0000000..aae7472 --- /dev/null +++ b/web/collapse/collapse_demo.dart @@ -0,0 +1,18 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +part of angular.ui.demo; + +/** + * Collapse controller. + */ +@Component( + selector: 'collapse-demo', + templateUrl: 'collapse/collapse_demo.html', + useShadowDom: false) +class CollapseDemo implements ScopeAware { + + Scope scope; + + var isCollapsed = true; +} diff --git a/web/collapse/collapse_demo.html b/web/collapse/collapse_demo.html new file mode 100644 index 0000000..1ff6d67 --- /dev/null +++ b/web/collapse/collapse_demo.html @@ -0,0 +1,13 @@ + + +
+ + +
+
Some content
+
+
\ No newline at end of file diff --git a/web/css/angular.css b/web/css/angular.css deleted file mode 100644 index b88e61e..0000000 --- a/web/css/angular.css +++ /dev/null @@ -1,11 +0,0 @@ -@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; -} diff --git a/web/css/bootstrap-theme.css b/web/css/bootstrap-theme.css index c69e377..c4cadf1 100644 --- a/web/css/bootstrap-theme.css +++ b/web/css/bootstrap-theme.css @@ -1,10 +1,9 @@ /*! - * Bootstrap v3.0.3 (http://getbootstrap.com) - * Copyright 2013 Twitter, Inc. - * Licensed under http://www.apache.org/licenses/LICENSE-2.0 + * Bootstrap v3.3.1 (http://getbootstrap.com) + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) */ - .btn-default, .btn-primary, .btn-success, @@ -30,6 +29,14 @@ -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); } +.btn-default .badge, +.btn-primary .badge, +.btn-success .badge, +.btn-info .badge, +.btn-warning .badge, +.btn-danger .badge { + text-shadow: none; +} .btn:active, .btn.active { background-image: none; @@ -37,6 +44,8 @@ .btn-default { text-shadow: 0 1px 0 #fff; background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%); + background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0)); background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0); filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); @@ -54,26 +63,40 @@ background-color: #e0e0e0; border-color: #dbdbdb; } +.btn-default:disabled, +.btn-default[disabled] { + background-color: #e0e0e0; + background-image: none; +} .btn-primary { - background-image: -webkit-linear-gradient(top, #428bca 0%, #2d6ca2 100%); - background-image: linear-gradient(to bottom, #428bca 0%, #2d6ca2 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0); + background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%); + background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88)); + background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0); filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); background-repeat: repeat-x; - border-color: #2b669a; + border-color: #245580; } .btn-primary:hover, .btn-primary:focus { - background-color: #2d6ca2; + background-color: #265a88; background-position: 0 -15px; } .btn-primary:active, .btn-primary.active { - background-color: #2d6ca2; - border-color: #2b669a; + background-color: #265a88; + border-color: #245580; +} +.btn-primary:disabled, +.btn-primary[disabled] { + background-color: #265a88; + background-image: none; } .btn-success { background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%); + background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641)); background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0); filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); @@ -90,8 +113,40 @@ background-color: #419641; border-color: #3e8f3e; } +.btn-success:disabled, +.btn-success[disabled] { + background-color: #419641; + background-image: none; +} +.btn-info { + background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); + background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2)); + background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + background-repeat: repeat-x; + border-color: #28a4c9; +} +.btn-info:hover, +.btn-info:focus { + background-color: #2aabd2; + background-position: 0 -15px; +} +.btn-info:active, +.btn-info.active { + background-color: #2aabd2; + border-color: #28a4c9; +} +.btn-info:disabled, +.btn-info[disabled] { + background-color: #2aabd2; + background-image: none; +} .btn-warning { background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); + background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316)); background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0); filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); @@ -108,8 +163,15 @@ background-color: #eb9316; border-color: #e38d13; } +.btn-warning:disabled, +.btn-warning[disabled] { + background-color: #eb9316; + background-image: none; +} .btn-danger { background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%); + background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a)); background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0); filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); @@ -126,23 +188,10 @@ background-color: #c12e2a; border-color: #b92c28; } -.btn-info { - background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); - background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - background-repeat: repeat-x; - border-color: #28a4c9; -} -.btn-info:hover, -.btn-info:focus { - background-color: #2aabd2; - background-position: 0 -15px; -} -.btn-info:active, -.btn-info.active { - background-color: #2aabd2; - border-color: #28a4c9; +.btn-danger:disabled, +.btn-danger[disabled] { + background-color: #c12e2a; + background-image: none; } .thumbnail, .img-thumbnail { @@ -151,36 +200,45 @@ } .dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus { - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); background-color: #e8e8e8; background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); + background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); background-repeat: repeat-x; } .dropdown-menu > .active > a, .dropdown-menu > .active > a:hover, .dropdown-menu > .active > a:focus { - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0); - background-color: #357ebd; - background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%); - background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%); + background-color: #2e6da4; + background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); + background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); + background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); background-repeat: repeat-x; } .navbar-default { background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%); + background-image: -o-linear-gradient(top, #fff 0%, #f8f8f8 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8)); background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%); - background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + background-repeat: repeat-x; border-radius: 4px; -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); } +.navbar-default .navbar-nav > .open > a, .navbar-default .navbar-nav > .active > a { - background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f3f3f3 100%); - background-image: linear-gradient(to bottom, #ebebeb 0%, #f3f3f3 100%); + background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); + background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2)); + background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0); background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0); -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); } @@ -190,16 +248,21 @@ } .navbar-inverse { background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%); + background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222)); background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%); - background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0); filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + background-repeat: repeat-x; } +.navbar-inverse .navbar-nav > .open > a, .navbar-inverse .navbar-nav > .active > a { - background-image: -webkit-linear-gradient(top, #222 0%, #282828 100%); - background-image: linear-gradient(to bottom, #222 0%, #282828 100%); + background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%); + background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f)); + background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0); background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0); -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); } @@ -212,6 +275,19 @@ .navbar-fixed-bottom { border-radius: 0; } +@media (max-width: 767px) { + .navbar .navbar-nav .open .dropdown-menu > .active > a, + .navbar .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #fff; + background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); + background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); + background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); + background-repeat: repeat-x; + } +} .alert { text-shadow: 0 1px 0 rgba(255, 255, 255, .2); -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); @@ -219,67 +295,92 @@ } .alert-success { background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); + background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc)); background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%); - background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); + background-repeat: repeat-x; border-color: #b2dba1; } .alert-info { background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%); + background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0)); background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%); - background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); + background-repeat: repeat-x; border-color: #9acfea; } .alert-warning { background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); + background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0)); background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); - background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); + background-repeat: repeat-x; border-color: #f5e79e; } .alert-danger { background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); + background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3)); background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); - background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); + background-repeat: repeat-x; border-color: #dca7a7; } .progress { background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); + background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5)); background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); - background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); + background-repeat: repeat-x; } .progress-bar { - background-image: -webkit-linear-gradient(top, #428bca 0%, #3071a9 100%); - background-image: linear-gradient(to bottom, #428bca 0%, #3071a9 100%); + background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%); + background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090)); + background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0); background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0); } .progress-bar-success { background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%); + background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44)); background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); - background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); + background-repeat: repeat-x; } .progress-bar-info { background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); + background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5)); background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); - background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); + background-repeat: repeat-x; } .progress-bar-warning { background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); + background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f)); background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); - background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); + background-repeat: repeat-x; } .progress-bar-danger { background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%); + background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c)); background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); - background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); + background-repeat: repeat-x; +} +.progress-bar-striped { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); } .list-group { border-radius: 4px; @@ -289,12 +390,19 @@ .list-group-item.active, .list-group-item.active:hover, .list-group-item.active:focus { - text-shadow: 0 -1px 0 #3071a9; - background-image: -webkit-linear-gradient(top, #428bca 0%, #3278b3 100%); - background-image: linear-gradient(to bottom, #428bca 0%, #3278b3 100%); + text-shadow: 0 -1px 0 #286090; + background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%); + background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a)); + background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0); background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0); - border-color: #3278b3; + border-color: #2b669a; +} +.list-group-item.active .badge, +.list-group-item.active:hover .badge, +.list-group-item.active:focus .badge { + text-shadow: none; } .panel { -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05); @@ -302,46 +410,61 @@ } .panel-default > .panel-heading { background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); + background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); - background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); + background-repeat: repeat-x; } .panel-primary > .panel-heading { - background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%); - background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%); + background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); + background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); + background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0); } .panel-success > .panel-heading { background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); + background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6)); background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%); - background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0); + background-repeat: repeat-x; } .panel-info > .panel-heading { background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); + background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3)); background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%); - background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); + background-repeat: repeat-x; } .panel-warning > .panel-heading { background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); + background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc)); background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); - background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); + background-repeat: repeat-x; } .panel-danger > .panel-heading { background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%); + background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc)); background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); - background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); + background-repeat: repeat-x; } .well { background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); + background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5)); background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%); - background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); + background-repeat: repeat-x; border-color: #dcdcdc; -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); } +/*# sourceMappingURL=bootstrap-theme.css.map */ diff --git a/web/css/bootstrap-theme.min.css b/web/css/bootstrap-theme.min.css new file mode 100644 index 0000000..4c3e7ba --- /dev/null +++ b/web/css/bootstrap-theme.min.css @@ -0,0 +1,5 @@ +/*! + * Bootstrap v3.3.1 (http://getbootstrap.com) + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */.btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-default .badge,.btn-primary .badge,.btn-success .badge,.btn-info .badge,.btn-warning .badge,.btn-danger .badge{text-shadow:none}.btn:active,.btn.active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:hover,.btn-default:focus{background-color:#e0e0e0;background-position:0 -15px}.btn-default:active,.btn-default.active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default:disabled,.btn-default[disabled]{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-o-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#265a88));background-image:linear-gradient(to bottom,#337ab7 0,#265a88 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#245580}.btn-primary:hover,.btn-primary:focus{background-color:#265a88;background-position:0 -15px}.btn-primary:active,.btn-primary.active{background-color:#265a88;border-color:#245580}.btn-primary:disabled,.btn-primary[disabled]{background-color:#265a88;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:hover,.btn-success:focus{background-color:#419641;background-position:0 -15px}.btn-success:active,.btn-success.active{background-color:#419641;border-color:#3e8f3e}.btn-success:disabled,.btn-success[disabled]{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:hover,.btn-info:focus{background-color:#2aabd2;background-position:0 -15px}.btn-info:active,.btn-info.active{background-color:#2aabd2;border-color:#28a4c9}.btn-info:disabled,.btn-info[disabled]{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:hover,.btn-warning:focus{background-color:#eb9316;background-position:0 -15px}.btn-warning:active,.btn-warning.active{background-color:#eb9316;border-color:#e38d13}.btn-warning:disabled,.btn-warning[disabled]{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:hover,.btn-danger:focus{background-color:#c12e2a;background-position:0 -15px}.btn-danger:active,.btn-danger.active{background-color:#c12e2a;border-color:#b92c28}.btn-danger:disabled,.btn-danger[disabled]{background-color:#c12e2a;background-image:none}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-color:#2e6da4;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-o-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dbdbdb),to(#e2e2e2));background-image:linear-gradient(to bottom,#dbdbdb 0,#e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-o-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#080808),to(#0f0f0f));background-image:linear-gradient(to bottom,#080808 0,#0f0f0f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}@media (max-width:767px){.navbar .navbar-nav .open .dropdown-menu>.active>a,.navbar .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-o-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#286090));background-image:linear-gradient(to bottom,#337ab7 0,#286090 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #286090;background-image:-webkit-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2b669a));background-image:linear-gradient(to bottom,#337ab7 0,#2b669a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);background-repeat:repeat-x;border-color:#2b669a}.list-group-item.active .badge,.list-group-item.active:hover .badge,.list-group-item.active:focus .badge{text-shadow:none}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} \ No newline at end of file diff --git a/web/css/bootstrap.css b/web/css/bootstrap.css index fbe07a9..1943a90 100644 --- a/web/css/bootstrap.css +++ b/web/css/bootstrap.css @@ -1,11 +1,18 @@ /*! - * Bootstrap v3.0.3 (http://getbootstrap.com) - * Copyright 2013 Twitter, Inc. - * Licensed under http://www.apache.org/licenses/LICENSE-2.0 + * Bootstrap v3.3.1 (http://getbootstrap.com) + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) */ - -/*! normalize.css v2.1.3 | MIT License | git.io/normalize */ +/*! normalize.css v3.0.2 | MIT License | git.io/normalize */ +html { + font-family: sans-serif; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} +body { + margin: 0; +} article, aside, details, @@ -15,6 +22,7 @@ footer, header, hgroup, main, +menu, nav, section, summary { @@ -22,8 +30,10 @@ summary { } audio, canvas, +progress, video { display: inline-block; + vertical-align: baseline; } audio:not([controls]) { display: none; @@ -33,28 +43,13 @@ audio:not([controls]) { template { display: none; } -html { - font-family: sans-serif; - -ms-text-size-adjust: 100%; - -webkit-text-size-adjust: 100%; -} -body { - margin: 0; -} a { - background: transparent; -} -a:focus { - outline: thin dotted; + background-color: transparent; } a:active, a:hover { outline: 0; } -h1 { - margin: .67em 0; - font-size: 2em; -} abbr[title] { border-bottom: 1px dotted; } @@ -65,28 +60,14 @@ strong { dfn { font-style: italic; } -hr { - height: 0; - -moz-box-sizing: content-box; - box-sizing: content-box; +h1 { + margin: .67em 0; + font-size: 2em; } mark { color: #000; background: #ff0; } -code, -kbd, -pre, -samp { - font-family: monospace, serif; - font-size: 1em; -} -pre { - white-space: pre-wrap; -} -q { - quotes: "\201C" "\201D" "\2018" "\2019"; -} small { font-size: 80%; } @@ -110,28 +91,35 @@ svg:not(:root) { overflow: hidden; } figure { - margin: 0; + margin: 1em 40px; } -fieldset { - padding: .35em .625em .75em; - margin: 0 2px; - border: 1px solid #c0c0c0; +hr { + height: 0; + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; } -legend { - padding: 0; - border: 0; +pre { + overflow: auto; +} +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; } button, input, +optgroup, select, textarea { margin: 0; - font-family: inherit; - font-size: 100%; + font: inherit; + color: inherit; } -button, -input { - line-height: normal; +button { + overflow: visible; } button, select { @@ -148,40 +136,68 @@ button[disabled], html input[disabled] { cursor: default; } +button::-moz-focus-inner, +input::-moz-focus-inner { + padding: 0; + border: 0; +} +input { + line-height: normal; +} input[type="checkbox"], input[type="radio"] { - box-sizing: border-box; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; padding: 0; } +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} input[type="search"] { - -webkit-appearance: textfield; -webkit-box-sizing: content-box; -moz-box-sizing: content-box; box-sizing: content-box; + -webkit-appearance: textfield; } input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; } -button::-moz-focus-inner, -input::-moz-focus-inner { +fieldset { + padding: .35em .625em .75em; + margin: 0 2px; + border: 1px solid #c0c0c0; +} +legend { padding: 0; border: 0; } textarea { overflow: auto; - vertical-align: top; +} +optgroup { + font-weight: bold; } table { border-spacing: 0; border-collapse: collapse; } +td, +th { + padding: 0; +} +/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */ @media print { - * { + *, + *:before, + *:after { color: #000 !important; text-shadow: none !important; background: transparent !important; - box-shadow: none !important; + -webkit-box-shadow: none !important; + box-shadow: none !important; } a, a:visited { @@ -193,13 +209,14 @@ table { abbr[title]:after { content: " (" attr(title) ")"; } - a[href^="javascript:"]:after, - a[href^="#"]:after { + a[href^="#"]:after, + a[href^="javascript:"]:after { content: ""; } pre, blockquote { border: 1px solid #999; + page-break-inside: avoid; } thead { @@ -212,9 +229,6 @@ table { img { max-width: 100% !important; } - @page { - margin: 2cm .5cm; - } p, h2, h3 { @@ -231,10 +245,6 @@ table { .navbar { display: none; } - .table td, - .table th { - background-color: #fff !important; - } .btn > .caret, .dropup > .btn > .caret { border-top-color: #000 !important; @@ -245,2645 +255,3127 @@ table { .table { border-collapse: collapse !important; } + .table td, + .table th { + background-color: #fff !important; + } .table-bordered th, .table-bordered td { border: 1px solid #ddd !important; } } -*, -*:before, -*:after { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; +@font-face { + font-family: 'Glyphicons Halflings'; + + src: url('../fonts/glyphicons-halflings-regular.eot'); + src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); } -html { - font-size: 62.5%; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +.glyphicon { + position: relative; + top: 1px; + display: inline-block; + font-family: 'Glyphicons Halflings'; + font-style: normal; + font-weight: normal; + line-height: 1; + + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } -body { - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 14px; - line-height: 1.428571429; - color: #333; - background-color: #fff; +.glyphicon-asterisk:before { + content: "\2a"; } -input, -button, -select, -textarea { - font-family: inherit; - font-size: inherit; - line-height: inherit; +.glyphicon-plus:before { + content: "\2b"; } -a { - color: #428bca; - text-decoration: none; +.glyphicon-euro:before, +.glyphicon-eur:before { + content: "\20ac"; } -a:hover, -a:focus { - color: #2a6496; - text-decoration: underline; +.glyphicon-minus:before { + content: "\2212"; } -a:focus { - outline: thin dotted; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; +.glyphicon-cloud:before { + content: "\2601"; } -img { - vertical-align: middle; +.glyphicon-envelope:before { + content: "\2709"; } -.img-responsive { - display: block; - max-width: 100%; - height: auto; +.glyphicon-pencil:before { + content: "\270f"; } -.img-rounded { - border-radius: 6px; +.glyphicon-glass:before { + content: "\e001"; } -.img-thumbnail { - display: inline-block; - max-width: 100%; - height: auto; - padding: 4px; - line-height: 1.428571429; - background-color: #fff; - border: 1px solid #ddd; - border-radius: 4px; - -webkit-transition: all .2s ease-in-out; - transition: all .2s ease-in-out; +.glyphicon-music:before { + content: "\e002"; } -.img-circle { - border-radius: 50%; +.glyphicon-search:before { + content: "\e003"; } -hr { - margin-top: 20px; - margin-bottom: 20px; - border: 0; - border-top: 1px solid #eee; +.glyphicon-heart:before { + content: "\e005"; } -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - border: 0; +.glyphicon-star:before { + content: "\e006"; } -h1, -h2, -h3, -h4, -h5, -h6, -.h1, -.h2, -.h3, -.h4, -.h5, -.h6 { - font-family: inherit; - font-weight: 500; - line-height: 1.1; - color: inherit; +.glyphicon-star-empty:before { + content: "\e007"; } -h1 small, -h2 small, -h3 small, -h4 small, -h5 small, -h6 small, -.h1 small, -.h2 small, -.h3 small, -.h4 small, -.h5 small, -.h6 small, -h1 .small, -h2 .small, -h3 .small, -h4 .small, -h5 .small, -h6 .small, -.h1 .small, -.h2 .small, -.h3 .small, -.h4 .small, -.h5 .small, -.h6 .small { - font-weight: normal; - line-height: 1; - color: #999; +.glyphicon-user:before { + content: "\e008"; } -h1, -h2, -h3 { - margin-top: 20px; - margin-bottom: 10px; +.glyphicon-film:before { + content: "\e009"; } -h1 small, -h2 small, -h3 small, -h1 .small, -h2 .small, -h3 .small { - font-size: 65%; +.glyphicon-th-large:before { + content: "\e010"; } -h4, -h5, -h6 { - margin-top: 10px; - margin-bottom: 10px; +.glyphicon-th:before { + content: "\e011"; } -h4 small, -h5 small, -h6 small, -h4 .small, -h5 .small, -h6 .small { - font-size: 75%; +.glyphicon-th-list:before { + content: "\e012"; } -h1, -.h1 { - font-size: 36px; +.glyphicon-ok:before { + content: "\e013"; } -h2, -.h2 { - font-size: 30px; +.glyphicon-remove:before { + content: "\e014"; } -h3, -.h3 { - font-size: 24px; +.glyphicon-zoom-in:before { + content: "\e015"; } -h4, -.h4 { - font-size: 18px; +.glyphicon-zoom-out:before { + content: "\e016"; } -h5, -.h5 { - font-size: 14px; +.glyphicon-off:before { + content: "\e017"; } -h6, -.h6 { - font-size: 12px; +.glyphicon-signal:before { + content: "\e018"; } -p { - margin: 0 0 10px; +.glyphicon-cog:before { + content: "\e019"; } -.lead { - margin-bottom: 20px; - font-size: 16px; - font-weight: 200; - line-height: 1.4; +.glyphicon-trash:before { + content: "\e020"; } -@media (min-width: 768px) { - .lead { - font-size: 21px; - } +.glyphicon-home:before { + content: "\e021"; } -small, -.small { - font-size: 85%; +.glyphicon-file:before { + content: "\e022"; } -cite { - font-style: normal; +.glyphicon-time:before { + content: "\e023"; } -.text-left { - text-align: left; +.glyphicon-road:before { + content: "\e024"; } -.text-right { - text-align: right; +.glyphicon-download-alt:before { + content: "\e025"; } -.text-center { - text-align: center; +.glyphicon-download:before { + content: "\e026"; } -.text-justify { - text-align: justify; +.glyphicon-upload:before { + content: "\e027"; } -.text-muted { - color: #999; +.glyphicon-inbox:before { + content: "\e028"; } -.text-primary { - color: #428bca; +.glyphicon-play-circle:before { + content: "\e029"; } -.text-primary:hover { - color: #3071a9; +.glyphicon-repeat:before { + content: "\e030"; } -.text-warning { - color: #8a6d3b; +.glyphicon-refresh:before { + content: "\e031"; } -.text-warning:hover { - color: #66512c; +.glyphicon-list-alt:before { + content: "\e032"; } -.text-danger { - color: #a94442; +.glyphicon-lock:before { + content: "\e033"; } -.text-danger:hover { - color: #843534; +.glyphicon-flag:before { + content: "\e034"; } -.text-success { - color: #3c763d; +.glyphicon-headphones:before { + content: "\e035"; } -.text-success:hover { - color: #2b542c; +.glyphicon-volume-off:before { + content: "\e036"; } -.text-info { - color: #31708f; +.glyphicon-volume-down:before { + content: "\e037"; } -.text-info:hover { - color: #245269; +.glyphicon-volume-up:before { + content: "\e038"; } -.bg-primary { - color: #fff; - background-color: #428bca; +.glyphicon-qrcode:before { + content: "\e039"; } -a.bg-primary:hover { - background-color: #3071a9; +.glyphicon-barcode:before { + content: "\e040"; } -.bg-warning { - background-color: #fcf8e3; +.glyphicon-tag:before { + content: "\e041"; } -a.bg-warning:hover { - background-color: #f7ecb5; +.glyphicon-tags:before { + content: "\e042"; } -.bg-danger { - background-color: #f2dede; +.glyphicon-book:before { + content: "\e043"; } -a.bg-danger:hover { - background-color: #e4b9b9; +.glyphicon-bookmark:before { + content: "\e044"; } -.bg-success { - background-color: #dff0d8; +.glyphicon-print:before { + content: "\e045"; } -a.bg-success:hover { - background-color: #c1e2b3; +.glyphicon-camera:before { + content: "\e046"; } -.bg-info { - background-color: #d9edf7; +.glyphicon-font:before { + content: "\e047"; } -a.bg-info:hover { - background-color: #afd9ee; +.glyphicon-bold:before { + content: "\e048"; } -.page-header { - padding-bottom: 9px; - margin: 40px 0 20px; - border-bottom: 1px solid #eee; +.glyphicon-italic:before { + content: "\e049"; } -ul, -ol { - margin-top: 0; - margin-bottom: 10px; +.glyphicon-text-height:before { + content: "\e050"; } -ul ul, -ol ul, -ul ol, -ol ol { - margin-bottom: 0; +.glyphicon-text-width:before { + content: "\e051"; } -.list-unstyled { - padding-left: 0; - list-style: none; +.glyphicon-align-left:before { + content: "\e052"; } -.list-inline { - padding-left: 0; - list-style: none; +.glyphicon-align-center:before { + content: "\e053"; } -.list-inline > li { - display: inline-block; - padding-right: 5px; - padding-left: 5px; +.glyphicon-align-right:before { + content: "\e054"; } -.list-inline > li:first-child { - padding-left: 0; +.glyphicon-align-justify:before { + content: "\e055"; } -dl { - margin-top: 0; - margin-bottom: 20px; +.glyphicon-list:before { + content: "\e056"; } -dt, -dd { - line-height: 1.428571429; +.glyphicon-indent-left:before { + content: "\e057"; } -dt { - font-weight: bold; +.glyphicon-indent-right:before { + content: "\e058"; } -dd { - margin-left: 0; +.glyphicon-facetime-video:before { + content: "\e059"; } -@media (min-width: 768px) { - .dl-horizontal dt { - float: left; - width: 160px; - overflow: hidden; - clear: left; - text-align: right; - text-overflow: ellipsis; - white-space: nowrap; - } - .dl-horizontal dd { - margin-left: 180px; - } +.glyphicon-picture:before { + content: "\e060"; } -abbr[title], -abbr[data-original-title] { - cursor: help; - border-bottom: 1px dotted #999; +.glyphicon-map-marker:before { + content: "\e062"; } -.initialism { - font-size: 90%; - text-transform: uppercase; +.glyphicon-adjust:before { + content: "\e063"; } -blockquote { - padding: 10px 20px; - margin: 0 0 20px; - border-left: 5px solid #eee; +.glyphicon-tint:before { + content: "\e064"; } -blockquote p { - font-size: 17.5px; +.glyphicon-edit:before { + content: "\e065"; } -blockquote p:last-child { - margin-bottom: 0; +.glyphicon-share:before { + content: "\e066"; } -blockquote small, -blockquote .small { - display: block; - line-height: 1.428571429; - color: #999; +.glyphicon-check:before { + content: "\e067"; } -blockquote small:before, -blockquote .small:before { - content: '\2014 \00A0'; +.glyphicon-move:before { + content: "\e068"; } -blockquote.pull-right { - padding-right: 15px; - padding-left: 0; - border-right: 5px solid #eee; - border-left: 0; +.glyphicon-step-backward:before { + content: "\e069"; } -blockquote.pull-right p, -blockquote.pull-right small, -blockquote.pull-right .small { - text-align: right; +.glyphicon-fast-backward:before { + content: "\e070"; } -blockquote.pull-right small:before, -blockquote.pull-right .small:before { - content: ''; +.glyphicon-backward:before { + content: "\e071"; } -blockquote.pull-right small:after, -blockquote.pull-right .small:after { - content: '\00A0 \2014'; +.glyphicon-play:before { + content: "\e072"; } -blockquote:before, -blockquote:after { - content: ""; +.glyphicon-pause:before { + content: "\e073"; } -address { - margin-bottom: 20px; - font-style: normal; - line-height: 1.428571429; +.glyphicon-stop:before { + content: "\e074"; } -code, -kbd, -pre, -samp { - font-family: Menlo, Monaco, Consolas, "Courier New", monospace; +.glyphicon-forward:before { + content: "\e075"; } -code { - padding: 2px 4px; - font-size: 90%; - color: #c7254e; - white-space: nowrap; - background-color: #f9f2f4; - border-radius: 4px; +.glyphicon-fast-forward:before { + content: "\e076"; } -kbd { - padding: 2px 4px; - font-size: 90%; - color: #fff; - background-color: #333; - border-radius: 3px; - box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25); +.glyphicon-step-forward:before { + content: "\e077"; } -pre { - display: block; - padding: 9.5px; - margin: 0 0 10px; - font-size: 13px; - line-height: 1.428571429; - color: #333; - word-break: break-all; - word-wrap: break-word; - background-color: #f5f5f5; - border: 1px solid #ccc; - border-radius: 4px; +.glyphicon-eject:before { + content: "\e078"; } -pre code { - padding: 0; - font-size: inherit; - color: inherit; - white-space: pre-wrap; - background-color: transparent; - border-radius: 0; +.glyphicon-chevron-left:before { + content: "\e079"; } -.pre-scrollable { - max-height: 340px; - overflow-y: scroll; +.glyphicon-chevron-right:before { + content: "\e080"; } -.container { - padding-right: 15px; - padding-left: 15px; - margin-right: auto; - margin-left: auto; +.glyphicon-plus-sign:before { + content: "\e081"; } -@media (min-width: 768px) { - .container { - width: 750px; - } +.glyphicon-minus-sign:before { + content: "\e082"; } -@media (min-width: 992px) { - .container { - width: 970px; - } +.glyphicon-remove-sign:before { + content: "\e083"; } -@media (min-width: 1200px) { - .container { - width: 1170px; - } +.glyphicon-ok-sign:before { + content: "\e084"; } -.container-fluid { - padding-right: 15px; - padding-left: 15px; - margin-right: auto; - margin-left: auto; +.glyphicon-question-sign:before { + content: "\e085"; } -.row { - margin-right: -15px; - margin-left: -15px; +.glyphicon-info-sign:before { + content: "\e086"; } -.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 { - position: relative; - min-height: 1px; - padding-right: 15px; - padding-left: 15px; +.glyphicon-screenshot:before { + content: "\e087"; } -.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 { - float: left; +.glyphicon-remove-circle:before { + content: "\e088"; } -.col-xs-12 { - width: 100%; +.glyphicon-ok-circle:before { + content: "\e089"; } -.col-xs-11 { - width: 91.66666666666666%; +.glyphicon-ban-circle:before { + content: "\e090"; } -.col-xs-10 { - width: 83.33333333333334%; +.glyphicon-arrow-left:before { + content: "\e091"; } -.col-xs-9 { - width: 75%; +.glyphicon-arrow-right:before { + content: "\e092"; } -.col-xs-8 { - width: 66.66666666666666%; +.glyphicon-arrow-up:before { + content: "\e093"; } -.col-xs-7 { - width: 58.333333333333336%; +.glyphicon-arrow-down:before { + content: "\e094"; } -.col-xs-6 { - width: 50%; +.glyphicon-share-alt:before { + content: "\e095"; } -.col-xs-5 { - width: 41.66666666666667%; +.glyphicon-resize-full:before { + content: "\e096"; } -.col-xs-4 { - width: 33.33333333333333%; +.glyphicon-resize-small:before { + content: "\e097"; } -.col-xs-3 { - width: 25%; +.glyphicon-exclamation-sign:before { + content: "\e101"; } -.col-xs-2 { - width: 16.666666666666664%; +.glyphicon-gift:before { + content: "\e102"; } -.col-xs-1 { - width: 8.333333333333332%; +.glyphicon-leaf:before { + content: "\e103"; } -.col-xs-pull-12 { - right: 100%; +.glyphicon-fire:before { + content: "\e104"; } -.col-xs-pull-11 { - right: 91.66666666666666%; +.glyphicon-eye-open:before { + content: "\e105"; } -.col-xs-pull-10 { - right: 83.33333333333334%; +.glyphicon-eye-close:before { + content: "\e106"; } -.col-xs-pull-9 { - right: 75%; +.glyphicon-warning-sign:before { + content: "\e107"; } -.col-xs-pull-8 { - right: 66.66666666666666%; +.glyphicon-plane:before { + content: "\e108"; } -.col-xs-pull-7 { - right: 58.333333333333336%; +.glyphicon-calendar:before { + content: "\e109"; } -.col-xs-pull-6 { - right: 50%; +.glyphicon-random:before { + content: "\e110"; } -.col-xs-pull-5 { - right: 41.66666666666667%; +.glyphicon-comment:before { + content: "\e111"; } -.col-xs-pull-4 { - right: 33.33333333333333%; +.glyphicon-magnet:before { + content: "\e112"; } -.col-xs-pull-3 { - right: 25%; +.glyphicon-chevron-up:before { + content: "\e113"; } -.col-xs-pull-2 { - right: 16.666666666666664%; +.glyphicon-chevron-down:before { + content: "\e114"; } -.col-xs-pull-1 { - right: 8.333333333333332%; +.glyphicon-retweet:before { + content: "\e115"; } -.col-xs-pull-0 { - right: 0; +.glyphicon-shopping-cart:before { + content: "\e116"; } -.col-xs-push-12 { - left: 100%; +.glyphicon-folder-close:before { + content: "\e117"; } -.col-xs-push-11 { - left: 91.66666666666666%; +.glyphicon-folder-open:before { + content: "\e118"; } -.col-xs-push-10 { - left: 83.33333333333334%; +.glyphicon-resize-vertical:before { + content: "\e119"; } -.col-xs-push-9 { - left: 75%; +.glyphicon-resize-horizontal:before { + content: "\e120"; } -.col-xs-push-8 { - left: 66.66666666666666%; +.glyphicon-hdd:before { + content: "\e121"; } -.col-xs-push-7 { - left: 58.333333333333336%; +.glyphicon-bullhorn:before { + content: "\e122"; } -.col-xs-push-6 { - left: 50%; +.glyphicon-bell:before { + content: "\e123"; } -.col-xs-push-5 { - left: 41.66666666666667%; +.glyphicon-certificate:before { + content: "\e124"; } -.col-xs-push-4 { - left: 33.33333333333333%; +.glyphicon-thumbs-up:before { + content: "\e125"; } -.col-xs-push-3 { - left: 25%; +.glyphicon-thumbs-down:before { + content: "\e126"; } -.col-xs-push-2 { - left: 16.666666666666664%; +.glyphicon-hand-right:before { + content: "\e127"; } -.col-xs-push-1 { - left: 8.333333333333332%; +.glyphicon-hand-left:before { + content: "\e128"; } -.col-xs-push-0 { - left: 0; +.glyphicon-hand-up:before { + content: "\e129"; } -.col-xs-offset-12 { - margin-left: 100%; +.glyphicon-hand-down:before { + content: "\e130"; } -.col-xs-offset-11 { - margin-left: 91.66666666666666%; +.glyphicon-circle-arrow-right:before { + content: "\e131"; } -.col-xs-offset-10 { - margin-left: 83.33333333333334%; +.glyphicon-circle-arrow-left:before { + content: "\e132"; } -.col-xs-offset-9 { - margin-left: 75%; +.glyphicon-circle-arrow-up:before { + content: "\e133"; } -.col-xs-offset-8 { - margin-left: 66.66666666666666%; +.glyphicon-circle-arrow-down:before { + content: "\e134"; } -.col-xs-offset-7 { - margin-left: 58.333333333333336%; +.glyphicon-globe:before { + content: "\e135"; } -.col-xs-offset-6 { - margin-left: 50%; +.glyphicon-wrench:before { + content: "\e136"; } -.col-xs-offset-5 { - margin-left: 41.66666666666667%; +.glyphicon-tasks:before { + content: "\e137"; } -.col-xs-offset-4 { - margin-left: 33.33333333333333%; +.glyphicon-filter:before { + content: "\e138"; } -.col-xs-offset-3 { - margin-left: 25%; +.glyphicon-briefcase:before { + content: "\e139"; } -.col-xs-offset-2 { - margin-left: 16.666666666666664%; +.glyphicon-fullscreen:before { + content: "\e140"; } -.col-xs-offset-1 { - margin-left: 8.333333333333332%; +.glyphicon-dashboard:before { + content: "\e141"; } -.col-xs-offset-0 { - margin-left: 0; +.glyphicon-paperclip:before { + content: "\e142"; } -@media (min-width: 768px) { - .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 { - float: left; - } - .col-sm-12 { - width: 100%; - } - .col-sm-11 { - width: 91.66666666666666%; - } - .col-sm-10 { - width: 83.33333333333334%; - } - .col-sm-9 { - width: 75%; - } - .col-sm-8 { - width: 66.66666666666666%; - } - .col-sm-7 { - width: 58.333333333333336%; - } - .col-sm-6 { - width: 50%; - } - .col-sm-5 { - width: 41.66666666666667%; - } - .col-sm-4 { - width: 33.33333333333333%; - } - .col-sm-3 { - width: 25%; - } - .col-sm-2 { - width: 16.666666666666664%; - } - .col-sm-1 { - width: 8.333333333333332%; - } - .col-sm-pull-12 { - right: 100%; - } - .col-sm-pull-11 { - right: 91.66666666666666%; - } - .col-sm-pull-10 { - right: 83.33333333333334%; - } - .col-sm-pull-9 { - right: 75%; - } - .col-sm-pull-8 { - right: 66.66666666666666%; - } - .col-sm-pull-7 { - right: 58.333333333333336%; - } - .col-sm-pull-6 { - right: 50%; - } - .col-sm-pull-5 { - right: 41.66666666666667%; - } - .col-sm-pull-4 { - right: 33.33333333333333%; - } - .col-sm-pull-3 { - right: 25%; - } - .col-sm-pull-2 { - right: 16.666666666666664%; - } - .col-sm-pull-1 { - right: 8.333333333333332%; - } - .col-sm-pull-0 { - right: 0; - } - .col-sm-push-12 { - left: 100%; - } - .col-sm-push-11 { - left: 91.66666666666666%; - } - .col-sm-push-10 { - left: 83.33333333333334%; - } - .col-sm-push-9 { - left: 75%; - } - .col-sm-push-8 { - left: 66.66666666666666%; - } - .col-sm-push-7 { - left: 58.333333333333336%; - } - .col-sm-push-6 { - left: 50%; - } - .col-sm-push-5 { - left: 41.66666666666667%; - } - .col-sm-push-4 { - left: 33.33333333333333%; - } - .col-sm-push-3 { - left: 25%; - } - .col-sm-push-2 { - left: 16.666666666666664%; - } - .col-sm-push-1 { - left: 8.333333333333332%; - } - .col-sm-push-0 { - left: 0; - } - .col-sm-offset-12 { - margin-left: 100%; - } - .col-sm-offset-11 { - margin-left: 91.66666666666666%; - } - .col-sm-offset-10 { - margin-left: 83.33333333333334%; - } - .col-sm-offset-9 { - margin-left: 75%; - } - .col-sm-offset-8 { - margin-left: 66.66666666666666%; - } - .col-sm-offset-7 { - margin-left: 58.333333333333336%; - } - .col-sm-offset-6 { - margin-left: 50%; - } - .col-sm-offset-5 { - margin-left: 41.66666666666667%; - } - .col-sm-offset-4 { - margin-left: 33.33333333333333%; - } - .col-sm-offset-3 { - margin-left: 25%; - } - .col-sm-offset-2 { - margin-left: 16.666666666666664%; - } - .col-sm-offset-1 { - margin-left: 8.333333333333332%; - } - .col-sm-offset-0 { - margin-left: 0; - } +.glyphicon-heart-empty:before { + content: "\e143"; } -@media (min-width: 992px) { - .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 { - float: left; - } - .col-md-12 { - width: 100%; - } - .col-md-11 { - width: 91.66666666666666%; - } - .col-md-10 { - width: 83.33333333333334%; - } - .col-md-9 { - width: 75%; - } - .col-md-8 { - width: 66.66666666666666%; - } - .col-md-7 { - width: 58.333333333333336%; - } - .col-md-6 { - width: 50%; - } - .col-md-5 { - width: 41.66666666666667%; - } - .col-md-4 { - width: 33.33333333333333%; - } - .col-md-3 { - width: 25%; - } - .col-md-2 { - width: 16.666666666666664%; - } - .col-md-1 { - width: 8.333333333333332%; - } - .col-md-pull-12 { - right: 100%; - } - .col-md-pull-11 { - right: 91.66666666666666%; - } - .col-md-pull-10 { - right: 83.33333333333334%; - } - .col-md-pull-9 { - right: 75%; - } - .col-md-pull-8 { - right: 66.66666666666666%; - } - .col-md-pull-7 { - right: 58.333333333333336%; - } - .col-md-pull-6 { - right: 50%; - } - .col-md-pull-5 { - right: 41.66666666666667%; - } - .col-md-pull-4 { - right: 33.33333333333333%; - } - .col-md-pull-3 { - right: 25%; - } - .col-md-pull-2 { - right: 16.666666666666664%; - } - .col-md-pull-1 { - right: 8.333333333333332%; - } - .col-md-pull-0 { - right: 0; - } - .col-md-push-12 { - left: 100%; - } - .col-md-push-11 { - left: 91.66666666666666%; - } - .col-md-push-10 { - left: 83.33333333333334%; - } - .col-md-push-9 { - left: 75%; - } - .col-md-push-8 { - left: 66.66666666666666%; - } - .col-md-push-7 { - left: 58.333333333333336%; - } - .col-md-push-6 { - left: 50%; - } - .col-md-push-5 { - left: 41.66666666666667%; - } - .col-md-push-4 { - left: 33.33333333333333%; - } - .col-md-push-3 { - left: 25%; - } - .col-md-push-2 { - left: 16.666666666666664%; - } - .col-md-push-1 { - left: 8.333333333333332%; - } - .col-md-push-0 { - left: 0; - } - .col-md-offset-12 { - margin-left: 100%; - } - .col-md-offset-11 { - margin-left: 91.66666666666666%; - } - .col-md-offset-10 { - margin-left: 83.33333333333334%; - } - .col-md-offset-9 { - margin-left: 75%; - } - .col-md-offset-8 { - margin-left: 66.66666666666666%; - } - .col-md-offset-7 { - margin-left: 58.333333333333336%; - } - .col-md-offset-6 { - margin-left: 50%; - } - .col-md-offset-5 { - margin-left: 41.66666666666667%; - } - .col-md-offset-4 { - margin-left: 33.33333333333333%; - } - .col-md-offset-3 { - margin-left: 25%; - } - .col-md-offset-2 { - margin-left: 16.666666666666664%; - } - .col-md-offset-1 { - margin-left: 8.333333333333332%; - } - .col-md-offset-0 { - margin-left: 0; - } -} -@media (min-width: 1200px) { - .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 { - float: left; - } - .col-lg-12 { - width: 100%; - } - .col-lg-11 { - width: 91.66666666666666%; - } - .col-lg-10 { - width: 83.33333333333334%; - } - .col-lg-9 { - width: 75%; - } - .col-lg-8 { - width: 66.66666666666666%; - } - .col-lg-7 { - width: 58.333333333333336%; - } - .col-lg-6 { - width: 50%; - } - .col-lg-5 { - width: 41.66666666666667%; - } - .col-lg-4 { - width: 33.33333333333333%; - } - .col-lg-3 { - width: 25%; - } - .col-lg-2 { - width: 16.666666666666664%; - } - .col-lg-1 { - width: 8.333333333333332%; - } - .col-lg-pull-12 { - right: 100%; - } - .col-lg-pull-11 { - right: 91.66666666666666%; - } - .col-lg-pull-10 { - right: 83.33333333333334%; - } - .col-lg-pull-9 { - right: 75%; - } - .col-lg-pull-8 { - right: 66.66666666666666%; - } - .col-lg-pull-7 { - right: 58.333333333333336%; - } - .col-lg-pull-6 { - right: 50%; - } - .col-lg-pull-5 { - right: 41.66666666666667%; - } - .col-lg-pull-4 { - right: 33.33333333333333%; - } - .col-lg-pull-3 { - right: 25%; - } - .col-lg-pull-2 { - right: 16.666666666666664%; - } - .col-lg-pull-1 { - right: 8.333333333333332%; - } - .col-lg-pull-0 { - right: 0; - } - .col-lg-push-12 { - left: 100%; - } - .col-lg-push-11 { - left: 91.66666666666666%; - } - .col-lg-push-10 { - left: 83.33333333333334%; - } - .col-lg-push-9 { - left: 75%; - } - .col-lg-push-8 { - left: 66.66666666666666%; - } - .col-lg-push-7 { - left: 58.333333333333336%; - } - .col-lg-push-6 { - left: 50%; - } - .col-lg-push-5 { - left: 41.66666666666667%; - } - .col-lg-push-4 { - left: 33.33333333333333%; - } - .col-lg-push-3 { - left: 25%; - } - .col-lg-push-2 { - left: 16.666666666666664%; - } - .col-lg-push-1 { - left: 8.333333333333332%; - } - .col-lg-push-0 { - left: 0; - } - .col-lg-offset-12 { - margin-left: 100%; - } - .col-lg-offset-11 { - margin-left: 91.66666666666666%; - } - .col-lg-offset-10 { - margin-left: 83.33333333333334%; - } - .col-lg-offset-9 { - margin-left: 75%; - } - .col-lg-offset-8 { - margin-left: 66.66666666666666%; - } - .col-lg-offset-7 { - margin-left: 58.333333333333336%; - } - .col-lg-offset-6 { - margin-left: 50%; - } - .col-lg-offset-5 { - margin-left: 41.66666666666667%; - } - .col-lg-offset-4 { - margin-left: 33.33333333333333%; - } - .col-lg-offset-3 { - margin-left: 25%; - } - .col-lg-offset-2 { - margin-left: 16.666666666666664%; - } - .col-lg-offset-1 { - margin-left: 8.333333333333332%; - } - .col-lg-offset-0 { - margin-left: 0; - } +.glyphicon-link:before { + content: "\e144"; } -table { - max-width: 100%; - background-color: transparent; +.glyphicon-phone:before { + content: "\e145"; } -th { - text-align: left; +.glyphicon-pushpin:before { + content: "\e146"; } -.table { - width: 100%; - margin-bottom: 20px; +.glyphicon-usd:before { + content: "\e148"; } -.table > thead > tr > th, -.table > tbody > tr > th, -.table > tfoot > tr > th, -.table > thead > tr > td, -.table > tbody > tr > td, -.table > tfoot > tr > td { - padding: 8px; - line-height: 1.428571429; - vertical-align: top; - border-top: 1px solid #ddd; +.glyphicon-gbp:before { + content: "\e149"; } -.table > thead > tr > th { - vertical-align: bottom; - border-bottom: 2px solid #ddd; +.glyphicon-sort:before { + content: "\e150"; } -.table > caption + thead > tr:first-child > th, -.table > colgroup + thead > tr:first-child > th, -.table > thead:first-child > tr:first-child > th, -.table > caption + thead > tr:first-child > td, -.table > colgroup + thead > tr:first-child > td, -.table > thead:first-child > tr:first-child > td { - border-top: 0; +.glyphicon-sort-by-alphabet:before { + content: "\e151"; } -.table > tbody + tbody { - border-top: 2px solid #ddd; +.glyphicon-sort-by-alphabet-alt:before { + content: "\e152"; } -.table .table { - background-color: #fff; +.glyphicon-sort-by-order:before { + content: "\e153"; } -.table-condensed > thead > tr > th, -.table-condensed > tbody > tr > th, -.table-condensed > tfoot > tr > th, -.table-condensed > thead > tr > td, -.table-condensed > tbody > tr > td, -.table-condensed > tfoot > tr > td { - padding: 5px; +.glyphicon-sort-by-order-alt:before { + content: "\e154"; } -.table-bordered { - border: 1px solid #ddd; +.glyphicon-sort-by-attributes:before { + content: "\e155"; } -.table-bordered > thead > tr > th, -.table-bordered > tbody > tr > th, -.table-bordered > tfoot > tr > th, -.table-bordered > thead > tr > td, -.table-bordered > tbody > tr > td, -.table-bordered > tfoot > tr > td { - border: 1px solid #ddd; +.glyphicon-sort-by-attributes-alt:before { + content: "\e156"; } -.table-bordered > thead > tr > th, -.table-bordered > thead > tr > td { - border-bottom-width: 2px; +.glyphicon-unchecked:before { + content: "\e157"; } -.table-striped > tbody > tr:nth-child(odd) > td, -.table-striped > tbody > tr:nth-child(odd) > th { - background-color: #f9f9f9; +.glyphicon-expand:before { + content: "\e158"; } -.table-hover > tbody > tr:hover > td, -.table-hover > tbody > tr:hover > th { - background-color: #f5f5f5; +.glyphicon-collapse-down:before { + content: "\e159"; } -table col[class*="col-"] { - position: static; - display: table-column; - float: none; +.glyphicon-collapse-up:before { + content: "\e160"; } -table td[class*="col-"], -table th[class*="col-"] { - display: table-cell; - float: none; +.glyphicon-log-in:before { + content: "\e161"; } -.table > thead > tr > td.active, -.table > tbody > tr > td.active, -.table > tfoot > tr > td.active, -.table > thead > tr > th.active, -.table > tbody > tr > th.active, -.table > tfoot > tr > th.active, -.table > thead > tr.active > td, -.table > tbody > tr.active > td, -.table > tfoot > tr.active > td, -.table > thead > tr.active > th, -.table > tbody > tr.active > th, -.table > tfoot > tr.active > th { - background-color: #f5f5f5; +.glyphicon-flash:before { + content: "\e162"; } -.table-hover > tbody > tr > td.active:hover, -.table-hover > tbody > tr > th.active:hover, -.table-hover > tbody > tr.active:hover > td, -.table-hover > tbody > tr.active:hover > th { - background-color: #e8e8e8; +.glyphicon-log-out:before { + content: "\e163"; } -.table > thead > tr > td.success, -.table > tbody > tr > td.success, -.table > tfoot > tr > td.success, -.table > thead > tr > th.success, -.table > tbody > tr > th.success, -.table > tfoot > tr > th.success, -.table > thead > tr.success > td, -.table > tbody > tr.success > td, -.table > tfoot > tr.success > td, -.table > thead > tr.success > th, -.table > tbody > tr.success > th, -.table > tfoot > tr.success > th { - background-color: #dff0d8; +.glyphicon-new-window:before { + content: "\e164"; } -.table-hover > tbody > tr > td.success:hover, -.table-hover > tbody > tr > th.success:hover, -.table-hover > tbody > tr.success:hover > td, -.table-hover > tbody > tr.success:hover > th { - background-color: #d0e9c6; +.glyphicon-record:before { + content: "\e165"; } -.table > thead > tr > td.warning, -.table > tbody > tr > td.warning, -.table > tfoot > tr > td.warning, -.table > thead > tr > th.warning, -.table > tbody > tr > th.warning, -.table > tfoot > tr > th.warning, -.table > thead > tr.warning > td, -.table > tbody > tr.warning > td, -.table > tfoot > tr.warning > td, -.table > thead > tr.warning > th, -.table > tbody > tr.warning > th, -.table > tfoot > tr.warning > th { - background-color: #fcf8e3; +.glyphicon-save:before { + content: "\e166"; } -.table-hover > tbody > tr > td.warning:hover, -.table-hover > tbody > tr > th.warning:hover, -.table-hover > tbody > tr.warning:hover > td, -.table-hover > tbody > tr.warning:hover > th { - background-color: #faf2cc; +.glyphicon-open:before { + content: "\e167"; } -.table > thead > tr > td.danger, -.table > tbody > tr > td.danger, -.table > tfoot > tr > td.danger, -.table > thead > tr > th.danger, -.table > tbody > tr > th.danger, -.table > tfoot > tr > th.danger, -.table > thead > tr.danger > td, -.table > tbody > tr.danger > td, -.table > tfoot > tr.danger > td, -.table > thead > tr.danger > th, -.table > tbody > tr.danger > th, -.table > tfoot > tr.danger > th { - background-color: #f2dede; +.glyphicon-saved:before { + content: "\e168"; } -.table-hover > tbody > tr > td.danger:hover, -.table-hover > tbody > tr > th.danger:hover, -.table-hover > tbody > tr.danger:hover > td, -.table-hover > tbody > tr.danger:hover > th { - background-color: #ebcccc; +.glyphicon-import:before { + content: "\e169"; } -.table > thead > tr > td.info, -.table > tbody > tr > td.info, -.table > tfoot > tr > td.info, -.table > thead > tr > th.info, -.table > tbody > tr > th.info, -.table > tfoot > tr > th.info, -.table > thead > tr.info > td, -.table > tbody > tr.info > td, -.table > tfoot > tr.info > td, -.table > thead > tr.info > th, -.table > tbody > tr.info > th, -.table > tfoot > tr.info > th { - background-color: #d9edf7; +.glyphicon-export:before { + content: "\e170"; } -.table-hover > tbody > tr > td.info:hover, -.table-hover > tbody > tr > th.info:hover, -.table-hover > tbody > tr.info:hover > td, -.table-hover > tbody > tr.info:hover > th { - background-color: #c4e3f3; +.glyphicon-send:before { + content: "\e171"; } -@media (max-width: 767px) { - .table-responsive { - width: 100%; - margin-bottom: 15px; - overflow-x: scroll; - overflow-y: hidden; - -ms-overflow-style: -ms-autohiding-scrollbar; - border: 1px solid #ddd; - -webkit-overflow-scrolling: touch; - } - .table-responsive > .table { - margin-bottom: 0; - } - .table-responsive > .table > thead > tr > th, - .table-responsive > .table > tbody > tr > th, - .table-responsive > .table > tfoot > tr > th, - .table-responsive > .table > thead > tr > td, - .table-responsive > .table > tbody > tr > td, - .table-responsive > .table > tfoot > tr > td { - white-space: nowrap; - } - .table-responsive > .table-bordered { - border: 0; - } - .table-responsive > .table-bordered > thead > tr > th:first-child, - .table-responsive > .table-bordered > tbody > tr > th:first-child, - .table-responsive > .table-bordered > tfoot > tr > th:first-child, - .table-responsive > .table-bordered > thead > tr > td:first-child, - .table-responsive > .table-bordered > tbody > tr > td:first-child, - .table-responsive > .table-bordered > tfoot > tr > td:first-child { - border-left: 0; - } - .table-responsive > .table-bordered > thead > tr > th:last-child, - .table-responsive > .table-bordered > tbody > tr > th:last-child, - .table-responsive > .table-bordered > tfoot > tr > th:last-child, - .table-responsive > .table-bordered > thead > tr > td:last-child, - .table-responsive > .table-bordered > tbody > tr > td:last-child, - .table-responsive > .table-bordered > tfoot > tr > td:last-child { - border-right: 0; - } - .table-responsive > .table-bordered > tbody > tr:last-child > th, - .table-responsive > .table-bordered > tfoot > tr:last-child > th, - .table-responsive > .table-bordered > tbody > tr:last-child > td, - .table-responsive > .table-bordered > tfoot > tr:last-child > td { - border-bottom: 0; - } +.glyphicon-floppy-disk:before { + content: "\e172"; } -fieldset { - padding: 0; - margin: 0; - border: 0; +.glyphicon-floppy-saved:before { + content: "\e173"; } -legend { - display: block; - width: 100%; - padding: 0; - margin-bottom: 20px; - font-size: 21px; - line-height: inherit; - color: #333; - border: 0; - border-bottom: 1px solid #e5e5e5; +.glyphicon-floppy-remove:before { + content: "\e174"; } -label { - display: inline-block; - margin-bottom: 5px; - font-weight: bold; +.glyphicon-floppy-save:before { + content: "\e175"; } -input[type="search"] { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; +.glyphicon-floppy-open:before { + content: "\e176"; } -input[type="radio"], -input[type="checkbox"] { - margin: 4px 0 0; - margin-top: 1px \9; - /* IE8-9 */ - line-height: normal; +.glyphicon-credit-card:before { + content: "\e177"; } -input[type="file"] { - display: block; +.glyphicon-transfer:before { + content: "\e178"; } -select[multiple], -select[size] { - height: auto; +.glyphicon-cutlery:before { + content: "\e179"; } -select optgroup { - font-family: inherit; - font-size: inherit; - font-style: inherit; +.glyphicon-header:before { + content: "\e180"; } -input[type="file"]:focus, -input[type="radio"]:focus, -input[type="checkbox"]:focus { - outline: thin dotted; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; +.glyphicon-compressed:before { + content: "\e181"; } -input[type="number"]::-webkit-outer-spin-button, -input[type="number"]::-webkit-inner-spin-button { - height: auto; +.glyphicon-earphone:before { + content: "\e182"; } -output { - display: block; - padding-top: 7px; - font-size: 14px; - line-height: 1.428571429; - color: #555; +.glyphicon-phone-alt:before { + content: "\e183"; } -.form-control { - display: block; - width: 100%; - height: 34px; - padding: 6px 12px; - font-size: 14px; - line-height: 1.428571429; - color: #555; - background-color: #fff; - background-image: none; - border: 1px solid #ccc; - border-radius: 4px; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); - -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; - transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; +.glyphicon-tower:before { + content: "\e184"; } -.form-control:focus { - border-color: #66afe9; - outline: 0; - -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6); - box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6); +.glyphicon-stats:before { + content: "\e185"; } -.form-control:-moz-placeholder { - color: #999; +.glyphicon-sd-video:before { + content: "\e186"; } -.form-control::-moz-placeholder { - color: #999; - opacity: 1; +.glyphicon-hd-video:before { + content: "\e187"; } -.form-control:-ms-input-placeholder { - color: #999; +.glyphicon-subtitles:before { + content: "\e188"; } -.form-control::-webkit-input-placeholder { - color: #999; +.glyphicon-sound-stereo:before { + content: "\e189"; } -.form-control[disabled], -.form-control[readonly], -fieldset[disabled] .form-control { - cursor: not-allowed; - background-color: #eee; +.glyphicon-sound-dolby:before { + content: "\e190"; } -textarea.form-control { - height: auto; +.glyphicon-sound-5-1:before { + content: "\e191"; } -.form-group { - margin-bottom: 15px; +.glyphicon-sound-6-1:before { + content: "\e192"; } -.radio, -.checkbox { - display: block; - min-height: 20px; - padding-left: 20px; - margin-top: 10px; - margin-bottom: 10px; +.glyphicon-sound-7-1:before { + content: "\e193"; } -.radio label, -.checkbox label { - display: inline; - font-weight: normal; - cursor: pointer; +.glyphicon-copyright-mark:before { + content: "\e194"; } -.radio input[type="radio"], -.radio-inline input[type="radio"], -.checkbox input[type="checkbox"], -.checkbox-inline input[type="checkbox"] { - float: left; - margin-left: -20px; +.glyphicon-registration-mark:before { + content: "\e195"; } -.radio + .radio, -.checkbox + .checkbox { - margin-top: -5px; +.glyphicon-cloud-download:before { + content: "\e197"; } -.radio-inline, -.checkbox-inline { - display: inline-block; - padding-left: 20px; - margin-bottom: 0; - font-weight: normal; - vertical-align: middle; - cursor: pointer; +.glyphicon-cloud-upload:before { + content: "\e198"; } -.radio-inline + .radio-inline, -.checkbox-inline + .checkbox-inline { - margin-top: 0; - margin-left: 10px; +.glyphicon-tree-conifer:before { + content: "\e199"; } -input[type="radio"][disabled], -input[type="checkbox"][disabled], -.radio[disabled], -.radio-inline[disabled], -.checkbox[disabled], -.checkbox-inline[disabled], -fieldset[disabled] input[type="radio"], -fieldset[disabled] input[type="checkbox"], -fieldset[disabled] .radio, -fieldset[disabled] .radio-inline, -fieldset[disabled] .checkbox, -fieldset[disabled] .checkbox-inline { - cursor: not-allowed; +.glyphicon-tree-deciduous:before { + content: "\e200"; } -.input-sm { - height: 30px; - padding: 5px 10px; - font-size: 12px; - line-height: 1.5; - border-radius: 3px; +.glyphicon-cd:before { + content: "\e201"; } -select.input-sm { - height: 30px; - line-height: 30px; +.glyphicon-save-file:before { + content: "\e202"; } -textarea.input-sm { - height: auto; +.glyphicon-open-file:before { + content: "\e203"; } -.input-lg { - height: 46px; - padding: 10px 16px; - font-size: 18px; - line-height: 1.33; - border-radius: 6px; +.glyphicon-level-up:before { + content: "\e204"; } -select.input-lg { - height: 46px; - line-height: 46px; +.glyphicon-copy:before { + content: "\e205"; } -textarea.input-lg { - height: auto; +.glyphicon-paste:before { + content: "\e206"; } -.has-warning .help-block, -.has-warning .control-label, -.has-warning .radio, -.has-warning .checkbox, -.has-warning .radio-inline, -.has-warning .checkbox-inline { - color: #8a6d3b; +.glyphicon-door:before { + content: "\1f6aa"; } -.has-warning .form-control { - border-color: #8a6d3b; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); +.glyphicon-key:before { + content: "\1F511"; } -.has-warning .form-control:focus { - border-color: #66512c; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b; +.glyphicon-alert:before { + content: "\e209"; } -.has-warning .input-group-addon { - color: #8a6d3b; - background-color: #fcf8e3; - border-color: #8a6d3b; +.glyphicon-equalizer:before { + content: "\e210"; } -.has-error .help-block, -.has-error .control-label, -.has-error .radio, -.has-error .checkbox, -.has-error .radio-inline, -.has-error .checkbox-inline { - color: #a94442; +.glyphicon-king:before { + content: "\e211"; } -.has-error .form-control { - border-color: #a94442; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); +.glyphicon-queen:before { + content: "\e212"; } -.has-error .form-control:focus { - border-color: #843534; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; +.glyphicon-pawn:before { + content: "\e213"; } -.has-error .input-group-addon { - color: #a94442; - background-color: #f2dede; - border-color: #a94442; +.glyphicon-bishop:before { + content: "\e214"; } -.has-success .help-block, -.has-success .control-label, -.has-success .radio, -.has-success .checkbox, -.has-success .radio-inline, -.has-success .checkbox-inline { - color: #3c763d; +.glyphicon-knight:before { + content: "\e215"; } -.has-success .form-control { - border-color: #3c763d; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); +.glyphicon-baby-formula:before { + content: "\e216"; } -.has-success .form-control:focus { - border-color: #2b542c; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168; +.glyphicon-tent:before { + content: "\26fa"; } -.has-success .input-group-addon { - color: #3c763d; - background-color: #dff0d8; - border-color: #3c763d; +.glyphicon-blackboard:before { + content: "\e218"; } -.form-control-static { - margin-bottom: 0; +.glyphicon-bed:before { + content: "\e219"; } -.help-block { - display: block; - margin-top: 5px; - margin-bottom: 10px; - color: #737373; +.glyphicon-apple:before { + content: "\f8ff"; } -@media (min-width: 768px) { - .form-inline .form-group { - display: inline-block; - margin-bottom: 0; - vertical-align: middle; - } - .form-inline .form-control { - display: inline-block; - vertical-align: middle; - } - .form-inline select.form-control { - width: auto; - } - .form-inline .radio, - .form-inline .checkbox { - display: inline-block; - padding-left: 0; - margin-top: 0; - margin-bottom: 0; - vertical-align: middle; - } - .form-inline .radio input[type="radio"], - .form-inline .checkbox input[type="checkbox"] { - float: none; - margin-left: 0; - } +.glyphicon-erase:before { + content: "\e221"; } -.form-horizontal .control-label, -.form-horizontal .radio, -.form-horizontal .checkbox, -.form-horizontal .radio-inline, -.form-horizontal .checkbox-inline { - padding-top: 7px; - margin-top: 0; - margin-bottom: 0; +.glyphicon-hourglass:before { + content: "\231b"; } -.form-horizontal .radio, -.form-horizontal .checkbox { - min-height: 27px; +.glyphicon-lamp:before { + content: "\e223"; } -.form-horizontal .form-group { - margin-right: -15px; - margin-left: -15px; +.glyphicon-duplicate:before { + content: "\e224"; } -.form-horizontal .form-control-static { - padding-top: 7px; +.glyphicon-piggy-bank:before { + content: "\e225"; } -@media (min-width: 768px) { - .form-horizontal .control-label { - text-align: right; - } +.glyphicon-scissors:before { + content: "\e226"; } -.btn { - display: inline-block; - padding: 6px 12px; - -o-user-select: none; - margin-bottom: 0; - font-size: 14px; - font-weight: normal; - line-height: 1.428571429; - text-align: center; - white-space: nowrap; - vertical-align: middle; - cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - background-image: none; - border: 1px solid transparent; - border-radius: 4px; +.glyphicon-bitcoin:before { + content: "\e227"; } -.btn:focus { - outline: thin dotted; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; +.glyphicon-yen:before { + content: "\00a5"; } -.btn:hover, -.btn:focus { - color: #333; - text-decoration: none; +.glyphicon-ruble:before { + content: "\20bd"; } -.btn:active, -.btn.active { - background-image: none; - outline: 0; - -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); - box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); +.glyphicon-scale:before { + content: "\e230"; } -.btn.disabled, -.btn[disabled], -fieldset[disabled] .btn { - pointer-events: none; - cursor: not-allowed; - opacity: .65; - filter: alpha(opacity=65); - -webkit-box-shadow: none; - box-shadow: none; +.glyphicon-ice-lolly:before { + content: "\e231"; } -.btn-default { - color: #333; - background-color: #fff; - border-color: #ccc; +.glyphicon-ice-lolly-tasted:before { + content: "\e232"; } -.btn-default:hover, -.btn-default:focus, -.btn-default:active, -.btn-default.active, -.open .dropdown-toggle.btn-default { - color: #333; - background-color: #ebebeb; - border-color: #adadad; +.glyphicon-education:before { + content: "\e233"; } -.btn-default:active, -.btn-default.active, -.open .dropdown-toggle.btn-default { - background-image: none; +.glyphicon-option-horizontal:before { + content: "\e234"; } -.btn-default.disabled, -.btn-default[disabled], -fieldset[disabled] .btn-default, -.btn-default.disabled:hover, -.btn-default[disabled]:hover, -fieldset[disabled] .btn-default:hover, -.btn-default.disabled:focus, -.btn-default[disabled]:focus, -fieldset[disabled] .btn-default:focus, -.btn-default.disabled:active, -.btn-default[disabled]:active, -fieldset[disabled] .btn-default:active, -.btn-default.disabled.active, -.btn-default[disabled].active, -fieldset[disabled] .btn-default.active { - background-color: #fff; - border-color: #ccc; +.glyphicon-option-vertical:before { + content: "\e235"; } -.btn-default .badge { - color: #fff; - background-color: #333; +.glyphicon-menu-hamburger:before { + content: "\e236"; } -.btn-primary { - color: #fff; - background-color: #428bca; - border-color: #357ebd; +.glyphicon-modal-window:before { + content: "\e237"; } -.btn-primary:hover, -.btn-primary:focus, -.btn-primary:active, -.btn-primary.active, -.open .dropdown-toggle.btn-primary { - color: #fff; - background-color: #3276b1; - border-color: #285e8e; +.glyphicon-oil:before { + content: "\e238"; } -.btn-primary:active, -.btn-primary.active, -.open .dropdown-toggle.btn-primary { - background-image: none; +.glyphicon-grain:before { + content: "\e239"; } -.btn-primary.disabled, -.btn-primary[disabled], -fieldset[disabled] .btn-primary, -.btn-primary.disabled:hover, -.btn-primary[disabled]:hover, -fieldset[disabled] .btn-primary:hover, -.btn-primary.disabled:focus, -.btn-primary[disabled]:focus, -fieldset[disabled] .btn-primary:focus, -.btn-primary.disabled:active, -.btn-primary[disabled]:active, -fieldset[disabled] .btn-primary:active, -.btn-primary.disabled.active, -.btn-primary[disabled].active, -fieldset[disabled] .btn-primary.active { - background-color: #428bca; - border-color: #357ebd; +.glyphicon-sunglasses:before { + content: "\e240"; } -.btn-primary .badge { - color: #428bca; - background-color: #fff; +.glyphicon-text-size:before { + content: "\e241"; } -.btn-success { - color: #fff; - background-color: #5cb85c; - border-color: #4cae4c; +.glyphicon-text-color:before { + content: "\e242"; } -.btn-success:hover, -.btn-success:focus, -.btn-success:active, -.btn-success.active, -.open .dropdown-toggle.btn-success { - color: #fff; - background-color: #47a447; - border-color: #398439; +.glyphicon-text-background:before { + content: "\e243"; } -.btn-success:active, -.btn-success.active, -.open .dropdown-toggle.btn-success { - background-image: none; +.glyphicon-object-align-top:before { + content: "\e244"; } -.btn-success.disabled, -.btn-success[disabled], -fieldset[disabled] .btn-success, -.btn-success.disabled:hover, -.btn-success[disabled]:hover, -fieldset[disabled] .btn-success:hover, -.btn-success.disabled:focus, -.btn-success[disabled]:focus, -fieldset[disabled] .btn-success:focus, -.btn-success.disabled:active, -.btn-success[disabled]:active, -fieldset[disabled] .btn-success:active, -.btn-success.disabled.active, -.btn-success[disabled].active, -fieldset[disabled] .btn-success.active { - background-color: #5cb85c; - border-color: #4cae4c; +.glyphicon-object-align-bottom:before { + content: "\e245"; } -.btn-success .badge { - color: #5cb85c; - background-color: #fff; +.glyphicon-object-align-horizontal:before { + content: "\e246"; } -.btn-warning { - color: #fff; - background-color: #f0ad4e; - border-color: #eea236; +.glyphicon-object-align-left:before { + content: "\e247"; } -.btn-warning:hover, -.btn-warning:focus, -.btn-warning:active, -.btn-warning.active, -.open .dropdown-toggle.btn-warning { - color: #fff; - background-color: #ed9c28; - border-color: #d58512; +.glyphicon-object-align-vertical:before { + content: "\e248"; } -.btn-warning:active, -.btn-warning.active, -.open .dropdown-toggle.btn-warning { - background-image: none; +.glyphicon-object-align-right:before { + content: "\e249"; } -.btn-warning.disabled, -.btn-warning[disabled], -fieldset[disabled] .btn-warning, -.btn-warning.disabled:hover, -.btn-warning[disabled]:hover, -fieldset[disabled] .btn-warning:hover, -.btn-warning.disabled:focus, -.btn-warning[disabled]:focus, -fieldset[disabled] .btn-warning:focus, -.btn-warning.disabled:active, -.btn-warning[disabled]:active, -fieldset[disabled] .btn-warning:active, -.btn-warning.disabled.active, -.btn-warning[disabled].active, -fieldset[disabled] .btn-warning.active { - background-color: #f0ad4e; - border-color: #eea236; +.glyphicon-triangle-right:before { + content: "\e250"; } -.btn-warning .badge { - color: #f0ad4e; - background-color: #fff; +.glyphicon-triangle-left:before { + content: "\e251"; } -.btn-danger { - color: #fff; - background-color: #d9534f; - border-color: #d43f3a; +.glyphicon-triangle-bottom:before { + content: "\e252"; } -.btn-danger:hover, -.btn-danger:focus, -.btn-danger:active, -.btn-danger.active, -.open .dropdown-toggle.btn-danger { - color: #fff; - background-color: #d2322d; - border-color: #ac2925; +.glyphicon-triangle-top:before { + content: "\e253"; } -.btn-danger:active, -.btn-danger.active, -.open .dropdown-toggle.btn-danger { - background-image: none; +.glyphicon-console:before { + content: "\e254"; } -.btn-danger.disabled, -.btn-danger[disabled], -fieldset[disabled] .btn-danger, -.btn-danger.disabled:hover, -.btn-danger[disabled]:hover, -fieldset[disabled] .btn-danger:hover, -.btn-danger.disabled:focus, -.btn-danger[disabled]:focus, -fieldset[disabled] .btn-danger:focus, -.btn-danger.disabled:active, -.btn-danger[disabled]:active, -fieldset[disabled] .btn-danger:active, -.btn-danger.disabled.active, -.btn-danger[disabled].active, -fieldset[disabled] .btn-danger.active { - background-color: #d9534f; - border-color: #d43f3a; +.glyphicon-superscript:before { + content: "\e255"; } -.btn-danger .badge { - color: #d9534f; - background-color: #fff; +.glyphicon-subscript:before { + content: "\e256"; } -.btn-info { - color: #fff; - background-color: #5bc0de; - border-color: #46b8da; +.glyphicon-menu-left:before { + content: "\e257"; } -.btn-info:hover, -.btn-info:focus, -.btn-info:active, -.btn-info.active, -.open .dropdown-toggle.btn-info { - color: #fff; - background-color: #39b3d7; - border-color: #269abc; +.glyphicon-menu-right:before { + content: "\e258"; } -.btn-info:active, -.btn-info.active, -.open .dropdown-toggle.btn-info { - background-image: none; +.glyphicon-menu-down:before { + content: "\e259"; } -.btn-info.disabled, -.btn-info[disabled], -fieldset[disabled] .btn-info, -.btn-info.disabled:hover, -.btn-info[disabled]:hover, -fieldset[disabled] .btn-info:hover, -.btn-info.disabled:focus, -.btn-info[disabled]:focus, -fieldset[disabled] .btn-info:focus, -.btn-info.disabled:active, -.btn-info[disabled]:active, -fieldset[disabled] .btn-info:active, -.btn-info.disabled.active, -.btn-info[disabled].active, -fieldset[disabled] .btn-info.active { - background-color: #5bc0de; - border-color: #46b8da; +.glyphicon-menu-up:before { + content: "\e260"; } -.btn-info .badge { - color: #5bc0de; - background-color: #fff; +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } -.btn-link { - font-weight: normal; - color: #428bca; - cursor: pointer; - border-radius: 0; +*:before, +*:after { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } -.btn-link, -.btn-link:active, -.btn-link[disabled], -fieldset[disabled] .btn-link { - background-color: transparent; - -webkit-box-shadow: none; - box-shadow: none; +html { + font-size: 10px; + + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } -.btn-link, -.btn-link:hover, -.btn-link:focus, -.btn-link:active { - border-color: transparent; +body { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 1.42857143; + color: #333; + background-color: #fff; } -.btn-link:hover, -.btn-link:focus { - color: #2a6496; - text-decoration: underline; - background-color: transparent; +input, +button, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; } -.btn-link[disabled]:hover, -fieldset[disabled] .btn-link:hover, -.btn-link[disabled]:focus, -fieldset[disabled] .btn-link:focus { - color: #999; +a { + color: #337ab7; text-decoration: none; } -.btn-lg { - padding: 10px 16px; - font-size: 18px; - line-height: 1.33; - border-radius: 6px; +a:hover, +a:focus { + color: #23527c; + text-decoration: underline; } -.btn-sm { - padding: 5px 10px; - font-size: 12px; - line-height: 1.5; - border-radius: 3px; +a:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; } -.btn-xs { - padding: 1px 5px; - font-size: 12px; - line-height: 1.5; - border-radius: 3px; +figure { + margin: 0; } -.btn-block { +img { + vertical-align: middle; +} +.img-responsive, +.thumbnail > img, +.thumbnail a > img, +.carousel-inner > .item > img, +.carousel-inner > .item > a > img { display: block; - width: 100%; - padding-right: 0; - padding-left: 0; + max-width: 100%; + height: auto; } -.btn-block + .btn-block { - margin-top: 5px; +.img-rounded { + border-radius: 6px; } -input[type="submit"].btn-block, -input[type="reset"].btn-block, -input[type="button"].btn-block { - width: 100%; +.img-thumbnail { + display: inline-block; + max-width: 100%; + height: auto; + padding: 4px; + line-height: 1.42857143; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 4px; + -webkit-transition: all .2s ease-in-out; + -o-transition: all .2s ease-in-out; + transition: all .2s ease-in-out; } -@font-face { - font-family: 'Glyphicons Halflings'; - src: url('../fonts/glyphicons-halflings-regular.eot'); - src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons-halflingsregular') format('svg'); +.img-circle { + border-radius: 50%; } -.glyphicon { - position: relative; - top: 1px; - display: inline-block; - font-family: 'Glyphicons Halflings'; - font-style: normal; +hr { + margin-top: 20px; + margin-bottom: 20px; + border: 0; + border-top: 1px solid #eee; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} +.sr-only-focusable:active, +.sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; +} +h1, +h2, +h3, +h4, +h5, +h6, +.h1, +.h2, +.h3, +.h4, +.h5, +.h6 { + font-family: inherit; + font-weight: 500; + line-height: 1.1; + color: inherit; +} +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small, +.h1 small, +.h2 small, +.h3 small, +.h4 small, +.h5 small, +.h6 small, +h1 .small, +h2 .small, +h3 .small, +h4 .small, +h5 .small, +h6 .small, +.h1 .small, +.h2 .small, +.h3 .small, +.h4 .small, +.h5 .small, +.h6 .small { font-weight: normal; line-height: 1; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; + color: #777; } -.glyphicon:empty { - width: 1em; +h1, +.h1, +h2, +.h2, +h3, +.h3 { + margin-top: 20px; + margin-bottom: 10px; } -.glyphicon-asterisk:before { - content: "\2a"; +h1 small, +.h1 small, +h2 small, +.h2 small, +h3 small, +.h3 small, +h1 .small, +.h1 .small, +h2 .small, +.h2 .small, +h3 .small, +.h3 .small { + font-size: 65%; } -.glyphicon-plus:before { - content: "\2b"; +h4, +.h4, +h5, +.h5, +h6, +.h6 { + margin-top: 10px; + margin-bottom: 10px; } -.glyphicon-euro:before { - content: "\20ac"; +h4 small, +.h4 small, +h5 small, +.h5 small, +h6 small, +.h6 small, +h4 .small, +.h4 .small, +h5 .small, +.h5 .small, +h6 .small, +.h6 .small { + font-size: 75%; } -.glyphicon-minus:before { - content: "\2212"; +h1, +.h1 { + font-size: 36px; } -.glyphicon-cloud:before { - content: "\2601"; +h2, +.h2 { + font-size: 30px; } -.glyphicon-envelope:before { - content: "\2709"; +h3, +.h3 { + font-size: 24px; } -.glyphicon-pencil:before { - content: "\270f"; +h4, +.h4 { + font-size: 18px; } -.glyphicon-glass:before { - content: "\e001"; +h5, +.h5 { + font-size: 14px; } -.glyphicon-music:before { - content: "\e002"; +h6, +.h6 { + font-size: 12px; } -.glyphicon-search:before { - content: "\e003"; +p { + margin: 0 0 10px; } -.glyphicon-heart:before { - content: "\e005"; +.lead { + margin-bottom: 20px; + font-size: 16px; + font-weight: 300; + line-height: 1.4; } -.glyphicon-star:before { - content: "\e006"; +@media (min-width: 768px) { + .lead { + font-size: 21px; + } } -.glyphicon-star-empty:before { - content: "\e007"; +small, +.small { + font-size: 85%; } -.glyphicon-user:before { - content: "\e008"; +mark, +.mark { + padding: .2em; + background-color: #fcf8e3; } -.glyphicon-film:before { - content: "\e009"; +.text-left { + text-align: left; } -.glyphicon-th-large:before { - content: "\e010"; +.text-right { + text-align: right; } -.glyphicon-th:before { - content: "\e011"; +.text-center { + text-align: center; } -.glyphicon-th-list:before { - content: "\e012"; +.text-justify { + text-align: justify; } -.glyphicon-ok:before { - content: "\e013"; +.text-nowrap { + white-space: nowrap; } -.glyphicon-remove:before { - content: "\e014"; +.text-lowercase { + text-transform: lowercase; } -.glyphicon-zoom-in:before { - content: "\e015"; +.text-uppercase { + text-transform: uppercase; } -.glyphicon-zoom-out:before { - content: "\e016"; +.text-capitalize { + text-transform: capitalize; } -.glyphicon-off:before { - content: "\e017"; +.text-muted { + color: #777; } -.glyphicon-signal:before { - content: "\e018"; +.text-primary { + color: #337ab7; } -.glyphicon-cog:before { - content: "\e019"; +a.text-primary:hover { + color: #286090; } -.glyphicon-trash:before { - content: "\e020"; +.text-success { + color: #3c763d; } -.glyphicon-home:before { - content: "\e021"; +a.text-success:hover { + color: #2b542c; } -.glyphicon-file:before { - content: "\e022"; +.text-info { + color: #31708f; } -.glyphicon-time:before { - content: "\e023"; +a.text-info:hover { + color: #245269; } -.glyphicon-road:before { - content: "\e024"; +.text-warning { + color: #8a6d3b; } -.glyphicon-download-alt:before { - content: "\e025"; +a.text-warning:hover { + color: #66512c; } -.glyphicon-download:before { - content: "\e026"; +.text-danger { + color: #a94442; } -.glyphicon-upload:before { - content: "\e027"; +a.text-danger:hover { + color: #843534; } -.glyphicon-inbox:before { - content: "\e028"; +.bg-primary { + color: #fff; + background-color: #337ab7; } -.glyphicon-play-circle:before { - content: "\e029"; +a.bg-primary:hover { + background-color: #286090; } -.glyphicon-repeat:before { - content: "\e030"; +.bg-success { + background-color: #dff0d8; } -.glyphicon-refresh:before { - content: "\e031"; +a.bg-success:hover { + background-color: #c1e2b3; } -.glyphicon-list-alt:before { - content: "\e032"; +.bg-info { + background-color: #d9edf7; } -.glyphicon-lock:before { - content: "\e033"; +a.bg-info:hover { + background-color: #afd9ee; } -.glyphicon-flag:before { - content: "\e034"; +.bg-warning { + background-color: #fcf8e3; } -.glyphicon-headphones:before { - content: "\e035"; +a.bg-warning:hover { + background-color: #f7ecb5; } -.glyphicon-volume-off:before { - content: "\e036"; +.bg-danger { + background-color: #f2dede; } -.glyphicon-volume-down:before { - content: "\e037"; +a.bg-danger:hover { + background-color: #e4b9b9; } -.glyphicon-volume-up:before { - content: "\e038"; +.page-header { + padding-bottom: 9px; + margin: 40px 0 20px; + border-bottom: 1px solid #eee; +} +ul, +ol { + margin-top: 0; + margin-bottom: 10px; +} +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} +.list-unstyled { + padding-left: 0; + list-style: none; +} +.list-inline { + padding-left: 0; + margin-left: -5px; + list-style: none; +} +.list-inline > li { + display: inline-block; + padding-right: 5px; + padding-left: 5px; +} +dl { + margin-top: 0; + margin-bottom: 20px; +} +dt, +dd { + line-height: 1.42857143; +} +dt { + font-weight: bold; +} +dd { + margin-left: 0; +} +@media (min-width: 768px) { + .dl-horizontal dt { + float: left; + width: 160px; + overflow: hidden; + clear: left; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; + } + .dl-horizontal dd { + margin-left: 180px; + } +} +abbr[title], +abbr[data-original-title] { + cursor: help; + border-bottom: 1px dotted #777; +} +.initialism { + font-size: 90%; + text-transform: uppercase; +} +blockquote { + padding: 10px 20px; + margin: 0 0 20px; + font-size: 17.5px; + border-left: 5px solid #eee; +} +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} +blockquote footer, +blockquote small, +blockquote .small { + display: block; + font-size: 80%; + line-height: 1.42857143; + color: #777; +} +blockquote footer:before, +blockquote small:before, +blockquote .small:before { + content: '\2014 \00A0'; +} +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + text-align: right; + border-right: 5px solid #eee; + border-left: 0; +} +.blockquote-reverse footer:before, +blockquote.pull-right footer:before, +.blockquote-reverse small:before, +blockquote.pull-right small:before, +.blockquote-reverse .small:before, +blockquote.pull-right .small:before { + content: ''; +} +.blockquote-reverse footer:after, +blockquote.pull-right footer:after, +.blockquote-reverse small:after, +blockquote.pull-right small:after, +.blockquote-reverse .small:after, +blockquote.pull-right .small:after { + content: '\00A0 \2014'; +} +address { + margin-bottom: 20px; + font-style: normal; + line-height: 1.42857143; +} +code, +kbd, +pre, +samp { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; +} +code { + padding: 2px 4px; + font-size: 90%; + color: #c7254e; + background-color: #f9f2f4; + border-radius: 4px; +} +kbd { + padding: 2px 4px; + font-size: 90%; + color: #fff; + background-color: #333; + border-radius: 3px; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25); +} +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: bold; + -webkit-box-shadow: none; + box-shadow: none; +} +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.42857143; + color: #333; + word-break: break-all; + word-wrap: break-word; + background-color: #f5f5f5; + border: 1px solid #ccc; + border-radius: 4px; +} +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; +} +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} +.container { + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} +@media (min-width: 768px) { + .container { + width: 750px; + } +} +@media (min-width: 992px) { + .container { + width: 970px; + } +} +@media (min-width: 1200px) { + .container { + width: 1170px; + } +} +.container-fluid { + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} +.row { + margin-right: -15px; + margin-left: -15px; +} +.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; +} +.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 { + float: left; +} +.col-xs-12 { + width: 100%; +} +.col-xs-11 { + width: 91.66666667%; +} +.col-xs-10 { + width: 83.33333333%; +} +.col-xs-9 { + width: 75%; +} +.col-xs-8 { + width: 66.66666667%; +} +.col-xs-7 { + width: 58.33333333%; +} +.col-xs-6 { + width: 50%; +} +.col-xs-5 { + width: 41.66666667%; +} +.col-xs-4 { + width: 33.33333333%; +} +.col-xs-3 { + width: 25%; +} +.col-xs-2 { + width: 16.66666667%; +} +.col-xs-1 { + width: 8.33333333%; +} +.col-xs-pull-12 { + right: 100%; +} +.col-xs-pull-11 { + right: 91.66666667%; +} +.col-xs-pull-10 { + right: 83.33333333%; +} +.col-xs-pull-9 { + right: 75%; +} +.col-xs-pull-8 { + right: 66.66666667%; +} +.col-xs-pull-7 { + right: 58.33333333%; +} +.col-xs-pull-6 { + right: 50%; +} +.col-xs-pull-5 { + right: 41.66666667%; +} +.col-xs-pull-4 { + right: 33.33333333%; +} +.col-xs-pull-3 { + right: 25%; +} +.col-xs-pull-2 { + right: 16.66666667%; +} +.col-xs-pull-1 { + right: 8.33333333%; +} +.col-xs-pull-0 { + right: auto; +} +.col-xs-push-12 { + left: 100%; +} +.col-xs-push-11 { + left: 91.66666667%; +} +.col-xs-push-10 { + left: 83.33333333%; +} +.col-xs-push-9 { + left: 75%; +} +.col-xs-push-8 { + left: 66.66666667%; +} +.col-xs-push-7 { + left: 58.33333333%; +} +.col-xs-push-6 { + left: 50%; +} +.col-xs-push-5 { + left: 41.66666667%; +} +.col-xs-push-4 { + left: 33.33333333%; +} +.col-xs-push-3 { + left: 25%; +} +.col-xs-push-2 { + left: 16.66666667%; +} +.col-xs-push-1 { + left: 8.33333333%; +} +.col-xs-push-0 { + left: auto; +} +.col-xs-offset-12 { + margin-left: 100%; +} +.col-xs-offset-11 { + margin-left: 91.66666667%; +} +.col-xs-offset-10 { + margin-left: 83.33333333%; +} +.col-xs-offset-9 { + margin-left: 75%; +} +.col-xs-offset-8 { + margin-left: 66.66666667%; +} +.col-xs-offset-7 { + margin-left: 58.33333333%; +} +.col-xs-offset-6 { + margin-left: 50%; +} +.col-xs-offset-5 { + margin-left: 41.66666667%; +} +.col-xs-offset-4 { + margin-left: 33.33333333%; +} +.col-xs-offset-3 { + margin-left: 25%; +} +.col-xs-offset-2 { + margin-left: 16.66666667%; +} +.col-xs-offset-1 { + margin-left: 8.33333333%; +} +.col-xs-offset-0 { + margin-left: 0; +} +@media (min-width: 768px) { + .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 { + float: left; + } + .col-sm-12 { + width: 100%; + } + .col-sm-11 { + width: 91.66666667%; + } + .col-sm-10 { + width: 83.33333333%; + } + .col-sm-9 { + width: 75%; + } + .col-sm-8 { + width: 66.66666667%; + } + .col-sm-7 { + width: 58.33333333%; + } + .col-sm-6 { + width: 50%; + } + .col-sm-5 { + width: 41.66666667%; + } + .col-sm-4 { + width: 33.33333333%; + } + .col-sm-3 { + width: 25%; + } + .col-sm-2 { + width: 16.66666667%; + } + .col-sm-1 { + width: 8.33333333%; + } + .col-sm-pull-12 { + right: 100%; + } + .col-sm-pull-11 { + right: 91.66666667%; + } + .col-sm-pull-10 { + right: 83.33333333%; + } + .col-sm-pull-9 { + right: 75%; + } + .col-sm-pull-8 { + right: 66.66666667%; + } + .col-sm-pull-7 { + right: 58.33333333%; + } + .col-sm-pull-6 { + right: 50%; + } + .col-sm-pull-5 { + right: 41.66666667%; + } + .col-sm-pull-4 { + right: 33.33333333%; + } + .col-sm-pull-3 { + right: 25%; + } + .col-sm-pull-2 { + right: 16.66666667%; + } + .col-sm-pull-1 { + right: 8.33333333%; + } + .col-sm-pull-0 { + right: auto; + } + .col-sm-push-12 { + left: 100%; + } + .col-sm-push-11 { + left: 91.66666667%; + } + .col-sm-push-10 { + left: 83.33333333%; + } + .col-sm-push-9 { + left: 75%; + } + .col-sm-push-8 { + left: 66.66666667%; + } + .col-sm-push-7 { + left: 58.33333333%; + } + .col-sm-push-6 { + left: 50%; + } + .col-sm-push-5 { + left: 41.66666667%; + } + .col-sm-push-4 { + left: 33.33333333%; + } + .col-sm-push-3 { + left: 25%; + } + .col-sm-push-2 { + left: 16.66666667%; + } + .col-sm-push-1 { + left: 8.33333333%; + } + .col-sm-push-0 { + left: auto; + } + .col-sm-offset-12 { + margin-left: 100%; + } + .col-sm-offset-11 { + margin-left: 91.66666667%; + } + .col-sm-offset-10 { + margin-left: 83.33333333%; + } + .col-sm-offset-9 { + margin-left: 75%; + } + .col-sm-offset-8 { + margin-left: 66.66666667%; + } + .col-sm-offset-7 { + margin-left: 58.33333333%; + } + .col-sm-offset-6 { + margin-left: 50%; + } + .col-sm-offset-5 { + margin-left: 41.66666667%; + } + .col-sm-offset-4 { + margin-left: 33.33333333%; + } + .col-sm-offset-3 { + margin-left: 25%; + } + .col-sm-offset-2 { + margin-left: 16.66666667%; + } + .col-sm-offset-1 { + margin-left: 8.33333333%; + } + .col-sm-offset-0 { + margin-left: 0; + } +} +@media (min-width: 992px) { + .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 { + float: left; + } + .col-md-12 { + width: 100%; + } + .col-md-11 { + width: 91.66666667%; + } + .col-md-10 { + width: 83.33333333%; + } + .col-md-9 { + width: 75%; + } + .col-md-8 { + width: 66.66666667%; + } + .col-md-7 { + width: 58.33333333%; + } + .col-md-6 { + width: 50%; + } + .col-md-5 { + width: 41.66666667%; + } + .col-md-4 { + width: 33.33333333%; + } + .col-md-3 { + width: 25%; + } + .col-md-2 { + width: 16.66666667%; + } + .col-md-1 { + width: 8.33333333%; + } + .col-md-pull-12 { + right: 100%; + } + .col-md-pull-11 { + right: 91.66666667%; + } + .col-md-pull-10 { + right: 83.33333333%; + } + .col-md-pull-9 { + right: 75%; + } + .col-md-pull-8 { + right: 66.66666667%; + } + .col-md-pull-7 { + right: 58.33333333%; + } + .col-md-pull-6 { + right: 50%; + } + .col-md-pull-5 { + right: 41.66666667%; + } + .col-md-pull-4 { + right: 33.33333333%; + } + .col-md-pull-3 { + right: 25%; + } + .col-md-pull-2 { + right: 16.66666667%; + } + .col-md-pull-1 { + right: 8.33333333%; + } + .col-md-pull-0 { + right: auto; + } + .col-md-push-12 { + left: 100%; + } + .col-md-push-11 { + left: 91.66666667%; + } + .col-md-push-10 { + left: 83.33333333%; + } + .col-md-push-9 { + left: 75%; + } + .col-md-push-8 { + left: 66.66666667%; + } + .col-md-push-7 { + left: 58.33333333%; + } + .col-md-push-6 { + left: 50%; + } + .col-md-push-5 { + left: 41.66666667%; + } + .col-md-push-4 { + left: 33.33333333%; + } + .col-md-push-3 { + left: 25%; + } + .col-md-push-2 { + left: 16.66666667%; + } + .col-md-push-1 { + left: 8.33333333%; + } + .col-md-push-0 { + left: auto; + } + .col-md-offset-12 { + margin-left: 100%; + } + .col-md-offset-11 { + margin-left: 91.66666667%; + } + .col-md-offset-10 { + margin-left: 83.33333333%; + } + .col-md-offset-9 { + margin-left: 75%; + } + .col-md-offset-8 { + margin-left: 66.66666667%; + } + .col-md-offset-7 { + margin-left: 58.33333333%; + } + .col-md-offset-6 { + margin-left: 50%; + } + .col-md-offset-5 { + margin-left: 41.66666667%; + } + .col-md-offset-4 { + margin-left: 33.33333333%; + } + .col-md-offset-3 { + margin-left: 25%; + } + .col-md-offset-2 { + margin-left: 16.66666667%; + } + .col-md-offset-1 { + margin-left: 8.33333333%; + } + .col-md-offset-0 { + margin-left: 0; + } +} +@media (min-width: 1200px) { + .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 { + float: left; + } + .col-lg-12 { + width: 100%; + } + .col-lg-11 { + width: 91.66666667%; + } + .col-lg-10 { + width: 83.33333333%; + } + .col-lg-9 { + width: 75%; + } + .col-lg-8 { + width: 66.66666667%; + } + .col-lg-7 { + width: 58.33333333%; + } + .col-lg-6 { + width: 50%; + } + .col-lg-5 { + width: 41.66666667%; + } + .col-lg-4 { + width: 33.33333333%; + } + .col-lg-3 { + width: 25%; + } + .col-lg-2 { + width: 16.66666667%; + } + .col-lg-1 { + width: 8.33333333%; + } + .col-lg-pull-12 { + right: 100%; + } + .col-lg-pull-11 { + right: 91.66666667%; + } + .col-lg-pull-10 { + right: 83.33333333%; + } + .col-lg-pull-9 { + right: 75%; + } + .col-lg-pull-8 { + right: 66.66666667%; + } + .col-lg-pull-7 { + right: 58.33333333%; + } + .col-lg-pull-6 { + right: 50%; + } + .col-lg-pull-5 { + right: 41.66666667%; + } + .col-lg-pull-4 { + right: 33.33333333%; + } + .col-lg-pull-3 { + right: 25%; + } + .col-lg-pull-2 { + right: 16.66666667%; + } + .col-lg-pull-1 { + right: 8.33333333%; + } + .col-lg-pull-0 { + right: auto; + } + .col-lg-push-12 { + left: 100%; + } + .col-lg-push-11 { + left: 91.66666667%; + } + .col-lg-push-10 { + left: 83.33333333%; + } + .col-lg-push-9 { + left: 75%; + } + .col-lg-push-8 { + left: 66.66666667%; + } + .col-lg-push-7 { + left: 58.33333333%; + } + .col-lg-push-6 { + left: 50%; + } + .col-lg-push-5 { + left: 41.66666667%; + } + .col-lg-push-4 { + left: 33.33333333%; + } + .col-lg-push-3 { + left: 25%; + } + .col-lg-push-2 { + left: 16.66666667%; + } + .col-lg-push-1 { + left: 8.33333333%; + } + .col-lg-push-0 { + left: auto; + } + .col-lg-offset-12 { + margin-left: 100%; + } + .col-lg-offset-11 { + margin-left: 91.66666667%; + } + .col-lg-offset-10 { + margin-left: 83.33333333%; + } + .col-lg-offset-9 { + margin-left: 75%; + } + .col-lg-offset-8 { + margin-left: 66.66666667%; + } + .col-lg-offset-7 { + margin-left: 58.33333333%; + } + .col-lg-offset-6 { + margin-left: 50%; + } + .col-lg-offset-5 { + margin-left: 41.66666667%; + } + .col-lg-offset-4 { + margin-left: 33.33333333%; + } + .col-lg-offset-3 { + margin-left: 25%; + } + .col-lg-offset-2 { + margin-left: 16.66666667%; + } + .col-lg-offset-1 { + margin-left: 8.33333333%; + } + .col-lg-offset-0 { + margin-left: 0; + } } -.glyphicon-qrcode:before { - content: "\e039"; +table { + background-color: transparent; } -.glyphicon-barcode:before { - content: "\e040"; +caption { + padding-top: 8px; + padding-bottom: 8px; + color: #777; + text-align: left; } -.glyphicon-tag:before { - content: "\e041"; +th { + text-align: left; } -.glyphicon-tags:before { - content: "\e042"; +.table { + width: 100%; + max-width: 100%; + margin-bottom: 20px; } -.glyphicon-book:before { - content: "\e043"; +.table > thead > tr > th, +.table > tbody > tr > th, +.table > tfoot > tr > th, +.table > thead > tr > td, +.table > tbody > tr > td, +.table > tfoot > tr > td { + padding: 8px; + line-height: 1.42857143; + vertical-align: top; + border-top: 1px solid #ddd; } -.glyphicon-bookmark:before { - content: "\e044"; +.table > thead > tr > th { + vertical-align: bottom; + border-bottom: 2px solid #ddd; } -.glyphicon-print:before { - content: "\e045"; +.table > caption + thead > tr:first-child > th, +.table > colgroup + thead > tr:first-child > th, +.table > thead:first-child > tr:first-child > th, +.table > caption + thead > tr:first-child > td, +.table > colgroup + thead > tr:first-child > td, +.table > thead:first-child > tr:first-child > td { + border-top: 0; } -.glyphicon-camera:before { - content: "\e046"; +.table > tbody + tbody { + border-top: 2px solid #ddd; } -.glyphicon-font:before { - content: "\e047"; +.table .table { + background-color: #fff; } -.glyphicon-bold:before { - content: "\e048"; +.table-condensed > thead > tr > th, +.table-condensed > tbody > tr > th, +.table-condensed > tfoot > tr > th, +.table-condensed > thead > tr > td, +.table-condensed > tbody > tr > td, +.table-condensed > tfoot > tr > td { + padding: 5px; } -.glyphicon-italic:before { - content: "\e049"; +.table-bordered { + border: 1px solid #ddd; } -.glyphicon-text-height:before { - content: "\e050"; +.table-bordered > thead > tr > th, +.table-bordered > tbody > tr > th, +.table-bordered > tfoot > tr > th, +.table-bordered > thead > tr > td, +.table-bordered > tbody > tr > td, +.table-bordered > tfoot > tr > td { + border: 1px solid #ddd; } -.glyphicon-text-width:before { - content: "\e051"; +.table-bordered > thead > tr > th, +.table-bordered > thead > tr > td { + border-bottom-width: 2px; } -.glyphicon-align-left:before { - content: "\e052"; +.table-striped > tbody > tr:nth-child(odd) { + background-color: #f9f9f9; } -.glyphicon-align-center:before { - content: "\e053"; +.table-hover > tbody > tr:hover { + background-color: #f5f5f5; } -.glyphicon-align-right:before { - content: "\e054"; +table col[class*="col-"] { + position: static; + display: table-column; + float: none; } -.glyphicon-align-justify:before { - content: "\e055"; +table td[class*="col-"], +table th[class*="col-"] { + position: static; + display: table-cell; + float: none; } -.glyphicon-list:before { - content: "\e056"; +.table > thead > tr > td.active, +.table > tbody > tr > td.active, +.table > tfoot > tr > td.active, +.table > thead > tr > th.active, +.table > tbody > tr > th.active, +.table > tfoot > tr > th.active, +.table > thead > tr.active > td, +.table > tbody > tr.active > td, +.table > tfoot > tr.active > td, +.table > thead > tr.active > th, +.table > tbody > tr.active > th, +.table > tfoot > tr.active > th { + background-color: #f5f5f5; } -.glyphicon-indent-left:before { - content: "\e057"; +.table-hover > tbody > tr > td.active:hover, +.table-hover > tbody > tr > th.active:hover, +.table-hover > tbody > tr.active:hover > td, +.table-hover > tbody > tr:hover > .active, +.table-hover > tbody > tr.active:hover > th { + background-color: #e8e8e8; } -.glyphicon-indent-right:before { - content: "\e058"; +.table > thead > tr > td.success, +.table > tbody > tr > td.success, +.table > tfoot > tr > td.success, +.table > thead > tr > th.success, +.table > tbody > tr > th.success, +.table > tfoot > tr > th.success, +.table > thead > tr.success > td, +.table > tbody > tr.success > td, +.table > tfoot > tr.success > td, +.table > thead > tr.success > th, +.table > tbody > tr.success > th, +.table > tfoot > tr.success > th { + background-color: #dff0d8; } -.glyphicon-facetime-video:before { - content: "\e059"; +.table-hover > tbody > tr > td.success:hover, +.table-hover > tbody > tr > th.success:hover, +.table-hover > tbody > tr.success:hover > td, +.table-hover > tbody > tr:hover > .success, +.table-hover > tbody > tr.success:hover > th { + background-color: #d0e9c6; } -.glyphicon-picture:before { - content: "\e060"; +.table > thead > tr > td.info, +.table > tbody > tr > td.info, +.table > tfoot > tr > td.info, +.table > thead > tr > th.info, +.table > tbody > tr > th.info, +.table > tfoot > tr > th.info, +.table > thead > tr.info > td, +.table > tbody > tr.info > td, +.table > tfoot > tr.info > td, +.table > thead > tr.info > th, +.table > tbody > tr.info > th, +.table > tfoot > tr.info > th { + background-color: #d9edf7; } -.glyphicon-map-marker:before { - content: "\e062"; +.table-hover > tbody > tr > td.info:hover, +.table-hover > tbody > tr > th.info:hover, +.table-hover > tbody > tr.info:hover > td, +.table-hover > tbody > tr:hover > .info, +.table-hover > tbody > tr.info:hover > th { + background-color: #c4e3f3; } -.glyphicon-adjust:before { - content: "\e063"; +.table > thead > tr > td.warning, +.table > tbody > tr > td.warning, +.table > tfoot > tr > td.warning, +.table > thead > tr > th.warning, +.table > tbody > tr > th.warning, +.table > tfoot > tr > th.warning, +.table > thead > tr.warning > td, +.table > tbody > tr.warning > td, +.table > tfoot > tr.warning > td, +.table > thead > tr.warning > th, +.table > tbody > tr.warning > th, +.table > tfoot > tr.warning > th { + background-color: #fcf8e3; } -.glyphicon-tint:before { - content: "\e064"; +.table-hover > tbody > tr > td.warning:hover, +.table-hover > tbody > tr > th.warning:hover, +.table-hover > tbody > tr.warning:hover > td, +.table-hover > tbody > tr:hover > .warning, +.table-hover > tbody > tr.warning:hover > th { + background-color: #faf2cc; } -.glyphicon-edit:before { - content: "\e065"; +.table > thead > tr > td.danger, +.table > tbody > tr > td.danger, +.table > tfoot > tr > td.danger, +.table > thead > tr > th.danger, +.table > tbody > tr > th.danger, +.table > tfoot > tr > th.danger, +.table > thead > tr.danger > td, +.table > tbody > tr.danger > td, +.table > tfoot > tr.danger > td, +.table > thead > tr.danger > th, +.table > tbody > tr.danger > th, +.table > tfoot > tr.danger > th { + background-color: #f2dede; } -.glyphicon-share:before { - content: "\e066"; +.table-hover > tbody > tr > td.danger:hover, +.table-hover > tbody > tr > th.danger:hover, +.table-hover > tbody > tr.danger:hover > td, +.table-hover > tbody > tr:hover > .danger, +.table-hover > tbody > tr.danger:hover > th { + background-color: #ebcccc; } -.glyphicon-check:before { - content: "\e067"; +.table-responsive { + min-height: .01%; + overflow-x: auto; } -.glyphicon-move:before { - content: "\e068"; +@media screen and (max-width: 767px) { + .table-responsive { + width: 100%; + margin-bottom: 15px; + overflow-y: hidden; + -ms-overflow-style: -ms-autohiding-scrollbar; + border: 1px solid #ddd; + } + .table-responsive > .table { + margin-bottom: 0; + } + .table-responsive > .table > thead > tr > th, + .table-responsive > .table > tbody > tr > th, + .table-responsive > .table > tfoot > tr > th, + .table-responsive > .table > thead > tr > td, + .table-responsive > .table > tbody > tr > td, + .table-responsive > .table > tfoot > tr > td { + white-space: nowrap; + } + .table-responsive > .table-bordered { + border: 0; + } + .table-responsive > .table-bordered > thead > tr > th:first-child, + .table-responsive > .table-bordered > tbody > tr > th:first-child, + .table-responsive > .table-bordered > tfoot > tr > th:first-child, + .table-responsive > .table-bordered > thead > tr > td:first-child, + .table-responsive > .table-bordered > tbody > tr > td:first-child, + .table-responsive > .table-bordered > tfoot > tr > td:first-child { + border-left: 0; + } + .table-responsive > .table-bordered > thead > tr > th:last-child, + .table-responsive > .table-bordered > tbody > tr > th:last-child, + .table-responsive > .table-bordered > tfoot > tr > th:last-child, + .table-responsive > .table-bordered > thead > tr > td:last-child, + .table-responsive > .table-bordered > tbody > tr > td:last-child, + .table-responsive > .table-bordered > tfoot > tr > td:last-child { + border-right: 0; + } + .table-responsive > .table-bordered > tbody > tr:last-child > th, + .table-responsive > .table-bordered > tfoot > tr:last-child > th, + .table-responsive > .table-bordered > tbody > tr:last-child > td, + .table-responsive > .table-bordered > tfoot > tr:last-child > td { + border-bottom: 0; + } } -.glyphicon-step-backward:before { - content: "\e069"; +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; } -.glyphicon-fast-backward:before { - content: "\e070"; +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 20px; + font-size: 21px; + line-height: inherit; + color: #333; + border: 0; + border-bottom: 1px solid #e5e5e5; } -.glyphicon-backward:before { - content: "\e071"; +label { + display: inline-block; + max-width: 100%; + margin-bottom: 5px; + font-weight: bold; } -.glyphicon-play:before { - content: "\e072"; +input[type="search"] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } -.glyphicon-pause:before { - content: "\e073"; +input[type="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; + line-height: normal; } -.glyphicon-stop:before { - content: "\e074"; +input[type="file"] { + display: block; } -.glyphicon-forward:before { - content: "\e075"; +input[type="range"] { + display: block; + width: 100%; } -.glyphicon-fast-forward:before { - content: "\e076"; +select[multiple], +select[size] { + height: auto; } -.glyphicon-step-forward:before { - content: "\e077"; +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; } -.glyphicon-eject:before { - content: "\e078"; +output { + display: block; + padding-top: 7px; + font-size: 14px; + line-height: 1.42857143; + color: #555; } -.glyphicon-chevron-left:before { - content: "\e079"; +.form-control { + display: block; + width: 100%; + height: 34px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857143; + color: #555; + background-color: #fff; + background-image: none; + border: 1px solid #ccc; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; + -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; } -.glyphicon-chevron-right:before { - content: "\e080"; +.form-control:focus { + border-color: #66afe9; + outline: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6); + box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6); } -.glyphicon-plus-sign:before { - content: "\e081"; +.form-control::-moz-placeholder { + color: #999; + opacity: 1; } -.glyphicon-minus-sign:before { - content: "\e082"; +.form-control:-ms-input-placeholder { + color: #999; } -.glyphicon-remove-sign:before { - content: "\e083"; +.form-control::-webkit-input-placeholder { + color: #999; } -.glyphicon-ok-sign:before { - content: "\e084"; +.form-control[disabled], +.form-control[readonly], +fieldset[disabled] .form-control { + cursor: not-allowed; + background-color: #eee; + opacity: 1; } -.glyphicon-question-sign:before { - content: "\e085"; +textarea.form-control { + height: auto; } -.glyphicon-info-sign:before { - content: "\e086"; +input[type="search"] { + -webkit-appearance: none; } -.glyphicon-screenshot:before { - content: "\e087"; +@media screen and (-webkit-min-device-pixel-ratio: 0) { + input[type="date"], + input[type="time"], + input[type="datetime-local"], + input[type="month"] { + line-height: 34px; + } + input[type="date"].input-sm, + input[type="time"].input-sm, + input[type="datetime-local"].input-sm, + input[type="month"].input-sm { + line-height: 30px; + } + input[type="date"].input-lg, + input[type="time"].input-lg, + input[type="datetime-local"].input-lg, + input[type="month"].input-lg { + line-height: 46px; + } } -.glyphicon-remove-circle:before { - content: "\e088"; +.form-group { + margin-bottom: 15px; } -.glyphicon-ok-circle:before { - content: "\e089"; +.radio, +.checkbox { + position: relative; + display: block; + margin-top: 10px; + margin-bottom: 10px; } -.glyphicon-ban-circle:before { - content: "\e090"; +.radio label, +.checkbox label { + min-height: 20px; + padding-left: 20px; + margin-bottom: 0; + font-weight: normal; + cursor: pointer; } -.glyphicon-arrow-left:before { - content: "\e091"; +.radio input[type="radio"], +.radio-inline input[type="radio"], +.checkbox input[type="checkbox"], +.checkbox-inline input[type="checkbox"] { + position: absolute; + margin-top: 4px \9; + margin-left: -20px; } -.glyphicon-arrow-right:before { - content: "\e092"; +.radio + .radio, +.checkbox + .checkbox { + margin-top: -5px; } -.glyphicon-arrow-up:before { - content: "\e093"; +.radio-inline, +.checkbox-inline { + display: inline-block; + padding-left: 20px; + margin-bottom: 0; + font-weight: normal; + vertical-align: middle; + cursor: pointer; } -.glyphicon-arrow-down:before { - content: "\e094"; +.radio-inline + .radio-inline, +.checkbox-inline + .checkbox-inline { + margin-top: 0; + margin-left: 10px; } -.glyphicon-share-alt:before { - content: "\e095"; +input[type="radio"][disabled], +input[type="checkbox"][disabled], +input[type="radio"].disabled, +input[type="checkbox"].disabled, +fieldset[disabled] input[type="radio"], +fieldset[disabled] input[type="checkbox"] { + cursor: not-allowed; } -.glyphicon-resize-full:before { - content: "\e096"; +.radio-inline.disabled, +.checkbox-inline.disabled, +fieldset[disabled] .radio-inline, +fieldset[disabled] .checkbox-inline { + cursor: not-allowed; } -.glyphicon-resize-small:before { - content: "\e097"; +.radio.disabled label, +.checkbox.disabled label, +fieldset[disabled] .radio label, +fieldset[disabled] .checkbox label { + cursor: not-allowed; } -.glyphicon-exclamation-sign:before { - content: "\e101"; +.form-control-static { + padding-top: 7px; + padding-bottom: 7px; + margin-bottom: 0; } -.glyphicon-gift:before { - content: "\e102"; +.form-control-static.input-lg, +.form-control-static.input-sm { + padding-right: 0; + padding-left: 0; } -.glyphicon-leaf:before { - content: "\e103"; +.input-sm { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; } -.glyphicon-fire:before { - content: "\e104"; +select.input-sm { + height: 30px; + line-height: 30px; } -.glyphicon-eye-open:before { - content: "\e105"; +textarea.input-sm, +select[multiple].input-sm { + height: auto; } -.glyphicon-eye-close:before { - content: "\e106"; +.form-group-sm .form-control { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; } -.glyphicon-warning-sign:before { - content: "\e107"; +select.form-group-sm .form-control { + height: 30px; + line-height: 30px; } -.glyphicon-plane:before { - content: "\e108"; +textarea.form-group-sm .form-control, +select[multiple].form-group-sm .form-control { + height: auto; } -.glyphicon-calendar:before { - content: "\e109"; +.input-lg { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; } -.glyphicon-random:before { - content: "\e110"; +select.input-lg { + height: 46px; + line-height: 46px; } -.glyphicon-comment:before { - content: "\e111"; +textarea.input-lg, +select[multiple].input-lg { + height: auto; } -.glyphicon-magnet:before { - content: "\e112"; +.form-group-lg .form-control { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; } -.glyphicon-chevron-up:before { - content: "\e113"; +select.form-group-lg .form-control { + height: 46px; + line-height: 46px; } -.glyphicon-chevron-down:before { - content: "\e114"; +textarea.form-group-lg .form-control, +select[multiple].form-group-lg .form-control { + height: auto; } -.glyphicon-retweet:before { - content: "\e115"; +.has-feedback { + position: relative; } -.glyphicon-shopping-cart:before { - content: "\e116"; +.has-feedback .form-control { + padding-right: 42.5px; } -.glyphicon-folder-close:before { - content: "\e117"; +.form-control-feedback { + position: absolute; + top: 0; + right: 0; + z-index: 2; + display: block; + width: 34px; + height: 34px; + line-height: 34px; + text-align: center; + pointer-events: none; } -.glyphicon-folder-open:before { - content: "\e118"; +.input-lg + .form-control-feedback { + width: 46px; + height: 46px; + line-height: 46px; } -.glyphicon-resize-vertical:before { - content: "\e119"; +.input-sm + .form-control-feedback { + width: 30px; + height: 30px; + line-height: 30px; } -.glyphicon-resize-horizontal:before { - content: "\e120"; +.has-success .help-block, +.has-success .control-label, +.has-success .radio, +.has-success .checkbox, +.has-success .radio-inline, +.has-success .checkbox-inline, +.has-success.radio label, +.has-success.checkbox label, +.has-success.radio-inline label, +.has-success.checkbox-inline label { + color: #3c763d; } -.glyphicon-hdd:before { - content: "\e121"; +.has-success .form-control { + border-color: #3c763d; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); } -.glyphicon-bullhorn:before { - content: "\e122"; +.has-success .form-control:focus { + border-color: #2b542c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168; } -.glyphicon-bell:before { - content: "\e123"; +.has-success .input-group-addon { + color: #3c763d; + background-color: #dff0d8; + border-color: #3c763d; } -.glyphicon-certificate:before { - content: "\e124"; +.has-success .form-control-feedback { + color: #3c763d; } -.glyphicon-thumbs-up:before { - content: "\e125"; +.has-warning .help-block, +.has-warning .control-label, +.has-warning .radio, +.has-warning .checkbox, +.has-warning .radio-inline, +.has-warning .checkbox-inline, +.has-warning.radio label, +.has-warning.checkbox label, +.has-warning.radio-inline label, +.has-warning.checkbox-inline label { + color: #8a6d3b; } -.glyphicon-thumbs-down:before { - content: "\e126"; +.has-warning .form-control { + border-color: #8a6d3b; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); } -.glyphicon-hand-right:before { - content: "\e127"; +.has-warning .form-control:focus { + border-color: #66512c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b; } -.glyphicon-hand-left:before { - content: "\e128"; +.has-warning .input-group-addon { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #8a6d3b; } -.glyphicon-hand-up:before { - content: "\e129"; +.has-warning .form-control-feedback { + color: #8a6d3b; } -.glyphicon-hand-down:before { - content: "\e130"; +.has-error .help-block, +.has-error .control-label, +.has-error .radio, +.has-error .checkbox, +.has-error .radio-inline, +.has-error .checkbox-inline, +.has-error.radio label, +.has-error.checkbox label, +.has-error.radio-inline label, +.has-error.checkbox-inline label { + color: #a94442; } -.glyphicon-circle-arrow-right:before { - content: "\e131"; +.has-error .form-control { + border-color: #a94442; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); } -.glyphicon-circle-arrow-left:before { - content: "\e132"; +.has-error .form-control:focus { + border-color: #843534; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; } -.glyphicon-circle-arrow-up:before { - content: "\e133"; +.has-error .input-group-addon { + color: #a94442; + background-color: #f2dede; + border-color: #a94442; } -.glyphicon-circle-arrow-down:before { - content: "\e134"; +.has-error .form-control-feedback { + color: #a94442; } -.glyphicon-globe:before { - content: "\e135"; +.has-feedback label ~ .form-control-feedback { + top: 25px; } -.glyphicon-wrench:before { - content: "\e136"; +.has-feedback label.sr-only ~ .form-control-feedback { + top: 0; } -.glyphicon-tasks:before { - content: "\e137"; +.help-block { + display: block; + margin-top: 5px; + margin-bottom: 10px; + color: #737373; } -.glyphicon-filter:before { - content: "\e138"; +@media (min-width: 768px) { + .form-inline .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + .form-inline .form-control-static { + display: inline-block; + } + .form-inline .input-group { + display: inline-table; + vertical-align: middle; + } + .form-inline .input-group .input-group-addon, + .form-inline .input-group .input-group-btn, + .form-inline .input-group .form-control { + width: auto; + } + .form-inline .input-group > .form-control { + width: 100%; + } + .form-inline .control-label { + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .radio, + .form-inline .checkbox { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .radio label, + .form-inline .checkbox label { + padding-left: 0; + } + .form-inline .radio input[type="radio"], + .form-inline .checkbox input[type="checkbox"] { + position: relative; + margin-left: 0; + } + .form-inline .has-feedback .form-control-feedback { + top: 0; + } } -.glyphicon-briefcase:before { - content: "\e139"; +.form-horizontal .radio, +.form-horizontal .checkbox, +.form-horizontal .radio-inline, +.form-horizontal .checkbox-inline { + padding-top: 7px; + margin-top: 0; + margin-bottom: 0; } -.glyphicon-fullscreen:before { - content: "\e140"; +.form-horizontal .radio, +.form-horizontal .checkbox { + min-height: 27px; } -.glyphicon-dashboard:before { - content: "\e141"; +.form-horizontal .form-group { + margin-right: -15px; + margin-left: -15px; } -.glyphicon-paperclip:before { - content: "\e142"; +@media (min-width: 768px) { + .form-horizontal .control-label { + padding-top: 7px; + margin-bottom: 0; + text-align: right; + } } -.glyphicon-heart-empty:before { - content: "\e143"; +.form-horizontal .has-feedback .form-control-feedback { + right: 15px; } -.glyphicon-link:before { - content: "\e144"; +@media (min-width: 768px) { + .form-horizontal .form-group-lg .control-label { + padding-top: 14.3px; + } } -.glyphicon-phone:before { - content: "\e145"; +@media (min-width: 768px) { + .form-horizontal .form-group-sm .control-label { + padding-top: 6px; + } } -.glyphicon-pushpin:before { - content: "\e146"; +.btn { + display: inline-block; + padding: 6px 12px; + margin-bottom: 0; + font-size: 14px; + font-weight: normal; + line-height: 1.42857143; + text-align: center; + white-space: nowrap; + vertical-align: middle; + -ms-touch-action: manipulation; + touch-action: manipulation; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; } -.glyphicon-usd:before { - content: "\e148"; +.btn:focus, +.btn:active:focus, +.btn.active:focus, +.btn.focus, +.btn:active.focus, +.btn.active.focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; } -.glyphicon-gbp:before { - content: "\e149"; +.btn:hover, +.btn:focus, +.btn.focus { + color: #333; + text-decoration: none; } -.glyphicon-sort:before { - content: "\e150"; +.btn:active, +.btn.active { + background-image: none; + outline: 0; + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); } -.glyphicon-sort-by-alphabet:before { - content: "\e151"; +.btn.disabled, +.btn[disabled], +fieldset[disabled] .btn { + pointer-events: none; + cursor: not-allowed; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + box-shadow: none; + opacity: .65; } -.glyphicon-sort-by-alphabet-alt:before { - content: "\e152"; +.btn-default { + color: #333; + background-color: #fff; + border-color: #ccc; } -.glyphicon-sort-by-order:before { - content: "\e153"; +.btn-default:hover, +.btn-default:focus, +.btn-default.focus, +.btn-default:active, +.btn-default.active, +.open > .dropdown-toggle.btn-default { + color: #333; + background-color: #e6e6e6; + border-color: #adadad; } -.glyphicon-sort-by-order-alt:before { - content: "\e154"; +.btn-default:active, +.btn-default.active, +.open > .dropdown-toggle.btn-default { + background-image: none; } -.glyphicon-sort-by-attributes:before { - content: "\e155"; +.btn-default.disabled, +.btn-default[disabled], +fieldset[disabled] .btn-default, +.btn-default.disabled:hover, +.btn-default[disabled]:hover, +fieldset[disabled] .btn-default:hover, +.btn-default.disabled:focus, +.btn-default[disabled]:focus, +fieldset[disabled] .btn-default:focus, +.btn-default.disabled.focus, +.btn-default[disabled].focus, +fieldset[disabled] .btn-default.focus, +.btn-default.disabled:active, +.btn-default[disabled]:active, +fieldset[disabled] .btn-default:active, +.btn-default.disabled.active, +.btn-default[disabled].active, +fieldset[disabled] .btn-default.active { + background-color: #fff; + border-color: #ccc; } -.glyphicon-sort-by-attributes-alt:before { - content: "\e156"; +.btn-default .badge { + color: #fff; + background-color: #333; } -.glyphicon-unchecked:before { - content: "\e157"; +.btn-primary { + color: #fff; + background-color: #337ab7; + border-color: #2e6da4; } -.glyphicon-expand:before { - content: "\e158"; +.btn-primary:hover, +.btn-primary:focus, +.btn-primary.focus, +.btn-primary:active, +.btn-primary.active, +.open > .dropdown-toggle.btn-primary { + color: #fff; + background-color: #286090; + border-color: #204d74; } -.glyphicon-collapse-down:before { - content: "\e159"; +.btn-primary:active, +.btn-primary.active, +.open > .dropdown-toggle.btn-primary { + background-image: none; } -.glyphicon-collapse-up:before { - content: "\e160"; +.btn-primary.disabled, +.btn-primary[disabled], +fieldset[disabled] .btn-primary, +.btn-primary.disabled:hover, +.btn-primary[disabled]:hover, +fieldset[disabled] .btn-primary:hover, +.btn-primary.disabled:focus, +.btn-primary[disabled]:focus, +fieldset[disabled] .btn-primary:focus, +.btn-primary.disabled.focus, +.btn-primary[disabled].focus, +fieldset[disabled] .btn-primary.focus, +.btn-primary.disabled:active, +.btn-primary[disabled]:active, +fieldset[disabled] .btn-primary:active, +.btn-primary.disabled.active, +.btn-primary[disabled].active, +fieldset[disabled] .btn-primary.active { + background-color: #337ab7; + border-color: #2e6da4; } -.glyphicon-log-in:before { - content: "\e161"; +.btn-primary .badge { + color: #337ab7; + background-color: #fff; } -.glyphicon-flash:before { - content: "\e162"; +.btn-success { + color: #fff; + background-color: #5cb85c; + border-color: #4cae4c; } -.glyphicon-log-out:before { - content: "\e163"; +.btn-success:hover, +.btn-success:focus, +.btn-success.focus, +.btn-success:active, +.btn-success.active, +.open > .dropdown-toggle.btn-success { + color: #fff; + background-color: #449d44; + border-color: #398439; } -.glyphicon-new-window:before { - content: "\e164"; +.btn-success:active, +.btn-success.active, +.open > .dropdown-toggle.btn-success { + background-image: none; } -.glyphicon-record:before { - content: "\e165"; +.btn-success.disabled, +.btn-success[disabled], +fieldset[disabled] .btn-success, +.btn-success.disabled:hover, +.btn-success[disabled]:hover, +fieldset[disabled] .btn-success:hover, +.btn-success.disabled:focus, +.btn-success[disabled]:focus, +fieldset[disabled] .btn-success:focus, +.btn-success.disabled.focus, +.btn-success[disabled].focus, +fieldset[disabled] .btn-success.focus, +.btn-success.disabled:active, +.btn-success[disabled]:active, +fieldset[disabled] .btn-success:active, +.btn-success.disabled.active, +.btn-success[disabled].active, +fieldset[disabled] .btn-success.active { + background-color: #5cb85c; + border-color: #4cae4c; } -.glyphicon-save:before { - content: "\e166"; +.btn-success .badge { + color: #5cb85c; + background-color: #fff; } -.glyphicon-open:before { - content: "\e167"; +.btn-info { + color: #fff; + background-color: #5bc0de; + border-color: #46b8da; } -.glyphicon-saved:before { - content: "\e168"; +.btn-info:hover, +.btn-info:focus, +.btn-info.focus, +.btn-info:active, +.btn-info.active, +.open > .dropdown-toggle.btn-info { + color: #fff; + background-color: #31b0d5; + border-color: #269abc; } -.glyphicon-import:before { - content: "\e169"; +.btn-info:active, +.btn-info.active, +.open > .dropdown-toggle.btn-info { + background-image: none; } -.glyphicon-export:before { - content: "\e170"; +.btn-info.disabled, +.btn-info[disabled], +fieldset[disabled] .btn-info, +.btn-info.disabled:hover, +.btn-info[disabled]:hover, +fieldset[disabled] .btn-info:hover, +.btn-info.disabled:focus, +.btn-info[disabled]:focus, +fieldset[disabled] .btn-info:focus, +.btn-info.disabled.focus, +.btn-info[disabled].focus, +fieldset[disabled] .btn-info.focus, +.btn-info.disabled:active, +.btn-info[disabled]:active, +fieldset[disabled] .btn-info:active, +.btn-info.disabled.active, +.btn-info[disabled].active, +fieldset[disabled] .btn-info.active { + background-color: #5bc0de; + border-color: #46b8da; } -.glyphicon-send:before { - content: "\e171"; +.btn-info .badge { + color: #5bc0de; + background-color: #fff; } -.glyphicon-floppy-disk:before { - content: "\e172"; +.btn-warning { + color: #fff; + background-color: #f0ad4e; + border-color: #eea236; } -.glyphicon-floppy-saved:before { - content: "\e173"; +.btn-warning:hover, +.btn-warning:focus, +.btn-warning.focus, +.btn-warning:active, +.btn-warning.active, +.open > .dropdown-toggle.btn-warning { + color: #fff; + background-color: #ec971f; + border-color: #d58512; } -.glyphicon-floppy-remove:before { - content: "\e174"; +.btn-warning:active, +.btn-warning.active, +.open > .dropdown-toggle.btn-warning { + background-image: none; } -.glyphicon-floppy-save:before { - content: "\e175"; +.btn-warning.disabled, +.btn-warning[disabled], +fieldset[disabled] .btn-warning, +.btn-warning.disabled:hover, +.btn-warning[disabled]:hover, +fieldset[disabled] .btn-warning:hover, +.btn-warning.disabled:focus, +.btn-warning[disabled]:focus, +fieldset[disabled] .btn-warning:focus, +.btn-warning.disabled.focus, +.btn-warning[disabled].focus, +fieldset[disabled] .btn-warning.focus, +.btn-warning.disabled:active, +.btn-warning[disabled]:active, +fieldset[disabled] .btn-warning:active, +.btn-warning.disabled.active, +.btn-warning[disabled].active, +fieldset[disabled] .btn-warning.active { + background-color: #f0ad4e; + border-color: #eea236; } -.glyphicon-floppy-open:before { - content: "\e176"; +.btn-warning .badge { + color: #f0ad4e; + background-color: #fff; } -.glyphicon-credit-card:before { - content: "\e177"; +.btn-danger { + color: #fff; + background-color: #d9534f; + border-color: #d43f3a; } -.glyphicon-transfer:before { - content: "\e178"; +.btn-danger:hover, +.btn-danger:focus, +.btn-danger.focus, +.btn-danger:active, +.btn-danger.active, +.open > .dropdown-toggle.btn-danger { + color: #fff; + background-color: #c9302c; + border-color: #ac2925; } -.glyphicon-cutlery:before { - content: "\e179"; +.btn-danger:active, +.btn-danger.active, +.open > .dropdown-toggle.btn-danger { + background-image: none; } -.glyphicon-header:before { - content: "\e180"; +.btn-danger.disabled, +.btn-danger[disabled], +fieldset[disabled] .btn-danger, +.btn-danger.disabled:hover, +.btn-danger[disabled]:hover, +fieldset[disabled] .btn-danger:hover, +.btn-danger.disabled:focus, +.btn-danger[disabled]:focus, +fieldset[disabled] .btn-danger:focus, +.btn-danger.disabled.focus, +.btn-danger[disabled].focus, +fieldset[disabled] .btn-danger.focus, +.btn-danger.disabled:active, +.btn-danger[disabled]:active, +fieldset[disabled] .btn-danger:active, +.btn-danger.disabled.active, +.btn-danger[disabled].active, +fieldset[disabled] .btn-danger.active { + background-color: #d9534f; + border-color: #d43f3a; } -.glyphicon-compressed:before { - content: "\e181"; +.btn-danger .badge { + color: #d9534f; + background-color: #fff; } -.glyphicon-earphone:before { - content: "\e182"; +.btn-link { + font-weight: normal; + color: #337ab7; + border-radius: 0; } -.glyphicon-phone-alt:before { - content: "\e183"; +.btn-link, +.btn-link:active, +.btn-link.active, +.btn-link[disabled], +fieldset[disabled] .btn-link { + background-color: transparent; + -webkit-box-shadow: none; + box-shadow: none; } -.glyphicon-tower:before { - content: "\e184"; +.btn-link, +.btn-link:hover, +.btn-link:focus, +.btn-link:active { + border-color: transparent; } -.glyphicon-stats:before { - content: "\e185"; +.btn-link:hover, +.btn-link:focus { + color: #23527c; + text-decoration: underline; + background-color: transparent; } -.glyphicon-sd-video:before { - content: "\e186"; +.btn-link[disabled]:hover, +fieldset[disabled] .btn-link:hover, +.btn-link[disabled]:focus, +fieldset[disabled] .btn-link:focus { + color: #777; + text-decoration: none; } -.glyphicon-hd-video:before { - content: "\e187"; +.btn-lg, +.btn-group-lg > .btn { + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; } -.glyphicon-subtitles:before { - content: "\e188"; +.btn-sm, +.btn-group-sm > .btn { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; } -.glyphicon-sound-stereo:before { - content: "\e189"; +.btn-xs, +.btn-group-xs > .btn { + padding: 1px 5px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; } -.glyphicon-sound-dolby:before { - content: "\e190"; +.btn-block { + display: block; + width: 100%; } -.glyphicon-sound-5-1:before { - content: "\e191"; +.btn-block + .btn-block { + margin-top: 5px; } -.glyphicon-sound-6-1:before { - content: "\e192"; +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; } -.glyphicon-sound-7-1:before { - content: "\e193"; +.fade { + opacity: 0; + -webkit-transition: opacity .15s linear; + -o-transition: opacity .15s linear; + transition: opacity .15s linear; } -.glyphicon-copyright-mark:before { - content: "\e194"; +.fade.in { + opacity: 1; } -.glyphicon-registration-mark:before { - content: "\e195"; +.collapse { + display: none; + visibility: hidden; } -.glyphicon-cloud-download:before { - content: "\e197"; +.collapse.in { + display: block; + visibility: visible; } -.glyphicon-cloud-upload:before { - content: "\e198"; +tr.collapse.in { + display: table-row; } -.glyphicon-tree-conifer:before { - content: "\e199"; +tbody.collapse.in { + display: table-row-group; } -.glyphicon-tree-deciduous:before { - content: "\e200"; +.collapsing { + position: relative; + height: 0; + overflow: hidden; + -webkit-transition-timing-function: ease; + -o-transition-timing-function: ease; + transition-timing-function: ease; + -webkit-transition-duration: .35s; + -o-transition-duration: .35s; + transition-duration: .35s; + -webkit-transition-property: height, visibility; + -o-transition-property: height, visibility; + transition-property: height, visibility; } .caret { display: inline-block; @@ -2912,9 +3404,11 @@ input[type="button"].btn-block { padding: 5px 0; margin: 2px 0 0; font-size: 14px; + text-align: left; list-style: none; background-color: #fff; - background-clip: padding-box; + -webkit-background-clip: padding-box; + background-clip: padding-box; border: 1px solid #ccc; border: 1px solid rgba(0, 0, 0, .15); border-radius: 4px; @@ -2936,7 +3430,7 @@ input[type="button"].btn-block { padding: 3px 20px; clear: both; font-weight: normal; - line-height: 1.428571429; + line-height: 1.42857143; color: #333; white-space: nowrap; } @@ -2951,21 +3445,21 @@ input[type="button"].btn-block { .dropdown-menu > .active > a:focus { color: #fff; text-decoration: none; - background-color: #428bca; + background-color: #337ab7; outline: 0; } .dropdown-menu > .disabled > a, .dropdown-menu > .disabled > a:hover, .dropdown-menu > .disabled > a:focus { - color: #999; + color: #777; } .dropdown-menu > .disabled > a:hover, .dropdown-menu > .disabled > a:focus { text-decoration: none; - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); cursor: not-allowed; background-color: transparent; background-image: none; + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); } .open > .dropdown-menu { display: block; @@ -2985,8 +3479,9 @@ input[type="button"].btn-block { display: block; padding: 3px 20px; font-size: 12px; - line-height: 1.428571429; - color: #999; + line-height: 1.42857143; + color: #777; + white-space: nowrap; } .dropdown-backdrop { position: fixed; @@ -3010,7 +3505,7 @@ input[type="button"].btn-block { .navbar-fixed-bottom .dropdown .dropdown-menu { top: auto; bottom: 100%; - margin-bottom: 1px; + margin-bottom: 2px; } @media (min-width: 768px) { .navbar-right .dropdown-menu { @@ -3043,10 +3538,6 @@ input[type="button"].btn-block { .btn-group-vertical > .btn.active { z-index: 2; } -.btn-group > .btn:focus, -.btn-group-vertical > .btn:focus { - outline: none; -} .btn-group .btn + .btn, .btn-group .btn + .btn-group, .btn-group .btn-group + .btn, @@ -3099,24 +3590,6 @@ input[type="button"].btn-block { .btn-group.open .dropdown-toggle { outline: 0; } -.btn-group-xs > .btn { - padding: 1px 5px; - font-size: 12px; - line-height: 1.5; - border-radius: 3px; -} -.btn-group-sm > .btn { - padding: 5px 10px; - font-size: 12px; - line-height: 1.5; - border-radius: 3px; -} -.btn-group-lg > .btn { - padding: 10px 16px; - font-size: 18px; - line-height: 1.33; - border-radius: 6px; -} .btn-group > .btn + .dropdown-toggle { padding-right: 8px; padding-left: 8px; @@ -3177,12 +3650,12 @@ input[type="button"].btn-block { .btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { border-radius: 0; } -.btn-group-vertical > .btn-group:first-child > .btn:last-child, -.btn-group-vertical > .btn-group:first-child > .dropdown-toggle { +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child, +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle { border-bottom-right-radius: 0; border-bottom-left-radius: 0; } -.btn-group-vertical > .btn-group:last-child > .btn:first-child { +.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { border-top-left-radius: 0; border-top-right-radius: 0; } @@ -3201,9 +3674,16 @@ input[type="button"].btn-block { .btn-group-justified > .btn-group .btn { width: 100%; } -[data-toggle="buttons"] > .btn > input[type="radio"], -[data-toggle="buttons"] > .btn > input[type="checkbox"] { - display: none; +.btn-group-justified > .btn-group .dropdown-menu { + left: auto; +} +[data-toggle="buttons"] > .btn input[type="radio"], +[data-toggle="buttons"] > .btn-group > .btn input[type="radio"], +[data-toggle="buttons"] > .btn input[type="checkbox"], +[data-toggle="buttons"] > .btn-group > .btn input[type="checkbox"] { + position: absolute; + clip: rect(0, 0, 0, 0); + pointer-events: none; } .input-group { position: relative; @@ -3216,6 +3696,8 @@ input[type="button"].btn-block { padding-left: 0; } .input-group .form-control { + position: relative; + z-index: 2; float: left; width: 100%; margin-bottom: 0; @@ -3237,7 +3719,10 @@ select.input-group-lg > .input-group-btn > .btn { } textarea.input-group-lg > .form-control, textarea.input-group-lg > .input-group-addon, -textarea.input-group-lg > .input-group-btn > .btn { +textarea.input-group-lg > .input-group-btn > .btn, +select[multiple].input-group-lg > .form-control, +select[multiple].input-group-lg > .input-group-addon, +select[multiple].input-group-lg > .input-group-btn > .btn { height: auto; } .input-group-sm > .form-control, @@ -3257,7 +3742,10 @@ select.input-group-sm > .input-group-btn > .btn { } textarea.input-group-sm > .form-control, textarea.input-group-sm > .input-group-addon, -textarea.input-group-sm > .input-group-btn > .btn { +textarea.input-group-sm > .input-group-btn > .btn, +select[multiple].input-group-sm > .form-control, +select[multiple].input-group-sm > .input-group-addon, +select[multiple].input-group-sm > .input-group-btn > .btn { height: auto; } .input-group-addon, @@ -3304,8 +3792,10 @@ textarea.input-group-sm > .input-group-btn > .btn { .input-group .form-control:first-child, .input-group-addon:first-child, .input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group > .btn, .input-group-btn:first-child > .dropdown-toggle, -.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle) { +.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), +.input-group-btn:last-child > .btn-group:not(:last-child) > .btn { border-top-right-radius: 0; border-bottom-right-radius: 0; } @@ -3315,8 +3805,10 @@ textarea.input-group-sm > .input-group-btn > .btn { .input-group .form-control:last-child, .input-group-addon:last-child, .input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group > .btn, .input-group-btn:last-child > .dropdown-toggle, -.input-group-btn:first-child > .btn:not(:first-child) { +.input-group-btn:first-child > .btn:not(:first-child), +.input-group-btn:first-child > .btn-group:not(:first-child) > .btn { border-top-left-radius: 0; border-bottom-left-radius: 0; } @@ -3325,30 +3817,28 @@ textarea.input-group-sm > .input-group-btn > .btn { } .input-group-btn { position: relative; + font-size: 0; white-space: nowrap; } -.input-group-btn:first-child > .btn { - margin-right: -1px; -} -.input-group-btn:last-child > .btn { - margin-left: -1px; -} .input-group-btn > .btn { position: relative; } -.input-group-btn > .btn:not(:first-of-type):not(:last-of-type) { - border-radius: 0; -} .input-group-btn > .btn + .btn { - margin-left: -4px; -} -.input-group-btn > .btn + .btn:last-of-type { - margin-left: -5px; + margin-left: -1px; } .input-group-btn > .btn:hover, +.input-group-btn > .btn:focus, .input-group-btn > .btn:active { z-index: 2; } +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group { + margin-right: -1px; +} +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group { + margin-left: -1px; +} .nav { padding-left: 0; margin-bottom: 0; @@ -3369,11 +3859,11 @@ textarea.input-group-sm > .input-group-btn > .btn { background-color: #eee; } .nav > li.disabled > a { - color: #999; + color: #777; } .nav > li.disabled > a:hover, .nav > li.disabled > a:focus { - color: #999; + color: #777; text-decoration: none; cursor: not-allowed; background-color: transparent; @@ -3382,7 +3872,7 @@ textarea.input-group-sm > .input-group-btn > .btn { .nav .open > a:hover, .nav .open > a:focus { background-color: #eee; - border-color: #428bca; + border-color: #337ab7; } .nav .nav-divider { height: 1px; @@ -3402,7 +3892,7 @@ textarea.input-group-sm > .input-group-btn > .btn { } .nav-tabs > li > a { margin-right: 2px; - line-height: 1.428571429; + line-height: 1.42857143; border: 1px solid transparent; border-radius: 4px 4px 0 0; } @@ -3475,7 +3965,7 @@ textarea.input-group-sm > .input-group-btn > .btn { .nav-pills > li.active > a:hover, .nav-pills > li.active > a:focus { color: #fff; - background-color: #428bca; + background-color: #337ab7; } .nav-stacked > li { float: none; @@ -3532,9 +4022,11 @@ textarea.input-group-sm > .input-group-btn > .btn { } .tab-content > .tab-pane { display: none; + visibility: hidden; } .tab-content > .active { display: block; + visibility: visible; } .nav-tabs .dropdown-menu { margin-top: -1px; @@ -3558,13 +4050,13 @@ textarea.input-group-sm > .input-group-btn > .btn { } } .navbar-collapse { - max-height: 340px; padding-right: 15px; padding-left: 15px; overflow-x: visible; - border-top: 1px solid transparent; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1); -webkit-overflow-scrolling: touch; + border-top: 1px solid transparent; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1); } .navbar-collapse.in { overflow-y: auto; @@ -3573,13 +4065,15 @@ textarea.input-group-sm > .input-group-btn > .btn { .navbar-collapse { width: auto; border-top: 0; - box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; } .navbar-collapse.collapse { display: block !important; height: auto !important; padding-bottom: 0; overflow: visible !important; + visibility: visible !important; } .navbar-collapse.in { overflow-y: visible; @@ -3591,6 +4085,16 @@ textarea.input-group-sm > .input-group-btn > .btn { padding-left: 0; } } +.navbar-fixed-top .navbar-collapse, +.navbar-fixed-bottom .navbar-collapse { + max-height: 340px; +} +@media (max-device-width: 480px) and (orientation: landscape) { + .navbar-fixed-top .navbar-collapse, + .navbar-fixed-bottom .navbar-collapse { + max-height: 200px; + } +} .container > .navbar-header, .container-fluid > .navbar-header, .container > .navbar-collapse, @@ -3640,6 +4144,7 @@ textarea.input-group-sm > .input-group-btn > .btn { } .navbar-brand { float: left; + height: 50px; padding: 15px 15px; font-size: 18px; line-height: 20px; @@ -3648,10 +4153,8 @@ textarea.input-group-sm > .input-group-btn > .btn { .navbar-brand:focus { text-decoration: none; } -.navbar-brand > .glyphicon { - float: left; - margin-top: -2px; - margin-right: 5px; +.navbar-brand > img { + display: block; } @media (min-width: 768px) { .navbar > .container .navbar-brand, @@ -3671,6 +4174,9 @@ textarea.input-group-sm > .input-group-btn > .btn { border: 1px solid transparent; border-radius: 4px; } +.navbar-toggle:focus { + outline: 0; +} .navbar-toggle .icon-bar { display: block; width: 22px; @@ -3701,7 +4207,8 @@ textarea.input-group-sm > .input-group-btn > .btn { margin-top: 0; background-color: transparent; border: 0; - box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; } .navbar-nav .open .dropdown-menu > li > a, .navbar-nav .open .dropdown-menu .dropdown-header { @@ -3727,17 +4234,6 @@ textarea.input-group-sm > .input-group-btn > .btn { padding-top: 15px; padding-bottom: 15px; } - .navbar-nav.navbar-right:last-child { - margin-right: -15px; - } -} -@media (min-width: 768px) { - .navbar-left { - float: left !important; - } - .navbar-right { - float: right !important; - } } .navbar-form { padding: 10px 15px; @@ -3758,29 +4254,55 @@ textarea.input-group-sm > .input-group-btn > .btn { } .navbar-form .form-control { display: inline-block; + width: auto; + vertical-align: middle; + } + .navbar-form .form-control-static { + display: inline-block; + } + .navbar-form .input-group { + display: inline-table; vertical-align: middle; } - .navbar-form select.form-control { + .navbar-form .input-group .input-group-addon, + .navbar-form .input-group .input-group-btn, + .navbar-form .input-group .form-control { width: auto; } + .navbar-form .input-group > .form-control { + width: 100%; + } + .navbar-form .control-label { + margin-bottom: 0; + vertical-align: middle; + } .navbar-form .radio, .navbar-form .checkbox { display: inline-block; - padding-left: 0; margin-top: 0; margin-bottom: 0; vertical-align: middle; } + .navbar-form .radio label, + .navbar-form .checkbox label { + padding-left: 0; + } .navbar-form .radio input[type="radio"], .navbar-form .checkbox input[type="checkbox"] { - float: none; + position: relative; margin-left: 0; } + .navbar-form .has-feedback .form-control-feedback { + top: 0; + } } @media (max-width: 767px) { .navbar-form .form-group { margin-bottom: 5px; } + .navbar-form .form-group:last-child { + margin-bottom: 0; + } } @media (min-width: 768px) { .navbar-form { @@ -3793,9 +4315,6 @@ textarea.input-group-sm > .input-group-btn > .btn { -webkit-box-shadow: none; box-shadow: none; } - .navbar-form.navbar-right:last-child { - margin-right: -15px; - } } .navbar-nav > li > .dropdown-menu { margin-top: 0; @@ -3803,6 +4322,8 @@ textarea.input-group-sm > .input-group-btn > .btn { border-top-right-radius: 0; } .navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { + border-top-left-radius: 4px; + border-top-right-radius: 4px; border-bottom-right-radius: 0; border-bottom-left-radius: 0; } @@ -3828,7 +4349,16 @@ textarea.input-group-sm > .input-group-btn > .btn { margin-right: 15px; margin-left: 15px; } - .navbar-text.navbar-right:last-child { +} +@media (min-width: 768px) { + .navbar-left { + float: left !important; + } + .navbar-right { + float: right !important; + margin-right: -15px; + } + .navbar-right ~ .navbar-right { margin-right: 0; } } @@ -3915,12 +4445,25 @@ textarea.input-group-sm > .input-group-btn > .btn { .navbar-default .navbar-link:hover { color: #333; } +.navbar-default .btn-link { + color: #777; +} +.navbar-default .btn-link:hover, +.navbar-default .btn-link:focus { + color: #333; +} +.navbar-default .btn-link[disabled]:hover, +fieldset[disabled] .navbar-default .btn-link:hover, +.navbar-default .btn-link[disabled]:focus, +fieldset[disabled] .navbar-default .btn-link:focus { + color: #ccc; +} .navbar-inverse { background-color: #222; border-color: #080808; } .navbar-inverse .navbar-brand { - color: #999; + color: #9d9d9d; } .navbar-inverse .navbar-brand:hover, .navbar-inverse .navbar-brand:focus { @@ -3928,10 +4471,10 @@ textarea.input-group-sm > .input-group-btn > .btn { background-color: transparent; } .navbar-inverse .navbar-text { - color: #999; + color: #9d9d9d; } .navbar-inverse .navbar-nav > li > a { - color: #999; + color: #9d9d9d; } .navbar-inverse .navbar-nav > li > a:hover, .navbar-inverse .navbar-nav > li > a:focus { @@ -3978,7 +4521,7 @@ textarea.input-group-sm > .input-group-btn > .btn { background-color: #080808; } .navbar-inverse .navbar-nav .open .dropdown-menu > li > a { - color: #999; + color: #9d9d9d; } .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus { @@ -3999,11 +4542,24 @@ textarea.input-group-sm > .input-group-btn > .btn { } } .navbar-inverse .navbar-link { - color: #999; + color: #9d9d9d; } .navbar-inverse .navbar-link:hover { color: #fff; } +.navbar-inverse .btn-link { + color: #9d9d9d; +} +.navbar-inverse .btn-link:hover, +.navbar-inverse .btn-link:focus { + color: #fff; +} +.navbar-inverse .btn-link[disabled]:hover, +fieldset[disabled] .navbar-inverse .btn-link:hover, +.navbar-inverse .btn-link[disabled]:focus, +fieldset[disabled] .navbar-inverse .btn-link:focus { + color: #444; +} .breadcrumb { padding: 8px 15px; margin-bottom: 20px; @@ -4020,7 +4576,7 @@ textarea.input-group-sm > .input-group-btn > .btn { content: "/\00a0"; } .breadcrumb > .active { - color: #999; + color: #777; } .pagination { display: inline-block; @@ -4037,7 +4593,8 @@ textarea.input-group-sm > .input-group-btn > .btn { float: left; padding: 6px 12px; margin-left: -1px; - line-height: 1.428571429; + line-height: 1.42857143; + color: #337ab7; text-decoration: none; background-color: #fff; border: 1px solid #ddd; @@ -4057,7 +4614,9 @@ textarea.input-group-sm > .input-group-btn > .btn { .pagination > li > span:hover, .pagination > li > a:focus, .pagination > li > span:focus { + color: #23527c; background-color: #eee; + border-color: #ddd; } .pagination > .active > a, .pagination > .active > span, @@ -4068,8 +4627,8 @@ textarea.input-group-sm > .input-group-btn > .btn { z-index: 2; color: #fff; cursor: default; - background-color: #428bca; - border-color: #428bca; + background-color: #337ab7; + border-color: #337ab7; } .pagination > .disabled > span, .pagination > .disabled > span:hover, @@ -4077,7 +4636,7 @@ textarea.input-group-sm > .input-group-btn > .btn { .pagination > .disabled > a, .pagination > .disabled > a:hover, .pagination > .disabled > a:focus { - color: #999; + color: #777; cursor: not-allowed; background-color: #fff; border-color: #ddd; @@ -4146,7 +4705,7 @@ textarea.input-group-sm > .input-group-btn > .btn { .pager .disabled > a:hover, .pager .disabled > a:focus, .pager .disabled > span { - color: #999; + color: #777; cursor: not-allowed; background-color: #fff; } @@ -4162,8 +4721,8 @@ textarea.input-group-sm > .input-group-btn > .btn { vertical-align: baseline; border-radius: .25em; } -.label[href]:hover, -.label[href]:focus { +a.label:hover, +a.label:focus { color: #fff; text-decoration: none; cursor: pointer; @@ -4176,18 +4735,18 @@ textarea.input-group-sm > .input-group-btn > .btn { top: -1px; } .label-default { - background-color: #999; + background-color: #777; } .label-default[href]:hover, .label-default[href]:focus { - background-color: #808080; + background-color: #5e5e5e; } .label-primary { - background-color: #428bca; + background-color: #337ab7; } .label-primary[href]:hover, .label-primary[href]:focus { - background-color: #3071a9; + background-color: #286090; } .label-success { background-color: #5cb85c; @@ -4228,7 +4787,7 @@ textarea.input-group-sm > .input-group-btn > .btn { text-align: center; white-space: nowrap; vertical-align: baseline; - background-color: #999; + background-color: #777; border-radius: 10px; } .badge:empty { @@ -4238,22 +4797,32 @@ textarea.input-group-sm > .input-group-btn > .btn { position: relative; top: -1px; } +.btn-xs .badge { + top: 0; + padding: 1px 5px; +} a.badge:hover, a.badge:focus { color: #fff; text-decoration: none; cursor: pointer; } -a.list-group-item.active > .badge, +.list-group-item.active > .badge, .nav-pills > .active > a > .badge { - color: #428bca; + color: #337ab7; background-color: #fff; } +.list-group-item > .badge { + float: right; +} +.list-group-item > .badge + .badge { + margin-right: 5px; +} .nav-pills > li > a > .badge { margin-left: 3px; } .jumbotron { - padding: 30px; + padding: 30px 15px; margin-bottom: 30px; color: inherit; background-color: #eee; @@ -4267,7 +4836,11 @@ a.list-group-item.active > .badge, font-size: 21px; font-weight: 200; } -.container .jumbotron { +.jumbotron > hr { + border-top-color: #d5d5d5; +} +.container .jumbotron, +.container-fluid .jumbotron { border-radius: 6px; } .jumbotron .container { @@ -4275,10 +4848,10 @@ a.list-group-item.active > .badge, } @media screen and (min-width: 768px) { .jumbotron { - padding-top: 48px; - padding-bottom: 48px; + padding: 48px 0; } - .container .jumbotron { + .container .jumbotron, + .container-fluid .jumbotron { padding-right: 60px; padding-left: 60px; } @@ -4291,25 +4864,23 @@ a.list-group-item.active > .badge, display: block; padding: 4px; margin-bottom: 20px; - line-height: 1.428571429; + line-height: 1.42857143; background-color: #fff; border: 1px solid #ddd; border-radius: 4px; - -webkit-transition: all .2s ease-in-out; - transition: all .2s ease-in-out; + -webkit-transition: border .2s ease-in-out; + -o-transition: border .2s ease-in-out; + transition: border .2s ease-in-out; } .thumbnail > img, .thumbnail a > img { - display: block; - max-width: 100%; - height: auto; margin-right: auto; margin-left: auto; } a.thumbnail:hover, a.thumbnail:focus, a.thumbnail.active { - border-color: #428bca; + border-color: #337ab7; } .thumbnail .caption { padding: 9px; @@ -4335,10 +4906,12 @@ a.thumbnail.active { .alert > p + p { margin-top: 5px; } -.alert-dismissable { +.alert-dismissable, +.alert-dismissible { padding-right: 35px; } -.alert-dismissable .close { +.alert-dismissable .close, +.alert-dismissible .close { position: relative; top: -2px; right: -21px; @@ -4396,6 +4969,14 @@ a.thumbnail.active { background-position: 0 0; } } +@-o-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} @keyframes progress-bar-stripes { from { background-position: 40px 0; @@ -4421,19 +5002,25 @@ a.thumbnail.active { line-height: 20px; color: #fff; text-align: center; - background-color: #428bca; + background-color: #337ab7; -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); -webkit-transition: width .6s ease; + -o-transition: width .6s ease; transition: width .6s ease; } -.progress-striped .progress-bar { +.progress-striped .progress-bar, +.progress-bar-striped { background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-size: 40px 40px; + -webkit-background-size: 40px 40px; + background-size: 40px 40px; } -.progress.active .progress-bar { +.progress.active .progress-bar, +.progress-bar.active { -webkit-animation: progress-bar-stripes 2s linear infinite; + -o-animation: progress-bar-stripes 2s linear infinite; animation: progress-bar-stripes 2s linear infinite; } .progress-bar-success { @@ -4441,6 +5028,7 @@ a.thumbnail.active { } .progress-striped .progress-bar-success { background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); } .progress-bar-info { @@ -4448,6 +5036,7 @@ a.thumbnail.active { } .progress-striped .progress-bar-info { background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); } .progress-bar-warning { @@ -4455,6 +5044,7 @@ a.thumbnail.active { } .progress-striped .progress-bar-warning { background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); } .progress-bar-danger { @@ -4462,31 +5052,46 @@ a.thumbnail.active { } .progress-striped .progress-bar-danger { background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); } -.media, -.media-body { - overflow: hidden; - zoom: 1; -} -.media, -.media .media { +.media { margin-top: 15px; } .media:first-child { margin-top: 0; } +.media, +.media-body { + overflow: hidden; + zoom: 1; +} .media-object { display: block; } -.media-heading { - margin: 0 0 5px; +.media-right, +.media > .pull-right { + padding-left: 10px; } +.media-left, .media > .pull-left { - margin-right: 10px; + padding-right: 10px; } -.media > .pull-right { - margin-left: 10px; +.media-left, +.media-right, +.media-body { + display: table-cell; + vertical-align: top; +} +.media-middle { + vertical-align: middle; +} +.media-bottom { + vertical-align: bottom; +} +.media-heading { + margin-top: 0; + margin-bottom: 5px; } .media-list { padding-left: 0; @@ -4513,12 +5118,6 @@ a.thumbnail.active { border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; } -.list-group-item > .badge { - float: right; -} -.list-group-item > .badge + .badge { - margin-right: 5px; -} a.list-group-item { color: #555; } @@ -4527,26 +5126,50 @@ a.list-group-item .list-group-item-heading { } a.list-group-item:hover, a.list-group-item:focus { + color: #555; text-decoration: none; background-color: #f5f5f5; } -a.list-group-item.active, -a.list-group-item.active:hover, -a.list-group-item.active:focus { +.list-group-item.disabled, +.list-group-item.disabled:hover, +.list-group-item.disabled:focus { + color: #777; + cursor: not-allowed; + background-color: #eee; +} +.list-group-item.disabled .list-group-item-heading, +.list-group-item.disabled:hover .list-group-item-heading, +.list-group-item.disabled:focus .list-group-item-heading { + color: inherit; +} +.list-group-item.disabled .list-group-item-text, +.list-group-item.disabled:hover .list-group-item-text, +.list-group-item.disabled:focus .list-group-item-text { + color: #777; +} +.list-group-item.active, +.list-group-item.active:hover, +.list-group-item.active:focus { z-index: 2; color: #fff; - background-color: #428bca; - border-color: #428bca; -} -a.list-group-item.active .list-group-item-heading, -a.list-group-item.active:hover .list-group-item-heading, -a.list-group-item.active:focus .list-group-item-heading { + background-color: #337ab7; + border-color: #337ab7; +} +.list-group-item.active .list-group-item-heading, +.list-group-item.active:hover .list-group-item-heading, +.list-group-item.active:focus .list-group-item-heading, +.list-group-item.active .list-group-item-heading > small, +.list-group-item.active:hover .list-group-item-heading > small, +.list-group-item.active:focus .list-group-item-heading > small, +.list-group-item.active .list-group-item-heading > .small, +.list-group-item.active:hover .list-group-item-heading > .small, +.list-group-item.active:focus .list-group-item-heading > .small { color: inherit; } -a.list-group-item.active .list-group-item-text, -a.list-group-item.active:hover .list-group-item-text, -a.list-group-item.active:focus .list-group-item-text { - color: #e1edf7; +.list-group-item.active .list-group-item-text, +.list-group-item.active:hover .list-group-item-text, +.list-group-item.active:focus .list-group-item-text { + color: #c7ddef; } .list-group-item-success { color: #3c763d; @@ -4570,6 +5193,28 @@ a.list-group-item-success.active:focus { background-color: #3c763d; border-color: #3c763d; } +.list-group-item-info { + color: #31708f; + background-color: #d9edf7; +} +a.list-group-item-info { + color: #31708f; +} +a.list-group-item-info .list-group-item-heading { + color: inherit; +} +a.list-group-item-info:hover, +a.list-group-item-info:focus { + color: #31708f; + background-color: #c4e3f3; +} +a.list-group-item-info.active, +a.list-group-item-info.active:hover, +a.list-group-item-info.active:focus { + color: #fff; + background-color: #31708f; + border-color: #31708f; +} .list-group-item-warning { color: #8a6d3b; background-color: #fcf8e3; @@ -4614,28 +5259,6 @@ a.list-group-item-danger.active:focus { background-color: #a94442; border-color: #a94442; } -.list-group-item-info { - color: #31708f; - background-color: #d9edf7; -} -a.list-group-item-info { - color: #31708f; -} -a.list-group-item-info .list-group-item-heading { - color: inherit; -} -a.list-group-item-info:hover, -a.list-group-item-info:focus { - color: #31708f; - background-color: #c4e3f3; -} -a.list-group-item-info.active, -a.list-group-item-info.active:hover, -a.list-group-item-info.active:focus { - color: #fff; - background-color: #31708f; - border-color: #31708f; -} .list-group-item-heading { margin-top: 0; margin-bottom: 5px; @@ -4655,32 +5278,141 @@ a.list-group-item-info.active:focus { .panel-body { padding: 15px; } -.panel > .list-group { - margin-bottom: 0; +.panel-heading { + padding: 10px 15px; + border-bottom: 1px solid transparent; + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel-heading > .dropdown .dropdown-toggle { + color: inherit; +} +.panel-title { + margin-top: 0; + margin-bottom: 0; + font-size: 16px; + color: inherit; +} +.panel-title > a { + color: inherit; +} +.panel-footer { + padding: 10px 15px; + background-color: #f5f5f5; + border-top: 1px solid #ddd; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel > .list-group, +.panel > .panel-collapse > .list-group { + margin-bottom: 0; +} +.panel > .list-group .list-group-item, +.panel > .panel-collapse > .list-group .list-group-item { + border-width: 1px 0; + border-radius: 0; +} +.panel > .list-group:first-child .list-group-item:first-child, +.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child { + border-top: 0; + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel > .list-group:last-child .list-group-item:last-child, +.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child { + border-bottom: 0; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel-heading + .list-group .list-group-item:first-child { + border-top-width: 0; +} +.list-group + .panel-footer { + border-top-width: 0; +} +.panel > .table, +.panel > .table-responsive > .table, +.panel > .panel-collapse > .table { + margin-bottom: 0; +} +.panel > .table caption, +.panel > .table-responsive > .table caption, +.panel > .panel-collapse > .table caption { + padding-right: 15px; + padding-left: 15px; +} +.panel > .table:first-child, +.panel > .table-responsive:first-child > .table:first-child { + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel > .table:first-child > thead:first-child > tr:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child { + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel > .table:first-child > thead:first-child > tr:first-child td:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child, +.panel > .table:first-child > thead:first-child > tr:first-child th:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child { + border-top-left-radius: 3px; } -.panel > .list-group .list-group-item { - border-width: 1px 0; +.panel > .table:first-child > thead:first-child > tr:first-child td:last-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child, +.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child, +.panel > .table:first-child > thead:first-child > tr:first-child th:last-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child, +.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child { + border-top-right-radius: 3px; } -.panel > .list-group .list-group-item:first-child { - border-top-left-radius: 0; - border-top-right-radius: 0; +.panel > .table:last-child, +.panel > .table-responsive:last-child > .table:last-child { + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; } -.panel > .list-group .list-group-item:last-child { - border-bottom: 0; +.panel > .table:last-child > tbody:last-child > tr:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child { + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; } -.panel-heading + .list-group .list-group-item:first-child { - border-top-width: 0; +.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child, +.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child { + border-bottom-left-radius: 3px; } -.panel > .table, -.panel > .table-responsive > .table { - margin-bottom: 0; +.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child, +.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child { + border-bottom-right-radius: 3px; } .panel > .panel-body + .table, -.panel > .panel-body + .table-responsive { +.panel > .panel-body + .table-responsive, +.panel > .table + .panel-body, +.panel > .table-responsive + .panel-body { border-top: 1px solid #ddd; } -.panel > .table > tbody:first-child th, -.panel > .table > tbody:first-child td { +.panel > .table > tbody:first-child > tr:first-child th, +.panel > .table > tbody:first-child > tr:first-child td { border-top: 0; } .panel > .table-bordered, @@ -4715,52 +5447,35 @@ a.list-group-item-info.active:focus { .panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child { border-right: 0; } -.panel > .table-bordered > thead > tr:last-child > th, -.panel > .table-responsive > .table-bordered > thead > tr:last-child > th, -.panel > .table-bordered > tbody > tr:last-child > th, -.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th, -.panel > .table-bordered > tfoot > tr:last-child > th, -.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th, -.panel > .table-bordered > thead > tr:last-child > td, -.panel > .table-responsive > .table-bordered > thead > tr:last-child > td, +.panel > .table-bordered > thead > tr:first-child > td, +.panel > .table-responsive > .table-bordered > thead > tr:first-child > td, +.panel > .table-bordered > tbody > tr:first-child > td, +.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td, +.panel > .table-bordered > thead > tr:first-child > th, +.panel > .table-responsive > .table-bordered > thead > tr:first-child > th, +.panel > .table-bordered > tbody > tr:first-child > th, +.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th { + border-bottom: 0; +} .panel > .table-bordered > tbody > tr:last-child > td, .panel > .table-responsive > .table-bordered > tbody > tr:last-child > td, .panel > .table-bordered > tfoot > tr:last-child > td, -.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td { +.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td, +.panel > .table-bordered > tbody > tr:last-child > th, +.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th, +.panel > .table-bordered > tfoot > tr:last-child > th, +.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th { border-bottom: 0; } .panel > .table-responsive { margin-bottom: 0; border: 0; } -.panel-heading { - padding: 10px 15px; - border-bottom: 1px solid transparent; - border-top-left-radius: 3px; - border-top-right-radius: 3px; -} -.panel-heading > .dropdown .dropdown-toggle { - color: inherit; -} -.panel-title { - margin-top: 0; - margin-bottom: 0; - font-size: 16px; - color: inherit; -} -.panel-title > a { - color: inherit; -} -.panel-footer { - padding: 10px 15px; - background-color: #f5f5f5; - border-top: 1px solid #ddd; - border-bottom-right-radius: 3px; - border-bottom-left-radius: 3px; +.panel-group { + margin-bottom: 20px; } .panel-group .panel { margin-bottom: 0; - overflow: hidden; border-radius: 4px; } .panel-group .panel + .panel { @@ -4769,7 +5484,8 @@ a.list-group-item-info.active:focus { .panel-group .panel-heading { border-bottom: 0; } -.panel-group .panel-heading + .panel-collapse .panel-body { +.panel-group .panel-heading + .panel-collapse > .panel-body, +.panel-group .panel-heading + .panel-collapse > .list-group { border-top: 1px solid #ddd; } .panel-group .panel-footer { @@ -4786,25 +5502,33 @@ a.list-group-item-info.active:focus { background-color: #f5f5f5; border-color: #ddd; } -.panel-default > .panel-heading + .panel-collapse .panel-body { +.panel-default > .panel-heading + .panel-collapse > .panel-body { border-top-color: #ddd; } -.panel-default > .panel-footer + .panel-collapse .panel-body { +.panel-default > .panel-heading .badge { + color: #f5f5f5; + background-color: #333; +} +.panel-default > .panel-footer + .panel-collapse > .panel-body { border-bottom-color: #ddd; } .panel-primary { - border-color: #428bca; + border-color: #337ab7; } .panel-primary > .panel-heading { color: #fff; - background-color: #428bca; - border-color: #428bca; + background-color: #337ab7; + border-color: #337ab7; +} +.panel-primary > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #337ab7; } -.panel-primary > .panel-heading + .panel-collapse .panel-body { - border-top-color: #428bca; +.panel-primary > .panel-heading .badge { + color: #337ab7; + background-color: #fff; } -.panel-primary > .panel-footer + .panel-collapse .panel-body { - border-bottom-color: #428bca; +.panel-primary > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #337ab7; } .panel-success { border-color: #d6e9c6; @@ -4814,12 +5538,34 @@ a.list-group-item-info.active:focus { background-color: #dff0d8; border-color: #d6e9c6; } -.panel-success > .panel-heading + .panel-collapse .panel-body { +.panel-success > .panel-heading + .panel-collapse > .panel-body { border-top-color: #d6e9c6; } -.panel-success > .panel-footer + .panel-collapse .panel-body { +.panel-success > .panel-heading .badge { + color: #dff0d8; + background-color: #3c763d; +} +.panel-success > .panel-footer + .panel-collapse > .panel-body { border-bottom-color: #d6e9c6; } +.panel-info { + border-color: #bce8f1; +} +.panel-info > .panel-heading { + color: #31708f; + background-color: #d9edf7; + border-color: #bce8f1; +} +.panel-info > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #bce8f1; +} +.panel-info > .panel-heading .badge { + color: #d9edf7; + background-color: #31708f; +} +.panel-info > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #bce8f1; +} .panel-warning { border-color: #faebcc; } @@ -4828,10 +5574,14 @@ a.list-group-item-info.active:focus { background-color: #fcf8e3; border-color: #faebcc; } -.panel-warning > .panel-heading + .panel-collapse .panel-body { +.panel-warning > .panel-heading + .panel-collapse > .panel-body { border-top-color: #faebcc; } -.panel-warning > .panel-footer + .panel-collapse .panel-body { +.panel-warning > .panel-heading .badge { + color: #fcf8e3; + background-color: #8a6d3b; +} +.panel-warning > .panel-footer + .panel-collapse > .panel-body { border-bottom-color: #faebcc; } .panel-danger { @@ -4842,25 +5592,41 @@ a.list-group-item-info.active:focus { background-color: #f2dede; border-color: #ebccd1; } -.panel-danger > .panel-heading + .panel-collapse .panel-body { +.panel-danger > .panel-heading + .panel-collapse > .panel-body { border-top-color: #ebccd1; } -.panel-danger > .panel-footer + .panel-collapse .panel-body { +.panel-danger > .panel-heading .badge { + color: #f2dede; + background-color: #a94442; +} +.panel-danger > .panel-footer + .panel-collapse > .panel-body { border-bottom-color: #ebccd1; } -.panel-info { - border-color: #bce8f1; +.embed-responsive { + position: relative; + display: block; + height: 0; + padding: 0; + overflow: hidden; } -.panel-info > .panel-heading { - color: #31708f; - background-color: #d9edf7; - border-color: #bce8f1; +.embed-responsive .embed-responsive-item, +.embed-responsive iframe, +.embed-responsive embed, +.embed-responsive object, +.embed-responsive video { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + border: 0; } -.panel-info > .panel-heading + .panel-collapse .panel-body { - border-top-color: #bce8f1; +.embed-responsive.embed-responsive-16by9 { + padding-bottom: 56.25%; } -.panel-info > .panel-footer + .panel-collapse .panel-body { - border-bottom-color: #bce8f1; +.embed-responsive.embed-responsive-4by3 { + padding-bottom: 75%; } .well { min-height: 20px; @@ -4891,23 +5657,23 @@ a.list-group-item-info.active:focus { line-height: 1; color: #000; text-shadow: 0 1px 0 #fff; - opacity: .2; filter: alpha(opacity=20); + opacity: .2; } .close:hover, .close:focus { color: #000; text-decoration: none; cursor: pointer; - opacity: .5; filter: alpha(opacity=50); + opacity: .5; } button.close { + -webkit-appearance: none; padding: 0; cursor: pointer; background: transparent; border: 0; - -webkit-appearance: none; } .modal-open { overflow: hidden; @@ -4918,26 +5684,31 @@ button.close { right: 0; bottom: 0; left: 0; - z-index: 1050; + z-index: 1040; display: none; - overflow: auto; - overflow-y: scroll; + overflow: hidden; + -webkit-overflow-scrolling: touch; outline: 0; } .modal.fade .modal-dialog { -webkit-transition: -webkit-transform .3s ease-out; - -moz-transition: -moz-transform .3s ease-out; - -o-transition: -o-transform .3s ease-out; - transition: transform .3s ease-out; + -o-transition: -o-transform .3s ease-out; + transition: transform .3s ease-out; -webkit-transform: translate(0, -25%); -ms-transform: translate(0, -25%); + -o-transform: translate(0, -25%); transform: translate(0, -25%); } .modal.in .modal-dialog { -webkit-transform: translate(0, 0); -ms-transform: translate(0, 0); + -o-transform: translate(0, 0); transform: translate(0, 0); } +.modal-open .modal { + overflow-x: hidden; + overflow-y: auto; +} .modal-dialog { position: relative; width: auto; @@ -4946,33 +5717,32 @@ button.close { .modal-content { position: relative; background-color: #fff; - background-clip: padding-box; + -webkit-background-clip: padding-box; + background-clip: padding-box; border: 1px solid #999; border: 1px solid rgba(0, 0, 0, .2); border-radius: 6px; - outline: none; + outline: 0; -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, .5); box-shadow: 0 3px 9px rgba(0, 0, 0, .5); } .modal-backdrop { - position: fixed; + position: absolute; top: 0; right: 0; - bottom: 0; left: 0; - z-index: 1040; background-color: #000; } .modal-backdrop.fade { - opacity: 0; filter: alpha(opacity=0); + opacity: 0; } .modal-backdrop.in { - opacity: .5; filter: alpha(opacity=50); + opacity: .5; } .modal-header { - min-height: 16.428571429px; + min-height: 16.42857143px; padding: 15px; border-bottom: 1px solid #e5e5e5; } @@ -4981,15 +5751,14 @@ button.close { } .modal-title { margin: 0; - line-height: 1.428571429; + line-height: 1.42857143; } .modal-body { position: relative; - padding: 20px; + padding: 15px; } .modal-footer { - padding: 19px 20px 20px; - margin-top: 15px; + padding: 15px; text-align: right; border-top: 1px solid #e5e5e5; } @@ -5003,6 +5772,13 @@ button.close { .modal-footer .btn-block + .btn-block { margin-left: 0; } +.modal-scrollbar-measure { + position: absolute; + top: -9999px; + width: 50px; + height: 50px; + overflow: scroll; +} @media (min-width: 768px) { .modal-dialog { width: 600px; @@ -5015,23 +5791,27 @@ button.close { .modal-sm { width: 300px; } +} +@media (min-width: 992px) { .modal-lg { width: 900px; } } .tooltip { position: absolute; - z-index: 1030; + z-index: 1070; display: block; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 12px; + font-weight: normal; line-height: 1.4; visibility: visible; - opacity: 0; filter: alpha(opacity=0); + opacity: 0; } .tooltip.in { - opacity: .9; filter: alpha(opacity=90); + opacity: .9; } .tooltip.top { padding: 5px 0; @@ -5073,14 +5853,16 @@ button.close { border-top-color: #000; } .tooltip.top-left .tooltip-arrow { + right: 5px; bottom: 0; - left: 5px; + margin-bottom: -5px; border-width: 5px 5px 0; border-top-color: #000; } .tooltip.top-right .tooltip-arrow { - right: 5px; bottom: 0; + left: 5px; + margin-bottom: -5px; border-width: 5px 5px 0; border-top-color: #000; } @@ -5107,13 +5889,15 @@ button.close { } .tooltip.bottom-left .tooltip-arrow { top: 0; - left: 5px; + right: 5px; + margin-top: -5px; border-width: 0 5px 5px; border-bottom-color: #000; } .tooltip.bottom-right .tooltip-arrow { top: 0; - right: 5px; + left: 5px; + margin-top: -5px; border-width: 0 5px 5px; border-bottom-color: #000; } @@ -5121,14 +5905,19 @@ button.close { position: absolute; top: 0; left: 0; - z-index: 1010; + z-index: 1060; display: none; max-width: 276px; padding: 1px; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + font-weight: normal; + line-height: 1.42857143; text-align: left; white-space: normal; background-color: #fff; - background-clip: padding-box; + -webkit-background-clip: padding-box; + background-clip: padding-box; border: 1px solid #ccc; border: 1px solid rgba(0, 0, 0, .2); border-radius: 6px; @@ -5151,8 +5940,6 @@ button.close { padding: 8px 14px; margin: 0; font-size: 14px; - font-weight: normal; - line-height: 18px; background-color: #f7f7f7; border-bottom: 1px solid #ebebeb; border-radius: 5px 5px 0 0; @@ -5160,8 +5947,8 @@ button.close { .popover-content { padding: 9px 14px; } -.popover .arrow, -.popover .arrow:after { +.popover > .arrow, +.popover > .arrow:after { position: absolute; display: block; width: 0; @@ -5169,14 +5956,14 @@ button.close { border-color: transparent; border-style: solid; } -.popover .arrow { +.popover > .arrow { border-width: 11px; } -.popover .arrow:after { +.popover > .arrow:after { content: ""; border-width: 10px; } -.popover.top .arrow { +.popover.top > .arrow { bottom: -11px; left: 50%; margin-left: -11px; @@ -5184,14 +5971,14 @@ button.close { border-top-color: rgba(0, 0, 0, .25); border-bottom-width: 0; } -.popover.top .arrow:after { +.popover.top > .arrow:after { bottom: 1px; margin-left: -10px; content: " "; border-top-color: #fff; border-bottom-width: 0; } -.popover.right .arrow { +.popover.right > .arrow { top: 50%; left: -11px; margin-top: -11px; @@ -5199,14 +5986,14 @@ button.close { border-right-color: rgba(0, 0, 0, .25); border-left-width: 0; } -.popover.right .arrow:after { +.popover.right > .arrow:after { bottom: -10px; left: 1px; content: " "; border-right-color: #fff; border-left-width: 0; } -.popover.bottom .arrow { +.popover.bottom > .arrow { top: -11px; left: 50%; margin-left: -11px; @@ -5214,14 +6001,14 @@ button.close { border-bottom-color: #999; border-bottom-color: rgba(0, 0, 0, .25); } -.popover.bottom .arrow:after { +.popover.bottom > .arrow:after { top: 1px; margin-left: -10px; content: " "; border-top-width: 0; border-bottom-color: #fff; } -.popover.left .arrow { +.popover.left > .arrow { top: 50%; right: -11px; margin-top: -11px; @@ -5229,7 +6016,7 @@ button.close { border-left-color: #999; border-left-color: rgba(0, 0, 0, .25); } -.popover.left .arrow:after { +.popover.left > .arrow:after { right: 1px; bottom: -10px; content: " "; @@ -5248,15 +6035,44 @@ button.close { position: relative; display: none; -webkit-transition: .6s ease-in-out left; + -o-transition: .6s ease-in-out left; transition: .6s ease-in-out left; } .carousel-inner > .item > img, .carousel-inner > .item > a > img { - display: block; - max-width: 100%; - height: auto; line-height: 1; } +@media all and (transform-3d), (-webkit-transform-3d) { + .carousel-inner > .item { + -webkit-transition: -webkit-transform .6s ease-in-out; + -o-transition: -o-transform .6s ease-in-out; + transition: transform .6s ease-in-out; + + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + -webkit-perspective: 1000; + perspective: 1000; + } + .carousel-inner > .item.next, + .carousel-inner > .item.active.right { + left: 0; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } + .carousel-inner > .item.prev, + .carousel-inner > .item.active.left { + left: 0; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } + .carousel-inner > .item.next.left, + .carousel-inner > .item.prev.right, + .carousel-inner > .item.active { + left: 0; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} .carousel-inner > .active, .carousel-inner > .next, .carousel-inner > .prev { @@ -5296,31 +6112,35 @@ button.close { font-size: 20px; color: #fff; text-align: center; - filter: alpha(opacity=50); text-shadow: 0 1px 2px rgba(0, 0, 0, .6); + filter: alpha(opacity=50); opacity: .5; } .carousel-control.left { - background-image: -webkit-linear-gradient(left, color-stop(rgba(0, 0, 0, .5) 0%), color-stop(rgba(0, 0, 0, .0001) 100%)); + background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); + background-image: -o-linear-gradient(left, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); + background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, .5)), to(rgba(0, 0, 0, .0001))); background-image: linear-gradient(to right, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); - background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1); + background-repeat: repeat-x; } .carousel-control.right { right: 0; left: auto; - background-image: -webkit-linear-gradient(left, color-stop(rgba(0, 0, 0, .0001) 0%), color-stop(rgba(0, 0, 0, .5) 100%)); + background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); + background-image: -o-linear-gradient(left, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); + background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, .0001)), to(rgba(0, 0, 0, .5))); background-image: linear-gradient(to right, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); - background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1); + background-repeat: repeat-x; } .carousel-control:hover, .carousel-control:focus { color: #fff; text-decoration: none; - outline: none; - opacity: .9; filter: alpha(opacity=90); + outline: 0; + opacity: .9; } .carousel-control .icon-prev, .carousel-control .icon-next, @@ -5334,18 +6154,20 @@ button.close { .carousel-control .icon-prev, .carousel-control .glyphicon-chevron-left { left: 50%; + margin-left: -10px; } .carousel-control .icon-next, .carousel-control .glyphicon-chevron-right { right: 50%; + margin-right: -10px; } .carousel-control .icon-prev, .carousel-control .icon-next { width: 20px; height: 20px; margin-top: -10px; - margin-left: -10px; font-family: serif; + line-height: 1; } .carousel-control .icon-prev:before { content: '\2039'; @@ -5398,16 +6220,23 @@ button.close { text-shadow: none; } @media screen and (min-width: 768px) { - .carousel-control .glyphicons-chevron-left, - .carousel-control .glyphicons-chevron-right, + .carousel-control .glyphicon-chevron-left, + .carousel-control .glyphicon-chevron-right, .carousel-control .icon-prev, .carousel-control .icon-next { width: 30px; height: 30px; margin-top: -15px; - margin-left: -15px; font-size: 30px; } + .carousel-control .glyphicon-chevron-left, + .carousel-control .icon-prev { + margin-left: -15px; + } + .carousel-control .glyphicon-chevron-right, + .carousel-control .icon-next { + margin-right: -15px; + } .carousel-caption { right: 20%; left: 20%; @@ -5417,29 +6246,10 @@ button.close { bottom: 20px; } } -.fade { - opacity: 0; - -webkit-transition: opacity .15s linear; - transition: opacity .15s linear; -} -.fade.in { - opacity: 1; -} -.collapse { - display: none; -} -.collapse.in { - display: block; -} -.collapsing { - position: relative; - height: 0; - overflow: hidden; - -webkit-transition: height .35s ease; - transition: height .35s ease; -} .clearfix:before, .clearfix:after, +.dl-horizontal dd:before, +.dl-horizontal dd:after, .container:before, .container:after, .container-fluid:before, @@ -5470,6 +6280,7 @@ button.close { content: " "; } .clearfix:after, +.dl-horizontal dd:after, .container:after, .container-fluid:after, .row:after, @@ -5523,9 +6334,23 @@ button.close { width: device-width; } .visible-xs, -tr.visible-xs, -th.visible-xs, -td.visible-xs { +.visible-sm, +.visible-md, +.visible-lg { + display: none !important; +} +.visible-xs-block, +.visible-xs-inline, +.visible-xs-inline-block, +.visible-sm-block, +.visible-sm-inline, +.visible-sm-inline-block, +.visible-md-block, +.visible-md-inline, +.visible-md-inline-block, +.visible-lg-block, +.visible-lg-inline, +.visible-lg-inline-block { display: none !important; } @media (max-width: 767px) { @@ -5543,11 +6368,20 @@ td.visible-xs { display: table-cell !important; } } -.visible-sm, -tr.visible-sm, -th.visible-sm, -td.visible-sm { - display: none !important; +@media (max-width: 767px) { + .visible-xs-block { + display: block !important; + } +} +@media (max-width: 767px) { + .visible-xs-inline { + display: inline !important; + } +} +@media (max-width: 767px) { + .visible-xs-inline-block { + display: inline-block !important; + } } @media (min-width: 768px) and (max-width: 991px) { .visible-sm { @@ -5564,11 +6398,20 @@ td.visible-sm { display: table-cell !important; } } -.visible-md, -tr.visible-md, -th.visible-md, -td.visible-md { - display: none !important; +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-block { + display: block !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-inline { + display: inline !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-inline-block { + display: inline-block !important; + } } @media (min-width: 992px) and (max-width: 1199px) { .visible-md { @@ -5585,11 +6428,20 @@ td.visible-md { display: table-cell !important; } } -.visible-lg, -tr.visible-lg, -th.visible-lg, -td.visible-lg { - display: none !important; +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-block { + display: block !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-inline { + display: inline !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-inline-block { + display: inline-block !important; + } } @media (min-width: 1200px) { .visible-lg { @@ -5606,42 +6458,42 @@ td.visible-lg { display: table-cell !important; } } +@media (min-width: 1200px) { + .visible-lg-block { + display: block !important; + } +} +@media (min-width: 1200px) { + .visible-lg-inline { + display: inline !important; + } +} +@media (min-width: 1200px) { + .visible-lg-inline-block { + display: inline-block !important; + } +} @media (max-width: 767px) { - .hidden-xs, - tr.hidden-xs, - th.hidden-xs, - td.hidden-xs { + .hidden-xs { display: none !important; } } @media (min-width: 768px) and (max-width: 991px) { - .hidden-sm, - tr.hidden-sm, - th.hidden-sm, - td.hidden-sm { + .hidden-sm { display: none !important; } } @media (min-width: 992px) and (max-width: 1199px) { - .hidden-md, - tr.hidden-md, - th.hidden-md, - td.hidden-md { + .hidden-md { display: none !important; } } @media (min-width: 1200px) { - .hidden-lg, - tr.hidden-lg, - th.hidden-lg, - td.hidden-lg { + .hidden-lg { display: none !important; } } -.visible-print, -tr.visible-print, -th.visible-print, -td.visible-print { +.visible-print { display: none !important; } @media print { @@ -5659,11 +6511,33 @@ td.visible-print { display: table-cell !important; } } +.visible-print-block { + display: none !important; +} +@media print { + .visible-print-block { + display: block !important; + } +} +.visible-print-inline { + display: none !important; +} +@media print { + .visible-print-inline { + display: inline !important; + } +} +.visible-print-inline-block { + display: none !important; +} +@media print { + .visible-print-inline-block { + display: inline-block !important; + } +} @media print { - .hidden-print, - tr.hidden-print, - th.hidden-print, - td.hidden-print { + .hidden-print { display: none !important; } } +/*# sourceMappingURL=bootstrap.css.map */ diff --git a/web/css/bootstrap.min.css b/web/css/bootstrap.min.css new file mode 100644 index 0000000..5f22efe --- /dev/null +++ b/web/css/bootstrap.min.css @@ -0,0 +1,5 @@ +/*! + * Bootstrap v3.3.1 (http://getbootstrap.com) + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + *//*! normalize.css v3.0.2 | MIT License | git.io/normalize */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:before,:after{color:#000!important;text-shadow:none!important;background:transparent!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff!important}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered th,.table-bordered td{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before,.glyphicon-eur:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-door:before{content:"\1f6aa"}.glyphicon-key:before{content:"\1F511"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:before,:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:hover,a:focus{color:#23527c;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive,.thumbnail>img,.thumbnail a>img,.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:400;line-height:1;color:#777}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}mark,.mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote footer:before,blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:''}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#dff0d8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#d9edf7}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr.info:hover>th{background-color:#c4e3f3}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr.warning:hover>th{background-color:#faf2cc}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#f2dede}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=radio],input[type=checkbox]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=radio]:focus,input[type=checkbox]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee;opacity:1}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date],input[type=time],input[type=datetime-local],input[type=month]{line-height:34px}input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.radio,.checkbox{position:relative;display:block;margin-top:10px;margin-bottom:10px}.radio label,.checkbox label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.radio input[type=radio],.radio-inline input[type=radio],.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox]{position:absolute;margin-top:4px \9;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type=radio][disabled],input[type=checkbox][disabled],input[type=radio].disabled,input[type=checkbox].disabled,fieldset[disabled] input[type=radio],fieldset[disabled] input[type=checkbox]{cursor:not-allowed}.radio-inline.disabled,.checkbox-inline.disabled,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.radio.disabled label,.checkbox.disabled label,fieldset[disabled] .radio label,fieldset[disabled] .checkbox label{cursor:not-allowed}.form-control-static{padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}textarea.input-sm,select[multiple].input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.form-group-sm .form-control{height:30px;line-height:30px}textarea.form-group-sm .form-control,select[multiple].form-group-sm .form-control{height:auto}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg{height:46px;line-height:46px}textarea.input-lg,select[multiple].input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.form-group-lg .form-control{height:46px;line-height:46px}textarea.form-group-lg .form-control,select[multiple].form-group-lg .form-control{height:auto}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline,.has-success.radio label,.has-success.checkbox label,.has-success.radio-inline label,.has-success.checkbox-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline,.has-warning.radio label,.has-warning.checkbox label,.has-warning.radio-inline label,.has-warning.checkbox-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline,.has-error.radio label,.has-error.checkbox label,.has-error.radio-inline label,.has-error.checkbox-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.form-inline .input-group .form-control{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .radio label,.form-inline .checkbox label{padding-left:0}.form-inline .radio input[type=radio],.form-inline .checkbox input[type=checkbox]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:14.3px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn:focus,.btn:active:focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn.active.focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus,.btn.focus{color:#333;text-decoration:none}.btn:active,.btn.active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{pointer-events:none;cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default.focus,.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled.focus,.btn-default[disabled].focus,fieldset[disabled] .btn-default.focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary:hover,.btn-primary:focus,.btn-primary.focus,.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success.focus,.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled.focus,.btn-success[disabled].focus,fieldset[disabled] .btn-success.focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info.focus,.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled.focus,.btn-info[disabled].focus,fieldset[disabled] .btn-info.focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning.focus,.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled.focus,.btn-warning[disabled].focus,fieldset[disabled] .btn-warning.focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger.focus,.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled.focus,.btn-danger[disabled].focus,fieldset[disabled] .btn-danger.focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link:active,.btn-link.active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#777;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=submit].btn-block,input[type=reset].btn-block,input[type=button].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none;visibility:hidden}.collapse.in{display:block;visibility:visible}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#777}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px solid}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=radio],[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=radio],.input-group-addon input[type=checkbox]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none;visibility:hidden}.tab-content>.active{display:block;visibility:visible}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important;visibility:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:200px}}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn,.navbar-form .input-group .form-control{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .radio label,.navbar-form .checkbox label{padding-left:0}.navbar-form .radio input[type=radio],.navbar-form .checkbox input[type=checkbox]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:hover,.navbar-default .btn-link:focus{color:#333}.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:hover,.navbar-default .btn-link[disabled]:focus,fieldset[disabled] .navbar-default .btn-link:focus{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:hover,.navbar-inverse .btn-link:focus{color:#fff}.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:hover,.navbar-inverse .btn-link[disabled]:focus,fieldset[disabled] .navbar-inverse .btn-link:focus{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:hover,a.label:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:hover,.label-default[href]:focus{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px 15px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding:48px 0}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail>img,.thumbnail a>img{margin-right:auto;margin-left:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar,.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress.active .progress-bar,.progress-bar.active{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-object{display:block}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-left,.media-right,.media-body{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,a.list-group-item:focus{color:#555;text-decoration:none;background-color:#f5f5f5}.list-group-item.disabled,.list-group-item.disabled:hover,.list-group-item.disabled:focus{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>.small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:hover .list-group-item-text,.list-group-item.active:focus .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,a.list-group-item-success:focus{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:hover,a.list-group-item-success.active:focus{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,a.list-group-item-info:focus{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:hover,a.list-group-item-info.active:focus{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,a.list-group-item-warning:focus{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,a.list-group-item-danger:focus{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.table,.panel>.table-responsive>.table,.panel>.panel-collapse>.table{margin-bottom:0}.panel>.table caption,.panel>.table-responsive>.table caption,.panel>.panel-collapse>.table caption{padding-right:15px;padding-left:15px}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.panel-body,.panel-group .panel-heading+.panel-collapse>.list-group{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:absolute;top:0;right:0;left:0;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{min-height:16.43px;padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12px;font-weight:400;line-height:1.4;visibility:visible;filter:alpha(opacity=0);opacity:0}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;font-weight:400;line-height:1.42857143;text-align:left;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000;perspective:1000}.carousel-inner>.item.next,.carousel-inner>.item.active.right{left:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.carousel-inner>.item.prev,.carousel-inner>.item.active.left{left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right,.carousel-inner>.item.active{left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%;margin-left:-10px}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%;margin-right:-10px}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;margin-top:-10px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-15px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-15px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-15px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after,.dl-horizontal dd:before,.dl-horizontal dd:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.panel-body:before,.panel-body:after,.modal-footer:before,.modal-footer:after{display:table;content:" "}.clearfix:after,.dl-horizontal dd:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after,.modal-footer:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important;visibility:hidden!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none!important}.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table}tr.visible-xs{display:table-row!important}th.visible-xs,td.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table}tr.visible-sm{display:table-row!important}th.visible-sm,td.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table}tr.visible-md{display:table-row!important}th.visible-md,td.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table}tr.visible-lg{display:table-row!important}th.visible-lg,td.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table}tr.visible-print{display:table-row!important}th.visible-print,td.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}} \ No newline at end of file diff --git a/web/datepicker/datepicker_demo.dart b/web/datepicker/datepicker_demo.dart new file mode 100644 index 0000000..3e62261 --- /dev/null +++ b/web/datepicker/datepicker_demo.dart @@ -0,0 +1,67 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +part of angular.ui.demo; + +/** + * Datepicker controller. + */ +@Component( + selector: 'datepicker-demo', + templateUrl: 'datepicker/datepicker_demo.html', + useShadowDom: false +) +class DatepickerDemo { + + var dt; + bool showWeeks = true; + DateTime minDate, maxDate = new DateTime(2020, 5, 22); + bool opened = false; + Map dateOptions = { + 'year-format': '\'yy\'', + 'starting-day': 1 + }; + List formats = ['dd-MMMM-yyyy', 'yyyy/MM/dd', 'shortDate']; + String format; + Map status = { + 'opened': false + }; + +// Date filter; + + DatepickerDemo() { //this.filter ) { + toggleMin(); + format = formats[0]; + } + + void today() { + dt = new DateTime.now(); + } + + void toggleWeeks() { + showWeeks = !showWeeks; + } + + void clear() { + dt = null; + } + + bool disabled(DateTime date, mode) { + return ( mode == 'day' && ( date.day == 0 || date.day == 6 ) ); + } + + void toggleMin() { + minDate = minDate == null ? new DateTime.now() : null; + } + + void open(dom.Event event) { + event.preventDefault(); + event.stopPropagation(); + + opened = true; + } + +// String translate(DateTime date) { +// return filter.call(date, format); +// } +} \ No newline at end of file diff --git a/web/datepicker/datepicker_demo.html b/web/datepicker/datepicker_demo.html new file mode 100644 index 0000000..793b5c3 --- /dev/null +++ b/web/datepicker/datepicker_demo.html @@ -0,0 +1,57 @@ + + +
+
Selected date is: {{dt | date:'fullDate' }}
+ +
+
+

+ +

+
+
+ +

Inline

+
+
+ +
+
+ +

Popup

+
+ + + +
+ + +
+ + + + + +
\ No newline at end of file diff --git a/web/dragdrop/dragdrop_demo.dart b/web/dragdrop/dragdrop_demo.dart new file mode 100644 index 0000000..29881a2 --- /dev/null +++ b/web/dragdrop/dragdrop_demo.dart @@ -0,0 +1,101 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +part of angular.ui.demo; + +@Component(selector: '[dd-shopping-demo-controller]', + templateUrl: 'dragdrop/dragdrop_demo_shopping.html', + useShadowDom: false + ) +class DragDropShoppingBasketDemoController { + + List availableProducts = []; + List shoppingBasket = []; + + DragDropShoppingBasketDemoController() { + availableProducts.add(new Product("Blue Shoes", 3, 35)); + availableProducts.add(new Product("Good Jacket", 1, 90)); + availableProducts.add(new Product("Red Shirt", 5, 12)); + availableProducts.add(new Product("Blue Jeans", 4, 60)); + } + + void orderedProduct(Product orderedProduct) { + print("New ordered product: " + orderedProduct.name); + orderedProduct.quantity--; + } + + void addToBasket(Product newProduct) { + print("Add to basket: " + newProduct.name); + for(Product product in shoppingBasket) { + if (product.name == newProduct.name) { + product.quantity++; + return; + } + } + shoppingBasket.add(new Product(newProduct.name, 1, newProduct.cost)); + } + + int totalCost() { + int cost = 0; + for(Product product in shoppingBasket) { + cost+= (product.cost*product.quantity); + } + return cost; + } +} + +class Product { + + int quantity = 0; + int cost = 0; + String name = ""; + + Product(this.name, this.quantity, this.cost); + +} + + +@Component( + selector: '[dd-list-demo-controller]', + templateUrl: 'dragdrop/dragdrop_demo_list.html', + useShadowDom: false) +class DragDropListDemoController { + + List todoList = ['Task 1: Fix Blu Bug','Task 2: Create Sortable','Task 3: Improve Tabs','Task 4: Publish new version']; + List ongoingList = ['Task 5: Create Drag&Drop']; + List doneList = ['Task 6: Create Alert', 'Task 7: Test Accordion']; + + void add(List list, String value) { + list.add(value); + } + + void remove(List list, String value) { + list.remove(value); + } +} + + +@Component( + selector: '[dd-custom-image-demo-controller]', + templateUrl: 'dragdrop/dragdrop_demo_image.html', + useShadowDom: false) +class DragDropCustomImageDemoController { + + DragDropConfig dragdropConfig = new DragDropConfig(); + + DragDropCustomImageDemoController() { + dragdropConfig.dragImage = new DragImage(new dom.ImageElement(src: "dragdrop/smile.jpg")); + dragdropConfig.dropEffect = DataTransferEffect.COPY; + dragdropConfig.dragEffect = DataTransferEffect.COPY; + } + +} + + +@Component( + selector: '[dd-multidropzones-demo-controller]', + templateUrl: 'dragdrop/dragdrop_demo_multidropzones.html', + useShadowDom: false) +class DragDropMultiDropZonesDemoController { + +} \ No newline at end of file diff --git a/web/dragdrop/dragdrop_demo.html b/web/dragdrop/dragdrop_demo.html new file mode 100644 index 0000000..3b8914f --- /dev/null +++ b/web/dragdrop/dragdrop_demo.html @@ -0,0 +1,16 @@ + + +
+
+ +
+ +
+ +
+ +
\ No newline at end of file diff --git a/web/dragdrop/dragdrop_demo_image.html b/web/dragdrop/dragdrop_demo_image.html new file mode 100644 index 0000000..c1173ad --- /dev/null +++ b/web/dragdrop/dragdrop_demo_image.html @@ -0,0 +1,28 @@ +
+

Drag And Drop - Custom Drag Image

+

Note: custom drag images are not supported in IE9/IE10.

+
+ +
+
+
Drag me
+
+
+
+ I show a custom image on drag +
+
+
+
+
+ +
+
+
Drop here
+
+
+
+
+
+
\ No newline at end of file diff --git a/web/dragdrop/dragdrop_demo_list.html b/web/dragdrop/dragdrop_demo_list.html new file mode 100644 index 0000000..1669b1d --- /dev/null +++ b/web/dragdrop/dragdrop_demo_list.html @@ -0,0 +1,48 @@ +
+

Drag And Drop - Move from list to list

+
+
+
+
To Do
+
+
+
+ {{item}} +
+
+
+
+
+ +
+
+
Ongoing
+
+
+
+ {{item}} +
+
+
+
+
+ + +
+
+
Done
+
+
+
+ {{item}} +
+
+
+
+
+
+
\ No newline at end of file diff --git a/web/dragdrop/dragdrop_demo_multidropzones.html b/web/dragdrop/dragdrop_demo_multidropzones.html new file mode 100644 index 0000000..da4a415 --- /dev/null +++ b/web/dragdrop/dragdrop_demo_multidropzones.html @@ -0,0 +1,58 @@ +
+

Drag And Drop - Use multiple drop-zones

+
+
+
+
I can drop only in zone ONE
+
+
+
+ Drag me +
+
+
+
+
+
+
+
I can drop only in zone TWO
+
+
+
+ Drag me +
+
+
+
+
+
+
+
I can drop in both zone ONE or TWO
+
+
+
+ Drag me +
+
+
+
+
+
+
+
Drop Zone ONE
+
+
+
+
+
+
+
Drop Zone TWO
+
+
+
+
+
+
\ No newline at end of file diff --git a/web/dragdrop/dragdrop_demo_shopping.html b/web/dragdrop/dragdrop_demo_shopping.html new file mode 100644 index 0000000..21bff33 --- /dev/null +++ b/web/dragdrop/dragdrop_demo_shopping.html @@ -0,0 +1,32 @@ +
+

Drag And Drop - Shopping basket

+
+ +
+
+
Available products
+
+
+
+
{{product.name}} - ${{product.cost}} (available: {{product.quantity}})
+
{{product.name}} (NOT available)
+
+
+
+
+
+
+
+
Shopping Basket (to pay: ${{totalCost()}})
+
+
+
+ {{product.name}} (ordered: {{product.quantity}} cost: ${{product.cost * product.quantity}}) +
+
+
+
+
+
+
\ No newline at end of file diff --git a/web/dragdrop/smile.jpg b/web/dragdrop/smile.jpg new file mode 100644 index 0000000..ab15da2 Binary files /dev/null and b/web/dragdrop/smile.jpg differ diff --git a/web/dragdrop/sortable_demo.dart b/web/dragdrop/sortable_demo.dart new file mode 100644 index 0000000..051ff61 --- /dev/null +++ b/web/dragdrop/sortable_demo.dart @@ -0,0 +1,35 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +part of angular.ui.demo; + +@Component( + selector: '[sortable-simple-controller]', + templateUrl: 'dragdrop/sortable_demo_simple.html', + useShadowDom: false + ) +class SortableController { + + List listOne = ['Coffee','Orange Juice','Red Wine','Unhealty drink!','Water']; + + SortableController() { + } + +} + + +@Component( + selector: '[sortable-multi-controller]', + templateUrl: 'dragdrop/sortable_demo_multi.html', + useShadowDom: false + ) +class SortableMultiController { + + List listBoxers = ['Sugar Ray Robinson','Muhammad Ali','George Foreman','Joe Frazier','Jake LaMotta','Joe Louis','Jack Dempsey','Rocky Marciano','Mike Tyson','Oscar De La Hoya']; + List listTeamOne = []; + List listTeamTwo = []; + + SortableMultiController() { + } + +} diff --git a/web/dragdrop/sortable_demo.html b/web/dragdrop/sortable_demo.html new file mode 100644 index 0000000..5879e30 --- /dev/null +++ b/web/dragdrop/sortable_demo.html @@ -0,0 +1,15 @@ + + +
+ +
+
+ +
+
+ +
diff --git a/web/dragdrop/sortable_demo_multi.html b/web/dragdrop/sortable_demo_multi.html new file mode 100644 index 0000000..ff6a259 --- /dev/null +++ b/web/dragdrop/sortable_demo_multi.html @@ -0,0 +1,41 @@ +
+

Multi list sortable

+
+
+
+
+ Available boxers +
+
+
    +
  • {{item}}
  • +
+
+
+
+
+
+
+ First Team +
+
+
    +
  • {{item}}
  • +
+
+
+
+
+
+
+ Second Team +
+
+
    +
  • {{item}}
  • +
+
+
+
+
+
diff --git a/web/dragdrop/sortable_demo_simple.html b/web/dragdrop/sortable_demo_simple.html new file mode 100644 index 0000000..126befe --- /dev/null +++ b/web/dragdrop/sortable_demo_simple.html @@ -0,0 +1,25 @@ +
+

Simple sortable

+
+
+
+
+ Favorite drinks +
+
+
    +
  • {{item}}
  • +
+
+
+
+
+
+
+ My prefences:
+ {{$index + 1}}) {{item}}
+
+
+
+
+
diff --git a/web/dropdown_toggle/dropdown_toggle_demo.dart b/web/dropdown_toggle/dropdown_toggle_demo.dart new file mode 100644 index 0000000..365fd3c --- /dev/null +++ b/web/dropdown_toggle/dropdown_toggle_demo.dart @@ -0,0 +1,22 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +part of angular.ui.demo; + +/** + * Dropdown demo component. + */ +@Component( + selector: 'dropdown-demo', + templateUrl: 'dropdown_toggle/dropdown_toggle_demo.html', + useShadowDom: false) +class DropdownDemo implements ScopeAware { + + Scope scope; + + var items = [ + "The first choice!", + "And another choice for you.", + "but wait! A third!" + ]; +} \ No newline at end of file diff --git a/web/dropdown_toggle/dropdown_toggle_demo.html b/web/dropdown_toggle/dropdown_toggle_demo.html new file mode 100644 index 0000000..265ce09 --- /dev/null +++ b/web/dropdown_toggle/dropdown_toggle_demo.html @@ -0,0 +1,12 @@ + + + \ No newline at end of file diff --git a/web/fonts/glyphicons-halflings-regular.eot b/web/fonts/glyphicons-halflings-regular.eot index 423bd5d..b93a495 100644 Binary files a/web/fonts/glyphicons-halflings-regular.eot and b/web/fonts/glyphicons-halflings-regular.eot differ diff --git a/web/fonts/glyphicons-halflings-regular.svg b/web/fonts/glyphicons-halflings-regular.svg index 4469488..94fb549 100644 --- a/web/fonts/glyphicons-halflings-regular.svg +++ b/web/fonts/glyphicons-halflings-regular.svg @@ -6,224 +6,283 @@ - - - + + - - + + - - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/fonts/glyphicons-halflings-regular.ttf b/web/fonts/glyphicons-halflings-regular.ttf index a498ef4..1413fc6 100644 Binary files a/web/fonts/glyphicons-halflings-regular.ttf and b/web/fonts/glyphicons-halflings-regular.ttf differ diff --git a/web/fonts/glyphicons-halflings-regular.woff b/web/fonts/glyphicons-halflings-regular.woff index d83c539..9e61285 100644 Binary files a/web/fonts/glyphicons-halflings-regular.woff and b/web/fonts/glyphicons-halflings-regular.woff differ diff --git a/web/index.html b/web/index.html index a1b1493..1d3904d 100644 --- a/web/index.html +++ b/web/index.html @@ -1,6 +1,6 @@ @@ -9,68 +9,85 @@ Angular dart ui - + - +

Angular Dart UI

+
+

Typeahead

+
-

Alert

-
- Immortal info - {{alert.msg}} - -
+

Popover

+ +
+

Tooltip

+
-

Collapse

-
- - -
-
Some content
-
-
+

Timepicker

+ +
-

Dropdown Toggle

- - +

Drag And Drop

+
+
+ +

Drag And Drop - Sortable

+

-

Buttons

-
- -

Single toggle

-
{{ctrl.singleModel}}
- - -

Checkbox

-
{{ctrl.leftModel}} - {{ctrl.middleModel}} - {{ctrl.rightModel}}
-
- - - -
+

Datepicker (partially implemented)

+ +
-

Radio

-
{{ctrl.radioModel}}
-
- - - -
-
+

Modal Window

+ + +
+

Rating

+ +
+ +

Accordion

+ +
+ +

Alert

+ +
+ +

Collapse

+ +
+ +

Dropdown Toggle

+ +
+ +

Pagination

+ +
+ +

Progressbar

+ +
+ +

Buttons

+ +
+ +

Tabs

+ +
+ +

Carousel

+ + diff --git a/web/main.css b/web/main.css index b999453..4f40017 100644 --- a/web/main.css +++ b/web/main.css @@ -1,5 +1,5 @@ /* -Copyright (c) 2013 - 2014, akserg (Sergey Akopkokhyants) +Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. https://github.com/akserg/angular.dart.ui All rights reserved. Please see the LICENSE.md file. */ @@ -11,4 +11,26 @@ body { font-weight: normal; line-height: 1.2em; margin: 15px; -} \ No newline at end of file +} + + +.ui-drag-start { + -moz-transform:scale(0.8); + -webkit-transform:scale(0.8); + transform:scale(0.8); + opacity:0.7; + border: 2px dashed #000; +} + +.ui-drag-over { + border: 2px dashed #000; +} + +.ui-sortable-drag { + -moz-transform:scale(0.9); + -webkit-transform:scale(0.9); + transform:scale(0.9); + opacity:0.7; + border: 1px dashed #000; +} + diff --git a/web/main.dart b/web/main.dart index b5e570b..1832ac0 100644 --- a/web/main.dart +++ b/web/main.dart @@ -1,16 +1,62 @@ -// Copyright (c) 2013 - 2014, akserg (Sergey Akopkokhyants) +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. // https://github.com/akserg/angular.dart.ui // All rights reserved. Please see the LICENSE.md file. library angular.ui.demo; +import 'dart:html' as dom; +import 'dart:math' as math; import 'package:angular/angular.dart'; +import 'package:angular/application_factory.dart'; import 'package:angular_ui/angular_ui.dart'; +import 'package:angular_ui/utils/utils.dart'; +import 'package:logging/logging.dart'; + +@MirrorsUsed(targets: const[ + 'angular.ui', + 'angular.ui.demo' +], override: '*') +import 'dart:mirrors'; +import 'package:angular_ui/dragdrop/dragdrop.dart'; + +part 'accordion/accordion_demo.dart'; +part 'alert/alert_demo.dart'; +part 'buttons/buttons_demo.dart'; +part 'carousel/carousel_demo.dart'; +part 'collapse/collapse_demo.dart'; +part 'dragdrop/dragdrop_demo.dart'; +part 'dragdrop/sortable_demo.dart'; +part 'datepicker/datepicker_demo.dart'; +part 'dropdown_toggle/dropdown_toggle_demo.dart'; +part 'modal/modal_demo_embedded_template.dart'; +part 'modal/modal_demo_embedded_template_static_backdrop.dart'; +part 'modal/modal_demo_template_element.dart'; +part 'modal/modal_demo_template_element_from_other_file.dart'; +//part 'modal/modal_demo_template_from_file.dart'; +part 'pagination/pagination_demo.dart'; +part 'progressbar/progressbar_demo.dart'; +part 'tabs/tabs_demo.dart'; +part 'timepicker/timepicker_demo.dart'; +part 'rating/rating_demo.dart'; +part 'tooltip/tooltip_demo.dart'; +part 'popover/popover_demo.dart'; +part 'typeahead/typeahead_demo.dart'; /** * Entry point into app. */ main() { - ngBootstrap(module: new DemoModule()); + + hierarchicalLoggingEnabled = true; + Logger.root.level = Level.OFF; + Logger.root.onRecord.listen((LogRecord r) { + DateTime now = new DateTime.now(); + dom.window.console.log('${now} [${r.level}] ${r.loggerName}: ${r.message}'); + }); + new Logger("angular.ui")..level = Level.FINER; + + applicationFactory() + .addModule(new DemoModule()) + .run(); } /** @@ -20,75 +66,31 @@ class DemoModule extends Module { DemoModule() { install(new AngularUIModule()); // - type(ButtonsCtrl); - type(CollapseCtrl); - type(DropdownCtrl); - type(AlertCtrl); + bind(PopoverDemo); + bind(TooltipDemo); + bind(TimepickerDemo); + bind(DatepickerDemo); + bind(ModalDemoEmbeddedTemplate); + bind(ModalDemoEmbeddedTemplateWithStaticBackdrop); + bind(ModalDemoTagTemplate); + bind(ModalDemoOtherTemplate); +// bind(ModalDemoFileTemplate); + bind(AlertDemo); + bind(CollapseDemo); + bind(DropdownDemo); + bind(PaginationDemo); + bind(ProgressDemo); + bind(ButtonsDemo); + bind(CarouselDemo); + bind(TabsDemo); + bind(AccordionDemo); + bind(RatingDemo); + bind(DragDropShoppingBasketDemoController); + bind(DragDropListDemoController); + bind(DragDropCustomImageDemoController); + bind(DragDropMultiDropZonesDemoController); + bind(SortableController); + bind(SortableMultiController); + bind(TypeaheadDemo); } -} - -/** - * Buttons controller. - */ -@NgController(selector: '[buttons-ctrl]', publishAs: 'ctrl') -class ButtonsCtrl { - - var singleModel = 1; - - var radioModel = 'Right'; - - var leftModel = false; - - var middleModel = true; - - var rightModel = false; -} - -/** - * Collapse controller. - */ -@NgController(selector: '[collapse-ctrl]', publishAs: 'ctrl') -class CollapseCtrl { - - var isCollapsed = true; -} - -/** - * Dropdown controller. - */ -@NgController(selector: '[dropdown-ctrl]', publishAs: 'ctrl') -class DropdownCtrl { - - var items = [ - "The first choice!", - "And another choice for you.", - "but wait! A third!" - ]; -} - -/** - * Dropdown controller. - */ -@NgController(selector: '[alert-ctrl]', publishAs: 'ctrl') -class AlertCtrl { - - List alerts = [ - new AlertItem(type:'danger', msg:'Oh snap! Change a few things up and try submitting again.'), - new AlertItem(type:'success', msg:'Well done! You successfully read this important alert message.') - ]; - - void addAlert() { - alerts.add(new AlertItem(msg:"Another alert!")); - } - - void closeAlert(index) { - alerts.removeAt(index); - } -} - -class AlertItem { - var type; - var msg; - - AlertItem({String this.type:null, String this.msg:''}); -} +} \ No newline at end of file diff --git a/web/modal/modal_demo_embedded_template.dart b/web/modal/modal_demo_embedded_template.dart new file mode 100644 index 0000000..d6acd8f --- /dev/null +++ b/web/modal/modal_demo_embedded_template.dart @@ -0,0 +1,75 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +part of angular.ui.demo; + +/** + * Modal controller with template. + */ +@Component( + selector: 'modal-demo-embedded-tmpl', + useShadowDom: false, + templateUrl: 'modal/modal_demo_embedded_template.html', + exportExpressions: const ["tmp", "ok"] +) +class ModalDemoEmbeddedTemplate implements ScopeAware { + List items = ["1111", "2222", "3333", "4444"]; + String selected; + String tmp; + + Modal modal; + ModalInstance modalInstance; + Scope scope; + + String template = """ + + + +"""; + +ModalDemoEmbeddedTemplate(this.modal); + + ModalInstance getModalInstance() { + return modal.open(new ModalOptions(template:template), scope); + } + + void open() { + modalInstance = getModalInstance(); + + modalInstance.opened + ..then((v) { + print('Opened'); + }, onError: (e) { + print('Open error is $e'); + }); + + // Override close to add you own functionality + modalInstance.close = (result) { + selected = result; + print('Closed with selection $selected'); + modal.hide(); + }; + // Override dismiss to add you own functionality + modalInstance.dismiss = (String reason) { + print('Dismissed with $reason'); + modal.hide(); + }; + } + + void ok(sel) { + modalInstance.close(sel); + } +} \ No newline at end of file diff --git a/web/modal/modal_demo_embedded_template.html b/web/modal/modal_demo_embedded_template.html new file mode 100644 index 0000000..0497364 --- /dev/null +++ b/web/modal/modal_demo_embedded_template.html @@ -0,0 +1,15 @@ + + +
+
Modal from embedded template
+
+
+ +
Selected: {{ selected }}
+
+
+
\ No newline at end of file diff --git a/web/modal/modal_demo_embedded_template_static_backdrop.dart b/web/modal/modal_demo_embedded_template_static_backdrop.dart new file mode 100644 index 0000000..985d723 --- /dev/null +++ b/web/modal/modal_demo_embedded_template_static_backdrop.dart @@ -0,0 +1,74 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +part of angular.ui.demo; + +/** + * Modal controller with template and static backdrop. + */ +@Component( + selector: 'modal-demo-embedded-tmpl-static-backdrop', + useShadowDom: false, + templateUrl: 'modal/modal_demo_embedded_template_static_backdrop.html', + exportExpressions: const ["tmp", "ok"]) +class ModalDemoEmbeddedTemplateWithStaticBackdrop implements ScopeAware { + List items = ["First", "Second", "Third", "Fourth"]; + String selected; + String tmp; + + Modal modal; + ModalInstance modalInstance; + Scope scope; + + String template = """ + + + +"""; + +ModalDemoEmbeddedTemplateWithStaticBackdrop(this.modal); + + ModalInstance getModalInstance() { + return modal.open(new ModalOptions(template:template, backdrop: 'static'), scope); + } + + void open() { + modalInstance = getModalInstance(); + + modalInstance.opened + ..then((v) { + print('Opened'); + }, onError: (e) { + print('Open error is $e'); + }); + + // Override close to add you own functionality + modalInstance.close = (result) { + selected = result; + print('Closed with selection $selected'); + modal.hide(); + }; + // Override dismiss to add you own functionality + modalInstance.dismiss = (String reason) { + print('Dismissed with $reason'); + modal.hide(); + }; + } + + void ok(sel) { + modalInstance.close(sel); + } +} diff --git a/web/modal/modal_demo_embedded_template_static_backdrop.html b/web/modal/modal_demo_embedded_template_static_backdrop.html new file mode 100644 index 0000000..5ee710d --- /dev/null +++ b/web/modal/modal_demo_embedded_template_static_backdrop.html @@ -0,0 +1,15 @@ + + +
+
Modal from embedded template and static backdrop
+
+
+ +
Selected: {{ selected }}
+
+
+
diff --git a/web/modal/modal_demo_template_element.dart b/web/modal/modal_demo_template_element.dart new file mode 100644 index 0000000..a264b57 --- /dev/null +++ b/web/modal/modal_demo_template_element.dart @@ -0,0 +1,43 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +part of angular.ui.demo; + +/** + * Modal controller with template from template element. + */ +@Component( + selector: 'modal-demo-tag-tmpl', + useShadowDom: false, + templateUrl: 'modal/modal_demo_template_element.html', + exportExpressions: const ["open"] +) +class ModalDemoTagTemplate implements ScopeAware { + List items = ["Java", "Dart", "JavaScript", "Ruby"]; + + String selected; + String tmp; + String other; + + Modal modal; + ModalInstance modalInstance; + Scope scope; + + ModalDemoTagTemplate(this.modal); + + void openUrl(String templateUrl) { + modalInstance = modal.open(new ModalOptions(templateUrl:templateUrl), scope); + + modalInstance.result + ..then((value) { + selected = value; + print('Closed with selection $value'); + }, onError:(e) { + print('Dismissed with $e'); + }); + } + + void ok(sel) { + modalInstance.close(sel); + } +} diff --git a/web/modal/modal_demo_template_element.html b/web/modal/modal_demo_template_element.html new file mode 100644 index 0000000..52e6c61 --- /dev/null +++ b/web/modal/modal_demo_template_element.html @@ -0,0 +1,43 @@ + + + + + + +
+
Multi Modal from template element and template URL
+
+
+ +
Selected: {{ selected + ' - ' + other }}
+
+
+
diff --git a/web/modal/modal_demo_template_element_from_other_file.dart b/web/modal/modal_demo_template_element_from_other_file.dart new file mode 100644 index 0000000..7cdd140 --- /dev/null +++ b/web/modal/modal_demo_template_element_from_other_file.dart @@ -0,0 +1,60 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +part of angular.ui.demo; + +/** + * Modal controller with template from file for other. + */ +@Component( + selector: 'modal-demo-other-tmpl', + useShadowDom: false, + templateUrl: 'modal/modal_demo_template_element_from_other_file.html', + exportExpressions: const ["open"] +) +class ModalDemoOtherTemplate implements ScopeAware { + List items = ["Hydrogen", "Lithium", "Oxygen", "Chromium"]; + + String selected; + String tmp; + + Modal modal; + ModalInstance modalInstance; + Scope _scope; + NgModel ngModel; + + ModalDemoOtherTemplate(this.modal, this.ngModel) { + print("!!! modal $modal"); + print("!!! ngModel $ngModel"); + } + + get scope => _scope; + set scope(Scope value) { + // Find ngModel in parent scope + print("!!! Parent ngModel ${value.parentScope.context['ngModel']}"); + // Update local variables from outside +// ngModel.render = (value) { +// selected2 = value; +// }; +// // Update model from inside +// _scope.watch('selected', (newValue, _) { +// ngModel.viewValue = newValue; +// }); + } + + void open(String templateUrl) { + modalInstance = modal.open(new ModalOptions(templateUrl:templateUrl), scope); + + modalInstance.result + ..then((value) { + selected = value; + print('Closed with selection $value'); + }, onError:(e) { + print('Dismissed with $e'); + }); + } + + void ok(sel) { + modalInstance.close(sel); + } +} diff --git a/web/modal/modal_demo_template_element_from_other_file.html b/web/modal/modal_demo_template_element_from_other_file.html new file mode 100644 index 0000000..afb8881 --- /dev/null +++ b/web/modal/modal_demo_template_element_from_other_file.html @@ -0,0 +1,22 @@ + + + + + \ No newline at end of file diff --git a/web/pagination/pagination_demo.dart b/web/pagination/pagination_demo.dart new file mode 100644 index 0000000..6aed832 --- /dev/null +++ b/web/pagination/pagination_demo.dart @@ -0,0 +1,39 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +part of angular.ui.demo; + +/** + * Pagination controller with template. + */ +@Component( + selector: 'pagination-demo', + templateUrl: 'pagination/pagination_demo.html', + useShadowDom: false +) +class PaginationDemo implements ScopeAware { + + Scope scope; + + int totalItems; + int currentPage; + int maxSize; + + int bigTotalItems; + int bigCurrentPage; + + var smallnumPages, numPages; + + PaginationDemo() { + totalItems = 64; + currentPage = 4; + maxSize = 5; + bigTotalItems = 175; + bigCurrentPage = 1; + } + + setPage(int newPage) => currentPage = newPage; + + pageChanged() => null; + +} \ No newline at end of file diff --git a/web/pagination/pagination_demo.html b/web/pagination/pagination_demo.html new file mode 100644 index 0000000..63c7522 --- /dev/null +++ b/web/pagination/pagination_demo.html @@ -0,0 +1,71 @@ + + +
+

Default

+ +
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
The selected page no: {{currentPage}}
+
+
+
+
+ +
+
+ +
+

Pager

+ +
+
+ +
+
+ +
+

Limit the maximum visible buttons

+ +
+
+ +
+
+
+
+ +
+
+
Page: {{bigCurrentPage}} / {{numPages}}
+
\ No newline at end of file diff --git a/web/popover/popover_demo.dart b/web/popover/popover_demo.dart new file mode 100644 index 0000000..79cab51 --- /dev/null +++ b/web/popover/popover_demo.dart @@ -0,0 +1,15 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +part of angular.ui.demo; + +@Component( + selector: 'popover-demo', + useShadowDom: false, + templateUrl: 'popover/popover_demo.html', + exportExpressions: const ["dynamicPopover","dynamicPopoverTitle"]) +class PopoverDemo implements ScopeAware { + Scope scope; + var dynamicPopover = 'Hello, World!'; + var dynamicPopoverTitle = 'Title'; +} \ No newline at end of file diff --git a/web/popover/popover_demo.html b/web/popover/popover_demo.html new file mode 100644 index 0000000..dfad672 --- /dev/null +++ b/web/popover/popover_demo.html @@ -0,0 +1,40 @@ + + +
+

Dynamic

+
+ + +
+
+ + +
+ + +
+

Positional

+
+
+ + + + +
+
+
+

Triggers

+

+ +

+ + +
+

Other

+ + +
diff --git a/web/progressbar/progressbar_demo.dart b/web/progressbar/progressbar_demo.dart new file mode 100644 index 0000000..f86f76d --- /dev/null +++ b/web/progressbar/progressbar_demo.dart @@ -0,0 +1,61 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +part of angular.ui.demo; + +/** + * Progress bar controller. + */ +@Component( + selector: 'progress-demo', + templateUrl: 'progressbar/progressbar_demo.html', + useShadowDom: false +) +class ProgressDemo implements ScopeAware { + + Scope scope; + + math.Random _random = new math.Random(); + + int max = 200; + String type; + int value = 0; + int dynamic; + + var stacked = []; + var showWarning; + + void random() { + value = ((_random.nextDouble() * 100).floor() + 1); + dynamic = value; + + if (value < 25) { + type = 'success'; + } else if (value < 50) { + type = 'info'; + } else if (value < 75) { + type = 'warning'; + } else { + type = 'danger'; + } + + showWarning = (type == 'danger' || type == 'warning'); + } + + void randomStacked() { + stacked = []; + var types = ['success', 'info', 'warning', 'danger']; + + for (var i = 0, n = ((_random.nextDouble() * 4).floor() + 1); i < n; i++) { + var index = ((_random.nextDouble() * 4)).floor(); + stacked.add({ + 'value': ((_random.nextDouble() * 30) + 1).floor(), 'type': types[index] + }); + } + } + + ProgressDemo() { + randomStacked(); + random(); + } +} \ No newline at end of file diff --git a/web/progressbar/progressbar_demo.html b/web/progressbar/progressbar_demo.html new file mode 100644 index 0000000..bf2950d --- /dev/null +++ b/web/progressbar/progressbar_demo.html @@ -0,0 +1,28 @@ + + +
+

Static

+
+
+
22%
+
166 / 200
+
+ +
+

Dynamic

+ {{dynamic}} / {{max}} + + No animation + {{dynamic}}% + + Object (changes type based on value) + {{type}} !!! Watch out !!! + +
+

Stacked

+ {{bar.value}}% +
\ No newline at end of file diff --git a/web/rating/rating_demo.dart b/web/rating/rating_demo.dart new file mode 100644 index 0000000..712a79e --- /dev/null +++ b/web/rating/rating_demo.dart @@ -0,0 +1,43 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +part of angular.ui.demo; + +/** + * Rating bar component. + */ +@Component( + selector: 'rating-demo', + templateUrl: 'rating/rating_demo.html', + useShadowDom: false, + exportExpressions: const ['customRatingStates'] +) +class RatingDemo implements ScopeAware { + + Scope scope; + + int rate = 7; + int max = 10; + bool isReadonly = false; + int overStar; + double percent = 100.0; + int x = 5; + int y = 2; + + List> customRatingStates = [ + {'stateOn': 'glyphicon-ok-sign', 'stateOff': 'glyphicon-ok-circle'}, + {'stateOn': 'glyphicon-star', 'stateOff': 'glyphicon-star-empty'}, + {'stateOn': 'glyphicon-heart', 'stateOff': 'glyphicon-ban-circle'}, + {'stateOn': 'glyphicon-heart'}, + {'stateOff': 'glyphicon-off'} + ]; + + void hoveringOver(int value) { + if(value == null || value == 0.0) { + percent = 0.0; + } else { + overStar = value; + percent = 100 * (value / max); + } + } +} \ No newline at end of file diff --git a/web/rating/rating_demo.html b/web/rating/rating_demo.html new file mode 100644 index 0000000..0556237 --- /dev/null +++ b/web/rating/rating_demo.html @@ -0,0 +1,21 @@ + + +
+

Default

+ + {{percent}}% + +
Rate: {{rate}} - Readonly is: {{isReadonly}} - Hovering over: {{overStar != null ? overStar : "none"}}
+ + + +
+ +

Custom icons

+
(Rate:{{x}})
+
(Rate:{{y}})
+
diff --git a/web/tabs/tabs_demo.dart b/web/tabs/tabs_demo.dart new file mode 100644 index 0000000..0320163 --- /dev/null +++ b/web/tabs/tabs_demo.dart @@ -0,0 +1,21 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +part of angular.ui.demo; + +@Component( + selector: 'tabs-demo', + templateUrl: 'tabs/tabs_demo.html', + useShadowDom: false) +class TabsDemo implements ScopeAware { + Scope scope; + + List tabs = [ + { 'title':'Dynamic Title 1', 'content':'Dynamic content 1', 'active':false, 'disabled': false }, + { 'title':'Dynamic Title 2', 'content':'Dynamic content 2', 'active':false, 'disabled': true } + ]; + + void alertMe() { + dom.window.alert('You\'ve selected the alert tab!'); + } +} diff --git a/web/tabs/tabs_demo.html b/web/tabs/tabs_demo.html new file mode 100644 index 0000000..4bc9f9a --- /dev/null +++ b/web/tabs/tabs_demo.html @@ -0,0 +1,46 @@ + + + +

Select a tab by setting active binding to true:

+

+ + +

+

+ +

+
+ + + Static content + + {{tab.content}} + + + + Alert! + + I've got an HTML heading, and a select callback. Pretty cool! + + + + +
+ + + Vertical content 1 + Vertical content 2 + + +
+ + + Justified content + Short Labeled Justified content + Long Labeled Justified content + + diff --git a/web/timepicker/timepicker_demo.dart b/web/timepicker/timepicker_demo.dart new file mode 100644 index 0000000..f715b7d --- /dev/null +++ b/web/timepicker/timepicker_demo.dart @@ -0,0 +1,44 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +part of angular.ui.demo; + +@Component( + selector: 'timepicker-demo', + templateUrl: 'timepicker/timepicker_demo.html', + exportExpressions: const ["ismeridian"], + useShadowDom: false) +class TimepickerDemo implements ScopeAware { + + Scope scope; + + DateTime mytime = new DateTime.now(); + + int hstep = 1; + int mstep = 15; + + Map options = { + 'hstep': [1, 2, 3], + 'mstep': [1, 5, 10, 15, 25, 30] + }; + + bool ismeridian = true; + void toggleMode() { + ismeridian = !ismeridian; + } + + void update() { + var d = new DateTime.now(); + mytime = new DateTime(d.year, d.month, d.day, 14, 0); + } + + void changed() { + print('Time changed to: $mytime'); + } + + void clear() { + mytime = null; + } + + +} diff --git a/web/timepicker/timepicker_demo.html b/web/timepicker/timepicker_demo.html new file mode 100644 index 0000000..165c9ca --- /dev/null +++ b/web/timepicker/timepicker_demo.html @@ -0,0 +1,38 @@ + + +
+ +
+ +
+ + +
Time is: {{mytime | date:'shortTime' }}
+ +
+
+ Hours step is: + +
+
+ Minutes step is: + +
+
+ +
+ + + + + +
diff --git a/web/tooltip/tooltip_demo.dart b/web/tooltip/tooltip_demo.dart new file mode 100644 index 0000000..69a4616 --- /dev/null +++ b/web/tooltip/tooltip_demo.dart @@ -0,0 +1,17 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +part of angular.ui.demo; + +@Component( + selector: 'tooltip-demo', + templateUrl: "tooltip/tooltip_demo.html", + useShadowDom: false) +class TooltipDemo implements ScopeAware { + + Scope scope; + + var dynamicTooltip = 'Hello, World!'; + var dynamicTooltipText = 'dynamic'; + var htmlTooltip = 'I\'ve been made bold!'; +} \ No newline at end of file diff --git a/web/tooltip/tooltip_demo.html b/web/tooltip/tooltip_demo.html new file mode 100644 index 0000000..e735fbe --- /dev/null +++ b/web/tooltip/tooltip_demo.html @@ -0,0 +1,45 @@ + + +
+
+ + +
+
+ + +
+

+ Pellentesque {{dynamicTooltipText}}, + sit amet venenatis urna cursus eget nunc scelerisque viverra mauris, in + aliquam. Tincidunt lobortis feugiat vivamus at + left eget + arcu dictum varius duis at consectetur lorem. Vitae elementum curabitur + right + nunc sed velit dignissim sodales ut eu sem integer vitae. Turpis egestas + bottom + pharetra convallis posuere morbi leo urna, + fading + at elementum eu, facilisis sed odio morbi quis commodo odio. In cursus + delayed turpis massa tincidunt dui ut. +

+ +

+ I can even contain HTML. Check me out! +

+ +
+
+
+
+ + +
+
+
+ +
\ No newline at end of file diff --git a/web/typeahead/typeahead_demo.dart b/web/typeahead/typeahead_demo.dart new file mode 100644 index 0000000..bd5337a --- /dev/null +++ b/web/typeahead/typeahead_demo.dart @@ -0,0 +1,41 @@ +// Copyright (C) 2013 - 2016 Angular Dart UI authors. Please see AUTHORS.md. +// https://github.com/akserg/angular.dart.ui +// All rights reserved. Please see the LICENSE.md file. +part of angular.ui.demo; + + +@Component( + selector: 'typeahead-demo', + templateUrl: 'typeahead/typeahead_demo.html', + useShadowDom: false, + exportExpressions: const ["states", r"$viewValue", "state", "statesWithFlags", "getLocation", "address", "popupId", "activeIdx"] +) +class TypeaheadDemo implements ScopeAware { + + Scope scope; + + Http _http; + var selected; + var asyncSelected; + var customSelected; + + bool loadingLocations = false; + + var states = ['Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorado', 'Connecticut', 'Delaware', 'Florida', 'Georgia', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas', 'Kentucky', 'Louisiana', 'Maine', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', 'Mississippi', 'Missouri', 'Montana', 'Nebraska', 'Nevada', 'New Hampshire', 'New Jersey', 'New Mexico', 'New York', 'North Dakota', 'North Carolina', 'Ohio', 'Oklahoma', 'Oregon', 'Pennsylvania', 'Rhode Island', 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont', 'Virginia', 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming']; + var statesWithFlags = [{'name':'Alabama','flag':'5/5c/Flag_of_Alabama.svg/45px-Flag_of_Alabama.svg.png'},{'name':'Alaska','flag':'e/e6/Flag_of_Alaska.svg/43px-Flag_of_Alaska.svg.png'},{'name':'Arizona','flag':'9/9d/Flag_of_Arizona.svg/45px-Flag_of_Arizona.svg.png'},{'name':'Arkansas','flag':'9/9d/Flag_of_Arkansas.svg/45px-Flag_of_Arkansas.svg.png'},{'name':'California','flag':'0/01/Flag_of_California.svg/45px-Flag_of_California.svg.png'},{'name':'Colorado','flag':'4/46/Flag_of_Colorado.svg/45px-Flag_of_Colorado.svg.png'},{'name':'Connecticut','flag':'9/96/Flag_of_Connecticut.svg/39px-Flag_of_Connecticut.svg.png'},{'name':'Delaware','flag':'c/c6/Flag_of_Delaware.svg/45px-Flag_of_Delaware.svg.png'},{'name':'Florida','flag':'f/f7/Flag_of_Florida.svg/45px-Flag_of_Florida.svg.png'},{'name':'Georgia','flag':'5/54/Flag_of_Georgia_%28U.S._state%29.svg/46px-Flag_of_Georgia_%28U.S._state%29.svg.png'},{'name':'Hawaii','flag':'e/ef/Flag_of_Hawaii.svg/46px-Flag_of_Hawaii.svg.png'},{'name':'Idaho','flag':'a/a4/Flag_of_Idaho.svg/38px-Flag_of_Idaho.svg.png'},{'name':'Illinois','flag':'0/01/Flag_of_Illinois.svg/46px-Flag_of_Illinois.svg.png'},{'name':'Indiana','flag':'a/ac/Flag_of_Indiana.svg/45px-Flag_of_Indiana.svg.png'},{'name':'Iowa','flag':'a/aa/Flag_of_Iowa.svg/44px-Flag_of_Iowa.svg.png'},{'name':'Kansas','flag':'d/da/Flag_of_Kansas.svg/46px-Flag_of_Kansas.svg.png'},{'name':'Kentucky','flag':'8/8d/Flag_of_Kentucky.svg/46px-Flag_of_Kentucky.svg.png'},{'name':'Louisiana','flag':'e/e0/Flag_of_Louisiana.svg/46px-Flag_of_Louisiana.svg.png'},{'name':'Maine','flag':'3/35/Flag_of_Maine.svg/45px-Flag_of_Maine.svg.png'},{'name':'Maryland','flag':'a/a0/Flag_of_Maryland.svg/45px-Flag_of_Maryland.svg.png'},{'name':'Massachusetts','flag':'f/f2/Flag_of_Massachusetts.svg/46px-Flag_of_Massachusetts.svg.png'},{'name':'Michigan','flag':'b/b5/Flag_of_Michigan.svg/45px-Flag_of_Michigan.svg.png'},{'name':'Minnesota','flag':'b/b9/Flag_of_Minnesota.svg/46px-Flag_of_Minnesota.svg.png'},{'name':'Mississippi','flag':'4/42/Flag_of_Mississippi.svg/45px-Flag_of_Mississippi.svg.png'},{'name':'Missouri','flag':'5/5a/Flag_of_Missouri.svg/46px-Flag_of_Missouri.svg.png'},{'name':'Montana','flag':'c/cb/Flag_of_Montana.svg/45px-Flag_of_Montana.svg.png'},{'name':'Nebraska','flag':'4/4d/Flag_of_Nebraska.svg/46px-Flag_of_Nebraska.svg.png'},{'name':'Nevada','flag':'f/f1/Flag_of_Nevada.svg/45px-Flag_of_Nevada.svg.png'},{'name':'New Hampshire','flag':'2/28/Flag_of_New_Hampshire.svg/45px-Flag_of_New_Hampshire.svg.png'},{'name':'New Jersey','flag':'9/92/Flag_of_New_Jersey.svg/45px-Flag_of_New_Jersey.svg.png'},{'name':'New Mexico','flag':'c/c3/Flag_of_New_Mexico.svg/45px-Flag_of_New_Mexico.svg.png'},{'name':'New York','flag':'1/1a/Flag_of_New_York.svg/46px-Flag_of_New_York.svg.png'},{'name':'North Carolina','flag':'b/bb/Flag_of_North_Carolina.svg/45px-Flag_of_North_Carolina.svg.png'},{'name':'North Dakota','flag':'e/ee/Flag_of_North_Dakota.svg/38px-Flag_of_North_Dakota.svg.png'},{'name':'Ohio','flag':'4/4c/Flag_of_Ohio.svg/46px-Flag_of_Ohio.svg.png'},{'name':'Oklahoma','flag':'6/6e/Flag_of_Oklahoma.svg/45px-Flag_of_Oklahoma.svg.png'},{'name':'Oregon','flag':'b/b9/Flag_of_Oregon.svg/46px-Flag_of_Oregon.svg.png'},{'name':'Pennsylvania','flag':'f/f7/Flag_of_Pennsylvania.svg/45px-Flag_of_Pennsylvania.svg.png'},{'name':'Rhode Island','flag':'f/f3/Flag_of_Rhode_Island.svg/32px-Flag_of_Rhode_Island.svg.png'},{'name':'South Carolina','flag':'6/69/Flag_of_South_Carolina.svg/45px-Flag_of_South_Carolina.svg.png'},{'name':'South Dakota','flag':'1/1a/Flag_of_South_Dakota.svg/46px-Flag_of_South_Dakota.svg.png'},{'name':'Tennessee','flag':'9/9e/Flag_of_Tennessee.svg/46px-Flag_of_Tennessee.svg.png'},{'name':'Texas','flag':'f/f7/Flag_of_Texas.svg/45px-Flag_of_Texas.svg.png'},{'name':'Utah','flag':'f/f6/Flag_of_Utah.svg/45px-Flag_of_Utah.svg.png'},{'name':'Vermont','flag':'4/49/Flag_of_Vermont.svg/46px-Flag_of_Vermont.svg.png'},{'name':'Virginia','flag':'4/47/Flag_of_Virginia.svg/44px-Flag_of_Virginia.svg.png'},{'name':'Washington','flag':'5/54/Flag_of_Washington.svg/46px-Flag_of_Washington.svg.png'},{'name':'West Virginia','flag':'2/22/Flag_of_West_Virginia.svg/46px-Flag_of_West_Virginia.svg.png'},{'name':'Wisconsin','flag':'2/22/Flag_of_Wisconsin.svg/45px-Flag_of_Wisconsin.svg.png'},{'name':'Wyoming','flag':'b/bc/Flag_of_Wyoming.svg/43px-Flag_of_Wyoming.svg.png'}]; + + TypeaheadDemo(this._http); + + getLocation(val) { + + return _http.get('http://maps.googleapis.com/maps/api/geocode/json', params: {'address' : val, 'sensor': false}).then((res){ + var addresses = []; + + res.data['results'].forEach((item){ + addresses.add(item['formatted_address']); + }); + + return addresses; + }); + } +} \ No newline at end of file diff --git a/web/typeahead/typeahead_demo.html b/web/typeahead/typeahead_demo.html new file mode 100644 index 0000000..eac6d9b --- /dev/null +++ b/web/typeahead/typeahead_demo.html @@ -0,0 +1,27 @@ + + + +
+

Static arrays of states (USA only)

+
Model: {{selected | json}}
+ + +

Custom templates for results of states (USA only)

+
Model: {{customSelected | json}}
+ + +

Asynchronous results of states and countries from Google Maps

+
Model: {{asyncSelected | json}}
+ + +
\ No newline at end of file