diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 9e92540ee5..6b74877384 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,15 +1,5 @@ -The issues forum is __NOT__ for support requests. It is for bugs and feature requests only. -Please read https://github.com/angular-ui/bootstrap/blob/master/CONTRIBUTING.md and search -existing issues (both open and closed) prior to opening any new issue and ensure you follow the instructions therein. +# PLEASE READ -### Bug description: +As per the [README](https://github.com/angular-ui/bootstrap/blob/master/README.md), this project is no longer being maintained. Any issues entered will remain uninvestigated and unresolved. -### Link to minimally-working plunker that reproduces the issue: - -### Version of Angular, UIBS, and Bootstrap - -Angular: - -UIBS: - -Bootstrap: +We thank you for your contributions over the years. This library would not have been successful without them. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..676cdb40f9 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,5 @@ +# PLEASE READ + +As per the [README](https://github.com/angular-ui/bootstrap/blob/master/README.md), this project is no longer being maintained. Any PRs entered will not be reviewed or merged and will remain open. + +We thank you for your contributions over the years. This library would not have been successful without them. diff --git a/.travis.yml b/.travis.yml index fba6738247..e69e4c76f0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ node_js: - "5.9" env: - CXX=g++-4.8 +dist: trusty addons: apt: sources: @@ -13,7 +14,7 @@ addons: before_install: - export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb start - - npm install --quiet -g grunt-cli karma + - npm install --quiet -g karma script: grunt sudo: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 4376216c31..56e5e8e6ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,246 @@ + +# [2.5.0](https://github.com/angular-ui/bootstrap/compare/2.4.0...v2.5.0) (2017-01-28) + + +### Bug Fixes + +* **angular:** add compatibility with Angular 1.6([0d79005](https://github.com/angular-ui/bootstrap/commit/0d79005)), closes [#6427](https://github.com/angular-ui/bootstrap/issues/6427) [#6360](https://github.com/angular-ui/bootstrap/issues/6360) +* **carousel:** remove transition buffering([86ee770](https://github.com/angular-ui/bootstrap/commit/86ee770)), closes [#6367](https://github.com/angular-ui/bootstrap/issues/6367) [#5967](https://github.com/angular-ui/bootstrap/issues/5967) +* **dropdown:** do nothing if not open when clicking([761db7b](https://github.com/angular-ui/bootstrap/commit/761db7b)), closes [#6414](https://github.com/angular-ui/bootstrap/issues/6414) +* **tooltip:** unbind keypress listener on hide([f5b357f](https://github.com/angular-ui/bootstrap/commit/f5b357f)), closes [#6423](https://github.com/angular-ui/bootstrap/issues/6423) [#6405](https://github.com/angular-ui/bootstrap/issues/6405) + + +### Features + +* **dropdown:** make dropdown-append-to-body configurable ([#6356](https://github.com/angular-ui/bootstrap/issues/6356))([7d3a750](https://github.com/angular-ui/bootstrap/commit/7d3a750)) +* **pagination:** Added menu and menuitem roles (closes [#6383](https://github.com/angular-ui/bootstrap/issues/6383)) ([#6386](https://github.com/angular-ui/bootstrap/issues/6386))([71dc691](https://github.com/angular-ui/bootstrap/commit/71dc691)), closes [#6383](https://github.com/angular-ui/bootstrap/issues/6383) [(#6386](https://github.com/(/issues/6386) + + + + +# [2.4.0](https://github.com/angular-ui/bootstrap/compare/2.3.2...v2.4.0) (2016-12-30) + + +### Features + +* **dateparser:** allow overriding of parsers([5a3e44a](https://github.com/angular-ui/bootstrap/commit/5a3e44a)), closes [#6370](https://github.com/angular-ui/bootstrap/issues/6370) [#6373](https://github.com/angular-ui/bootstrap/issues/6373) + + + + +## [2.3.2](https://github.com/angular-ui/bootstrap/compare/2.3.1...v2.3.2) (2016-12-27) + + +### Bug Fixes + +* **dropdown:** re-add close([955848c](https://github.com/angular-ui/bootstrap/commit/955848c)), closes [#6382](https://github.com/angular-ui/bootstrap/issues/6382) [#6321](https://github.com/angular-ui/bootstrap/issues/6321) [#6357](https://github.com/angular-ui/bootstrap/issues/6357) [#6364](https://github.com/angular-ui/bootstrap/issues/6364) + + + + +## [2.3.1](https://github.com/angular-ui/bootstrap/compare/2.3.0...v2.3.1) (2016-12-10) + + +### Bug Fixes + +* **dateparser:** add new date format for angular 1.5+ only([f2722b5](https://github.com/angular-ui/bootstrap/commit/f2722b5)), closes [#6349](https://github.com/angular-ui/bootstrap/issues/6349) + +* **datepickerPopup:** change to toTimezone only([1962485](https://github.com/angular-ui/bootstrap/commit/1962485)), fixes [#6235](https://github.com/angular-ui/bootstrap/issues/6235) + +* **modal:** revert focus behavior on open([8a4f625](https://github.com/angular-ui/bootstrap/commit/8a4f625)), closes [#6295](https://github.com/angular-ui/bootstrap/issues/6295) + + +# [2.3.0](https://github.com/angular-ui/bootstrap/compare/2.2.0...2.3.0) (2016-11-26) + + +### Features + +* **dateparser:** add LLLL support([25ff206](https://github.com/angular-ui/bootstrap/commit/25ff206)), closes [#6281](https://github.com/angular-ui/bootstrap/issues/6281) + + + + +# [2.2.0](https://github.com/angular-ui/bootstrap/compare/2.1.4...2.2.0) (2016-10-10) + + +### Bug Fixes + +* **dropdown:** exit keybind is not open ([14384fc](https://github.com/angular-ui/bootstrap/commit/14384fc)), closes [#6278](https://github.com/angular-ui/bootstrap/issues/6278) [#6208](https://github.com/angular-ui/bootstrap/issues/6208) +* **modal:** improve ARIA support. ([f9f7e02](https://github.com/angular-ui/bootstrap/commit/f9f7e02)), closes [#6203](https://github.com/angular-ui/bootstrap/issues/6203) +* **position:** correct scrollbar width calculation ([58f1813](https://github.com/angular-ui/bootstrap/commit/58f1813)), closes [#6273](https://github.com/angular-ui/bootstrap/issues/6273) +* **tooltip:** cancel timeout when hidden ([7855976](https://github.com/angular-ui/bootstrap/commit/7855976)), closes [#6226](https://github.com/angular-ui/bootstrap/issues/6226) [#6221](https://github.com/angular-ui/bootstrap/issues/6221) + +### Features + +* **timepicker:** add validation information ([9666c64](https://github.com/angular-ui/bootstrap/commit/9666c64)), closes [#6230](https://github.com/angular-ui/bootstrap/issues/6230) [#6259](https://github.com/angular-ui/bootstrap/issues/6259) + + + + +## [2.1.4](https://github.com/angular-ui/bootstrap/compare/2.1.3...2.1.4) (2016-09-24) + + +### Bug Fixes + +* **datepicker:** improve accessibility ([3f70d76](https://github.com/angular-ui/bootstrap/commit/3f70d76)), closes [#6247](https://github.com/angular-ui/bootstrap/issues/6247) +* **dropdown:** prevent premature scope removal ([08ee30a](https://github.com/angular-ui/bootstrap/commit/08ee30a)), closes [#6238](https://github.com/angular-ui/bootstrap/issues/6238) [#6225](https://github.com/angular-ui/bootstrap/issues/6225) + + + + +## [2.1.3](https://github.com/angular-ui/bootstrap/compare/2.1.2...2.1.3) (2016-08-25) + + +### Bug Fixes + +* **modal:** compile only once with component ([969eb9c](https://github.com/angular-ui/bootstrap/commit/969eb9c)), closes [#6202](https://github.com/angular-ui/bootstrap/issues/6202) [#6201](https://github.com/angular-ui/bootstrap/issues/6201) + + + + +## [2.1.2](https://github.com/angular-ui/bootstrap/compare/2.1.1...2.1.2) (2016-08-22) + + +### Bug Fixes + +* **collapse:** revert change to transition css ([515bcf2](https://github.com/angular-ui/bootstrap/commit/515bcf2)), closes [#6196](https://github.com/angular-ui/bootstrap/issues/6196) [#6194](https://github.com/angular-ui/bootstrap/issues/6194) +* **datepicker:** fix accidental global ([ddcacb7](https://github.com/angular-ui/bootstrap/commit/ddcacb7)), closes [#6188](https://github.com/angular-ui/bootstrap/issues/6188) +* **modal:** close and dismiss bindings on component ([3e8ecff](https://github.com/angular-ui/bootstrap/commit/3e8ecff)), closes [#6192](https://github.com/angular-ui/bootstrap/issues/6192) [#6191](https://github.com/angular-ui/bootstrap/issues/6191) + + + + +## [2.1.1](https://github.com/angular-ui/bootstrap/compare/2.1.0...2.1.1) (2016-08-21) + + +### Bug Fixes + +* **collapse:** default to css([aef24cd](https://github.com/angular-ui/bootstrap/commit/aef24cd)), closes [#6182](https://github.com/angular-ui/bootstrap/issues/6182) [#6045](https://github.com/angular-ui/bootstrap/issues/6045) +* **modal:** switch to .append([fb5fabf](https://github.com/angular-ui/bootstrap/commit/fb5fabf)), closes [#6187](https://github.com/angular-ui/bootstrap/issues/6187) [#6186](https://github.com/angular-ui/bootstrap/issues/6186) + + + + +# [2.1.0](https://github.com/angular-ui/bootstrap/compare/2.0.2...2.1.0) (2016-08-19) + + +### Bug Fixes + +* **collapse:** remove unnecessary inherit ([ca20be4](https://github.com/angular-ui/bootstrap/commit/ca20be4)), closes [#6164](https://github.com/angular-ui/bootstrap/issues/6164) [#6163](https://github.com/angular-ui/bootstrap/issues/6163) +* **collapse:** set overflow to hidden on transition ([84cc2cf](https://github.com/angular-ui/bootstrap/commit/84cc2cf)), closes [#6180](https://github.com/angular-ui/bootstrap/issues/6180) [#5474](https://github.com/angular-ui/bootstrap/issues/5474) +* **datepickerPopup:** apply timezone conversion ([f147d22](https://github.com/angular-ui/bootstrap/commit/f147d22)), closes [#6173](https://github.com/angular-ui/bootstrap/issues/6173) [#6147](https://github.com/angular-ui/bootstrap/issues/6147) +* **modal:** improve ARIA support ([4a5e6a7](https://github.com/angular-ui/bootstrap/commit/4a5e6a7)), closes [#4772](https://github.com/angular-ui/bootstrap/issues/4772) +* **tooltip:** close tooltip on esc ([f5ff12c](https://github.com/angular-ui/bootstrap/commit/f5ff12c)), closes [#6177](https://github.com/angular-ui/bootstrap/issues/6177) [#6108](https://github.com/angular-ui/bootstrap/issues/6108) + +### Features + +* **modal:** add component support ([2ade054](https://github.com/angular-ui/bootstrap/commit/2ade054)), closes [#5683](https://github.com/angular-ui/bootstrap/issues/5683) [#6179](https://github.com/angular-ui/bootstrap/issues/6179) + + + + +## [2.0.2](https://github.com/angular-ui/bootstrap/compare/2.0.1...2.0.2) (2016-08-15) + + +### Bug Fixes + +* **datepickerPopup:** correctly format to timezone ([fbd0845](https://github.com/angular-ui/bootstrap/commit/fbd0845)), closes [#6159](https://github.com/angular-ui/bootstrap/issues/6159) [#6105](https://github.com/angular-ui/bootstrap/issues/6105) [#6146](https://github.com/angular-ui/bootstrap/issues/6146) [#6147](https://github.com/angular-ui/bootstrap/issues/6147) +* **dropdown:** fix keyboard-nav ([6bad759](https://github.com/angular-ui/bootstrap/commit/6bad759)), closes [#6102](https://github.com/angular-ui/bootstrap/issues/6102) [#6154](https://github.com/angular-ui/bootstrap/issues/6154) + + + + +## [2.0.1](https://github.com/angular-ui/bootstrap/compare/2.0.0...2.0.1) (2016-08-02) + + +### Bug Fixes + +* **modal:** restore broken stacked modals([c61d16a](https://github.com/angular-ui/bootstrap/commit/c61d16a)), closes [#6103](https://github.com/angular-ui/bootstrap/issues/6103) [#6104](https://github.com/angular-ui/bootstrap/issues/6104) + + + + +# [2.0.0](https://github.com/angular-ui/bootstrap/compare/1.3.3...2.0.0) (2016-07-20) + + +### Bug Fixes + +* **dateparser:** correctly format with literals([d846e2d](https://github.com/angular-ui/bootstrap/commit/d846e2d)), closes [#6055](https://github.com/angular-ui/bootstrap/issues/6055) [#5620](https://github.com/angular-ui/bootstrap/issues/5620) [#5802](https://github.com/angular-ui/bootstrap/issues/5802) +* **datepickerPopup:** clear date when button is clicked([b0466d9](https://github.com/angular-ui/bootstrap/commit/b0466d9)), closes [#5945](https://github.com/angular-ui/bootstrap/issues/5945) [#5906](https://github.com/angular-ui/bootstrap/issues/5906) +* **datepickerPopup:** specify dependency on datepicker([4fef037](https://github.com/angular-ui/bootstrap/commit/4fef037)), closes [#5954](https://github.com/angular-ui/bootstrap/issues/5954) +* **datepickerPopup:** use value instead of viewValue([7e320e0](https://github.com/angular-ui/bootstrap/commit/7e320e0)), closes [#6007](https://github.com/angular-ui/bootstrap/issues/6007) +* **dropdown:** align position correctly with scrollbar([2d831bc](https://github.com/angular-ui/bootstrap/commit/2d831bc)), closes [#6008](https://github.com/angular-ui/bootstrap/issues/6008) [#5942](https://github.com/angular-ui/bootstrap/issues/5942) +* **dropdown:** bind event listener on dropdown menu([6038403](https://github.com/angular-ui/bootstrap/commit/6038403)), closes [#5982](https://github.com/angular-ui/bootstrap/issues/5982) +* **modal:** check for overflow hidden([433e536](https://github.com/angular-ui/bootstrap/commit/433e536)), closes [#6037](https://github.com/angular-ui/bootstrap/issues/6037) [#6041](https://github.com/angular-ui/bootstrap/issues/6041) +* **modal:** filter out non-tabbable elements([35ced04](https://github.com/angular-ui/bootstrap/commit/35ced04)), closes [#5963](https://github.com/angular-ui/bootstrap/issues/5963) [#5955](https://github.com/angular-ui/bootstrap/issues/5955) +* **modal:** remove unused template from modal([1de58a3](https://github.com/angular-ui/bootstrap/commit/1de58a3)) +* **modal:** remove window class after animation([409b7aa](https://github.com/angular-ui/bootstrap/commit/409b7aa)), closes [#6056](https://github.com/angular-ui/bootstrap/issues/6056) [#6051](https://github.com/angular-ui/bootstrap/issues/6051) +* **tooltip:** missed dependency for cjs([164811a](https://github.com/angular-ui/bootstrap/commit/164811a)), closes [#5999](https://github.com/angular-ui/bootstrap/issues/5999) +* **tooltip:** reposition for placement top([34a1443](https://github.com/angular-ui/bootstrap/commit/34a1443)), closes [#5959](https://github.com/angular-ui/bootstrap/issues/5959) +* **typeahead:** change to select class([13c14af](https://github.com/angular-ui/bootstrap/commit/13c14af)), closes [#5922](https://github.com/angular-ui/bootstrap/issues/5922) [#5848](https://github.com/angular-ui/bootstrap/issues/5848) +* **typeahead:** clear validity in $digest([ed3400b](https://github.com/angular-ui/bootstrap/commit/ed3400b)), closes [#6033](https://github.com/angular-ui/bootstrap/issues/6033) [#6032](https://github.com/angular-ui/bootstrap/issues/6032) +* **typeahead:** remove duplicate id attribute([6d5b84a](https://github.com/angular-ui/bootstrap/commit/6d5b84a)), closes [#5936](https://github.com/angular-ui/bootstrap/issues/5936) [#5926](https://github.com/angular-ui/bootstrap/issues/5926) + + +### Features + +* **accordion:** add appropriate tabindex on disabled([5f4eedd](https://github.com/angular-ui/bootstrap/commit/5f4eedd)), closes [#4067](https://github.com/angular-ui/bootstrap/issues/4067) [#5990](https://github.com/angular-ui/bootstrap/issues/5990) +* **accordion:** remove replace: true usage([3819bbe](https://github.com/angular-ui/bootstrap/commit/3819bbe)), closes [#5985](https://github.com/angular-ui/bootstrap/issues/5985) +* **alert:** remove replace: true usage([2458c28](https://github.com/angular-ui/bootstrap/commit/2458c28)), closes [#5986](https://github.com/angular-ui/bootstrap/issues/5986) +* **carousel:** remove replace: true usage([5046bd4](https://github.com/angular-ui/bootstrap/commit/5046bd4)), closes [#5987](https://github.com/angular-ui/bootstrap/issues/5987) +* **collapse:** add horizontal support([1ec0997](https://github.com/angular-ui/bootstrap/commit/1ec0997)), closes [#3593](https://github.com/angular-ui/bootstrap/issues/3593) [#6010](https://github.com/angular-ui/bootstrap/issues/6010) +* **datepicker:** add monthColumns([39d9b98](https://github.com/angular-ui/bootstrap/commit/39d9b98)), closes [#5515](https://github.com/angular-ui/bootstrap/issues/5515) [#5935](https://github.com/angular-ui/bootstrap/issues/5935) +* **datepicker:** add ngModelOptions support([23b91d9](https://github.com/angular-ui/bootstrap/commit/23b91d9)), closes [#5933](https://github.com/angular-ui/bootstrap/issues/5933) [#5825](https://github.com/angular-ui/bootstrap/issues/5825) +* **datepicker:** remove replace: true usage([e92fb7f](https://github.com/angular-ui/bootstrap/commit/e92fb7f)), closes [#5988](https://github.com/angular-ui/bootstrap/issues/5988) +* **datepickerPopup:** remove replace usage([a47bced](https://github.com/angular-ui/bootstrap/commit/a47bced)), closes [#5993](https://github.com/angular-ui/bootstrap/issues/5993) +* **dropdown:** focus toggle on close from click([cce0097](https://github.com/angular-ui/bootstrap/commit/cce0097)), closes [#5934](https://github.com/angular-ui/bootstrap/issues/5934) [#5782](https://github.com/angular-ui/bootstrap/issues/5782) +* **dropdown:** use .value() for uibDropdownConfig([7457fb0](https://github.com/angular-ui/bootstrap/commit/7457fb0)), closes [#6004](https://github.com/angular-ui/bootstrap/issues/6004) [#6006](https://github.com/angular-ui/bootstrap/issues/6006) +* **modal:** append using $animate([1cbd73d](https://github.com/angular-ui/bootstrap/commit/1cbd73d)), closes [#6023](https://github.com/angular-ui/bootstrap/issues/6023) [#6029](https://github.com/angular-ui/bootstrap/issues/6029) +* **modal:** remove replace usage([96d52ce](https://github.com/angular-ui/bootstrap/commit/96d52ce)), closes [#5989](https://github.com/angular-ui/bootstrap/issues/5989) +* **pager:** remove replace usage([9b24e1d](https://github.com/angular-ui/bootstrap/commit/9b24e1d)), closes [#5991](https://github.com/angular-ui/bootstrap/issues/5991) +* **pager:** toggle tabindex when disabled([0d8cec6](https://github.com/angular-ui/bootstrap/commit/0d8cec6)), closes [#5996](https://github.com/angular-ui/bootstrap/issues/5996) +* **pagination:** disable tabbing when disabled([1a870a3](https://github.com/angular-ui/bootstrap/commit/1a870a3)), closes [#5984](https://github.com/angular-ui/bootstrap/issues/5984) +* **pagination:** remove replace usage([387c6e7](https://github.com/angular-ui/bootstrap/commit/387c6e7)), closes [#5992](https://github.com/angular-ui/bootstrap/issues/5992) +* **rating:** remove replace usage([d6fe9c7](https://github.com/angular-ui/bootstrap/commit/d6fe9c7)), closes [#5998](https://github.com/angular-ui/bootstrap/issues/5998) +* **stackedMap:** improve perf of removeTop([a075824](https://github.com/angular-ui/bootstrap/commit/a075824)), closes [#5925](https://github.com/angular-ui/bootstrap/issues/5925) [#5932](https://github.com/angular-ui/bootstrap/issues/5932) +* **timepicker:** avoid allowing to tab to controls([4e68778](https://github.com/angular-ui/bootstrap/commit/4e68778)), closes [#5930](https://github.com/angular-ui/bootstrap/issues/5930) +* **timepicker:** remove replace usage([7518821](https://github.com/angular-ui/bootstrap/commit/7518821)), closes [#5997](https://github.com/angular-ui/bootstrap/issues/5997) +* **tooltip:** add expression support([4b91222](https://github.com/angular-ui/bootstrap/commit/4b91222)), closes [#5221](https://github.com/angular-ui/bootstrap/issues/5221) [#5938](https://github.com/angular-ui/bootstrap/issues/5938) +* **tooltip:** remove replace usage([1616e97](https://github.com/angular-ui/bootstrap/commit/1616e97)), closes [#5994](https://github.com/angular-ui/bootstrap/issues/5994) + + +### Reverts + +* **dropdown:** change back to .constant()([4e0e34f](https://github.com/angular-ui/bootstrap/commit/4e0e34f)) + + +### BREAKING CHANGES + +* tooltip: The template structure changed slightly due to the removal of `replace: true` - see documentation examples in action to see differences in practice. +* typeahead: This changes the selector used so that it doesn't select for the `li` tag specifically, but the elements with the class `uib-typeahead-match` - if using a custom template, this class needs to be added to the `li` element in the typeahead popup template. +* rating: Due to the removal of `replace: true`, this results in a slight change to the HTML structure - see the documentation examples to see it in action. +* timepicker: This removes `replace: true`, which changes the HTML structure slightly - see the documentation examples to see it in action. +* datepickerPopup: Due to the nature of `replace: true`, this has a slight structural HTML change in the popup as a result - see documentation examples for the change in action. +* pagination: Due to the removal of `replace: true`, this affects the HTML structure slightly - see documentation examples to see new usage. +* pager: This removes `replace: true` usage from the pager, which causes a slight usage change - see documentation examples for new usage. +* modal: This removes `replace: true` usage, causing some structural changes to the HTML - the major part here is that there is no more backdrop template, and the top level elements for the window/backdrop elements lose a little flexibility. See documentation examples for new structure. +* modal: This introduces a minor behavior change in when the class is removed +* carousel: Due to the removal of `replace: true`, this causes a slight HTML structure change to the carousel and the slide elements - see documentation demos to see how it changes. This also caused removal of the ngTouch built in support - if one is using ng-touch, one needs to add the `ng-swipe-left` and `ng-swipe-right` directives to the carousel element with relevant logic. +* alert: This removes the `replace: true` usage, so this has an effect on how one uses the directive in the template - see documentation for reference +* accordion: This removes usage of `replace: true` in the accordion group, which results in a template change where the template no longer needs to contain the panel itself, but its contents. The accordion group will add the `panel` class by default, so the user just needs to add the appropriate classes to the accordion group element. This allows the user to use ng-class as well to fully control the panel related classes, so `panel-class` now is unnecessary +* tooltip: This removes support for plain strings and interpolations in tooltip-trigger and popover-trigger - please change these appropriately. See test changes in this commit for reference +* typeahead: This change removes the `id` attribute on the first `` +element placed into the DOM when the `typeahead-show-hint` attribute is used +and there is an `id` attribute present on the original `uib-typeahead` element. +This could affect selectors if they are being used. +* dropdown: This changes the focus behavior of the dropdown slightly, and potentially may break code built around current usage +* datepicker: This modifies the current behavior around the datepicker & popup's ngModelOptions, which may affect custom components that are built around both +* datepicker: As a result of removal of `replace: true`, there is the potential that this may break some CSS layout due to the slightly different HTML. Refer to the documentation examples to see the new structure. + + + -## [1.3.3](https://github.com/angular-ui/bootstrap/compare/1.3.2...v1.3.3) (2016-05-23) +## [1.3.3](https://github.com/angular-ui/bootstrap/compare/1.3.2...1.3.3) (2016-05-23) ### Bug Fixes @@ -25,7 +266,7 @@ -## [1.3.2](https://github.com/angular-ui/bootstrap/compare/1.3.1...v1.3.2) (2016-04-14) +## [1.3.2](https://github.com/angular-ui/bootstrap/compare/1.3.1...1.3.2) (2016-04-14) ### Bug Fixes @@ -51,7 +292,7 @@ -## [1.3.1](https://github.com/angular-ui/bootstrap/compare/1.3.0...v1.3.1) (2016-04-05) +## [1.3.1](https://github.com/angular-ui/bootstrap/compare/1.3.0...1.3.1) (2016-04-05) ### Bug Fixes @@ -62,7 +303,7 @@ -# [1.3.0](https://github.com/angular-ui/bootstrap/compare/1.2.5...v1.3.0) (2016-04-05) +# [1.3.0](https://github.com/angular-ui/bootstrap/compare/1.2.5...1.3.0) (2016-04-05) ### Bug Fixes @@ -105,7 +346,7 @@ attribute pass-throughs in the popup -## [1.2.5](https://github.com/angular-ui/bootstrap/compare/1.2.4...v1.2.5) (2016-03-20) +## [1.2.5](https://github.com/angular-ui/bootstrap/compare/1.2.4...1.2.5) (2016-03-20) ### Bug Fixes @@ -128,13 +369,13 @@ attribute pass-throughs in the popup -## [1.2.4](https://github.com/angular-ui/bootstrap/compare/1.2.3...v1.2.4) (2016-03-06) +## [1.2.4](https://github.com/angular-ui/bootstrap/compare/1.2.3...1.2.4) (2016-03-06) -## [1.2.3](https://github.com/angular-ui/bootstrap/compare/1.2.2...v1.2.3) (2016-03-06) +## [1.2.3](https://github.com/angular-ui/bootstrap/compare/1.2.2...1.2.3) (2016-03-06) ### Bug Fixes @@ -153,7 +394,7 @@ attribute pass-throughs in the popup -## [1.2.2](https://github.com/angular-ui/bootstrap/compare/1.2.1...v1.2.2) (2016-03-03) +## [1.2.2](https://github.com/angular-ui/bootstrap/compare/1.2.1...1.2.2) (2016-03-03) ### Bug Fixes @@ -170,7 +411,7 @@ attribute pass-throughs in the popup -## [1.2.1](https://github.com/angular-ui/bootstrap/compare/1.2.0...v1.2.1) (2016-02-27) +## [1.2.1](https://github.com/angular-ui/bootstrap/compare/1.2.0...1.2.1) (2016-02-27) ### Bug Fixes @@ -180,7 +421,7 @@ attribute pass-throughs in the popup -# [1.2.0](https://github.com/angular-ui/bootstrap/compare/1.1.2...v1.2.0) (2016-02-26) +# [1.2.0](https://github.com/angular-ui/bootstrap/compare/1.1.2...1.2.0) (2016-02-26) ### Bug Fixes @@ -240,7 +481,7 @@ template -## [1.1.2](https://github.com/angular-ui/bootstrap/compare/1.1.1...v1.1.2) (2016-02-01) +## [1.1.2](https://github.com/angular-ui/bootstrap/compare/1.1.1...1.1.2) (2016-02-01) ### Bug Fixes @@ -258,7 +499,7 @@ template -## [1.1.1](https://github.com/angular-ui/bootstrap/compare/v1.1.0...v1.1.1) (2016-01-25) +## [1.1.1](https://github.com/angular-ui/bootstrap/compare/1.1.0...1.1.1) (2016-01-25) ### Bug Fixes @@ -308,7 +549,7 @@ template in one's app and provide the necessary CSS -# [1.0.3](https://github.com/angular-ui/bootstrap/compare/v1.0.2...v1.0.3) (2016-01-12) +# [1.0.3](https://github.com/angular-ui/bootstrap/compare/1.0.2...1.0.3) (2016-01-12) ## Bug Fixes @@ -318,7 +559,7 @@ template in one's app and provide the necessary CSS -# [1.0.2](https://github.com/angular-ui/bootstrap/compare/v1.0.1...v1.0.2) (2016-01-12) +# [1.0.2](https://github.com/angular-ui/bootstrap/compare/1.0.1...1.0.2) (2016-01-12) ## Bug Fixes @@ -328,7 +569,7 @@ template in one's app and provide the necessary CSS -# [1.0.1](https://github.com/angular-ui/bootstrap/compare/1.0.0...v1.0.1) (2016-01-12) +# [1.0.1](https://github.com/angular-ui/bootstrap/compare/1.0.0...1.0.1) (2016-01-12) ## Bug Fixes @@ -349,7 +590,7 @@ template in one's app and provide the necessary CSS -# [1.0.0](https://github.com/angular-ui/bootstrap/compare/0.14.3...v1.0.0) (2016-01-08) +# [1.0.0](https://github.com/angular-ui/bootstrap/compare/0.14.3...1.0.0) (2016-01-08) ## Bug Fixes @@ -494,7 +735,7 @@ $scope.typeaheadContainer = angular.element(document.querySelector('#typeaheadCo -# [0.14.3](https://github.com/angular-ui/bootstrap/compare/0.14.2...v0.14.3) (2015-10-23) +# [0.14.3](https://github.com/angular-ui/bootstrap/compare/0.14.2...0.14.3) (2015-10-23) ## Bug Fixes @@ -519,7 +760,7 @@ $scope.typeaheadContainer = angular.element(document.querySelector('#typeaheadCo -# [0.14.2](https://github.com/angular-ui/bootstrap/compare/0.14.1...v0.14.2) (2015-10-14) +# [0.14.2](https://github.com/angular-ui/bootstrap/compare/0.14.1...0.14.2) (2015-10-14) ## Bug Fixes @@ -531,7 +772,7 @@ $scope.typeaheadContainer = angular.element(document.querySelector('#typeaheadCo -# [0.14.1](https://github.com/angular-ui/bootstrap/compare/0.14.0...v0.14.1) (2015-10-11) +# [0.14.1](https://github.com/angular-ui/bootstrap/compare/0.14.0...0.14.1) (2015-10-11) ## Bug Fixes @@ -714,7 +955,7 @@ $scope.typeaheadContainer = angular.element(document.querySelector('#typeaheadCo -# 0.13.3 (2015-08-09) +# [0.13.3](https://github.com/angular-ui/bootstrap/compare/0.13.2...0.13.3) (2015-08-09) ## Bug Fixes @@ -801,7 +1042,7 @@ Closes #4080 -# 0.13.2 (2015-08-02) +# [0.13.2](https://github.com/angular-ui/bootstrap/compare/0.13.1...0.13.2) (2015-08-02) ## Bug Fixes @@ -847,7 +1088,7 @@ Closes #4080 -# 0.13.1 (2015-07-23) +# [0.13.1](https://github.com/angular-ui/bootstrap/compare/0.13.0...0.13.1) (2015-07-23) ## Bug Fixes @@ -908,7 +1149,7 @@ Closes #4080 -# 0.13.0 (2015-05-02) +# [0.13.0](https://github.com/angular-ui/bootstrap/compare/0.12.1...0.13.0) (2015-05-02) ## Bug Fixes @@ -1007,7 +1248,8 @@ Closes #4080 * **transition:** deprecate transition module ([8a552443](https://github.com/angular-ui/bootstrap/commit/8a552443741d1e5b4b29d9da9c7e9990fa37886c), closes [#3497](https://github.com/angular-ui/bootstrap/issues/3497)) -# 0.12.1 (2015-02-20) + +# [0.12.1](https://github.com/angular-ui/bootstrap/compare/0.12.0...0.12.1) (2015-02-20) ## Bug Fixes @@ -1015,7 +1257,7 @@ Closes #4080 - incorrect position when text wraps ([5726e3ef](http://github.com/angular-ui/bootstrap/commit/5726e3ef)) -# 0.12.0 (2014-11-16) +# [0.12.0](https://github.com/angular-ui/bootstrap/compare/0.11.2...0.12.0) (2014-11-16) ## Bug Fixes @@ -1068,11 +1310,13 @@ once* and can no longer be changed after initialization. ``` -# 0.11.2 (2014-09-26) + +# [0.11.2](https://github.com/angular-ui/bootstrap/compare/0.11.1...0.11.2) (2014-09-26) Revert breaking change in **dropdown** ([1a998c4](http://github.com/angular-ui/bootstrap/commit/1a998c4)) -# 0.11.1 (2014-09-26) + +# [0.11.1](https://github.com/angular-ui/bootstrap/compare/0.11.0...0.11.1) (2014-09-26) ## Features @@ -1106,7 +1350,8 @@ Revert breaking change in **dropdown** ([1a998c4](http://github.com/angular-ui/b - allow multiple line expression ([c7db0df4](http://github.com/angular-ui/bootstrap/commit/c7db0df4)) - replace ng-if with ng-show in matches popup ([a0be450d](http://github.com/angular-ui/bootstrap/commit/a0be450d)) -# 0.11.0 (2014-05-01) + +# [0.11.0](https://github.com/angular-ui/bootstrap/compare/0.10.0...0.11.0) (2014-05-01) ## Features @@ -1284,7 +1529,8 @@ Revert breaking change in **dropdown** ([1a998c4](http://github.com/angular-ui/b ``` -# 0.10.0 (2014-01-13) + +# [0.10.0](https://github.com/angular-ui/bootstrap/compare/0.9.0...0.10.0) (2014-01-13) _This release adds AngularJS 1.2 support_ @@ -1305,7 +1551,8 @@ _This release adds AngularJS 1.2 support_ - **tooltip:** - performance and scope fixes ([c0df3201](http://github.com/angular-ui/bootstrap/commit/c0df3201)) -# 0.9.0 (2013-12-28) + +# [0.9.0](https://github.com/angular-ui/bootstrap/compare/0.8.0...0.9.0) (2013-12-28) _This release adds Bootstrap3 support_ @@ -1348,7 +1595,8 @@ _This release adds Bootstrap3 support_ - **tooltip:** - re-position tooltip after draw ([a99b3608](http://github.com/angular-ui/bootstrap/commit/a99b3608)) -# 0.8.0 (2013-12-28) + +# [0.8.0](https://github.com/angular-ui/bootstrap/compare/0.7.0...0.8.0) (2013-12-28) ## Features @@ -1415,7 +1663,8 @@ _This release adds Bootstrap3 support_ ``` -# 0.7.0 (2013-11-22) + +# [0.7.0](https://github.com/angular-ui/bootstrap/compare/0.6.0...0.7.0) (2013-11-22) ## Features @@ -1463,7 +1712,8 @@ _This release adds Bootstrap3 support_ - evaluate matches source against a correct scope ([fd21214d](http://github.com/angular-ui/bootstrap/commit/fd21214d)) - support IE8 ([0e9f9980](http://github.com/angular-ui/bootstrap/commit/0e9f9980)) -# 0.6.0 (2013-09-08) + +# [0.6.0](https://github.com/angular-ui/bootstrap/compare/0.6.0...0.7.0) (2013-09-08) ## Features @@ -1542,7 +1792,8 @@ Check the documentation for the `$modal` service to migrate from `$dialog` The placment='mouse' is gone with no equivalent -# 0.5.0 (2013-08-04) + +# [0.5.0](https://github.com/angular-ui/bootstrap/compare/0.4.0...0.5.0) (2013-08-04) ## Features @@ -1625,7 +1876,8 @@ The placment='mouse' is gone with no equivalent ``` -# 0.4.0 (2013-06-24) + +# [0.4.0](https://github.com/angular-ui/bootstrap/compare/0.3.0...0.4.0) (2013-06-24) ## Features @@ -1743,8 +1995,8 @@ The placment='mouse' is gone with no equivalent ``` - -# 0.3.0 (2013-04-30) + +# [0.3.0](https://github.com/angular-ui/bootstrap/compare/0.2.0...0.3.0) (2013-04-30) ## Features @@ -1785,7 +2037,8 @@ The placment='mouse' is gone with no equivalent - correctly higlight matches if query contains regexp-special chars ([467afcd6](https://github.com/angular-ui/bootstrap/commit/467afcd6)) - fix matches pop-up positioning issues ([74beecdb](https://github.com/angular-ui/bootstrap/commit/74beecdb)) -# 0.2.0 (2013-03-03) + +# [0.2.0](https://github.com/angular-ui/bootstrap/compare/0.1.0...0.2.0) (2013-03-03) ## Features @@ -1819,6 +2072,7 @@ The placment='mouse' is gone with no equivalent - **typeahead:** - update inputs value on mapping where label is not derived from the model ([a5f64de](https://github.com/angular-ui/bootstrap/commit/a5f64de)) + # 0.1.0 (2013-02-02) _Very first, initial release_. diff --git a/Gruntfile.js b/Gruntfile.js index 72baea5040..ed0ec0576a 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -9,8 +9,8 @@ module.exports = function(grunt) { grunt.util.linefeed = '\n'; grunt.initConfig({ - ngversion: '1.5.5', - bsversion: '3.3.6', + ngversion: '1.6.1', + bsversion: '3.3.7', modules: [],//to be filled in by build task pkg: grunt.file.readJSON('package.json'), dist: 'dist', diff --git a/LICENSE b/LICENSE index 64050c0cbc..cf7b84b3fb 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License -Copyright (c) 2012-2016 the AngularUI Team, https://github.com/organizations/angular-ui/teams/291112 +Copyright (c) 2012-2017 the AngularUI Team, https://github.com/organizations/angular-ui/teams/291112 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index a2e08beb10..8964146889 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,16 @@ +# Project Status (please read) +Due to [Angular](https://angular.io)'s continued adoption, our creation of [the Angular version of this library](https://ng-bootstrap.github.io), and the the project maintainers' moving on to other things, this project is considered feature-complete and is no longer being maintained. + +We thank you for all your contributions over the years and hope you've enjoyed using this library as much as we've had developing and maintaining it. It would not have been successful without them. + +--- + ### UI Bootstrap - [AngularJS](http://angularjs.org/) directives specific to [Bootstrap](http://getbootstrap.com) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/angular-ui/bootstrap?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://secure.travis-ci.org/angular-ui/bootstrap.svg)](http://travis-ci.org/angular-ui/bootstrap) [![devDependency Status](https://david-dm.org/angular-ui/bootstrap/dev-status.svg?branch=master)](https://david-dm.org/angular-ui/bootstrap#info=devDependencies) +[![CDNJS](https://img.shields.io/cdnjs/v/angular-ui-bootstrap.svg)](https://cdnjs.com/libraries/angular-ui-bootstrap/) ### Quick links - [Demo](#demo) @@ -27,7 +35,7 @@ # Demo -Do you want to see directives in action? Visit http://angular-ui.github.io/bootstrap/! +Do you want to see directives in action? Visit https://angular-ui.github.io/bootstrap/! # Angular 2 @@ -73,7 +81,7 @@ PM> Install-Package Angular.UI.Bootstrap #### Custom build -Head over to http://angular-ui.github.io/bootstrap/ and hit the *Custom build* button to create your own custom UI Bootstrap build, just the way you like it. +Head over to https://angular-ui.github.io/bootstrap/ and hit the *Custom build* button to create your own custom UI Bootstrap build, just the way you like it. #### Manual download @@ -118,6 +126,7 @@ If you would prefer not to load your css through your JavaScript file loader/bun * datepicker * datepickerPopup * dropdown +* modal * popover * position * timepicker @@ -130,9 +139,13 @@ The other modules, such as `accordion` in the example below, do not have CSS res import accordion from 'angular-ui-bootstrap/src/accordion'; import typeahead from 'angular-ui-bootstrap/src/typeahead/index-nocss.js'; -angular.module('myModule', [accordion, datepicker]); +angular.module('myModule', [accordion, typeahead]); ``` +# Versioning + +Pre-2.0.0 does not follow a particular versioning system. 2.0.0 and onwards follows [semantic versioning](http://semver.org/). All release changes can be viewed on our [changelog](CHANGELOG.md). + # Support ## FAQ diff --git a/misc/demo/assets/demo.css b/misc/demo/assets/demo.css index d1cadcddd3..994d63b1dd 100644 --- a/misc/demo/assets/demo.css +++ b/misc/demo/assets/demo.css @@ -73,7 +73,7 @@ section { } -.navbar .collapse { +.navbar-fixed-top .collapse { border-top: 1px solid #e7e7e7; margin-left: -15px; margin-right: -15px; diff --git a/misc/demo/assets/favicon.ico b/misc/demo/assets/favicon.ico index 3d3f0002f1..147f4de37e 100644 Binary files a/misc/demo/assets/favicon.ico and b/misc/demo/assets/favicon.ico differ diff --git a/misc/demo/assets/plunker.js b/misc/demo/assets/plunker.js index 1e586108ab..f1bd5487b8 100644 --- a/misc/demo/assets/plunker.js +++ b/misc/demo/assets/plunker.js @@ -4,7 +4,7 @@ angular.module('plunker', []) return function (ngVersion, bsVersion, version, module, content) { - var form = angular.element('
'); + var form = angular.element('
'); var addField = function (name, value) { var input = angular.element(''); input.attr('value', value); @@ -17,6 +17,7 @@ angular.module('plunker', []) ' \n' + ' \n' + ' \n' + + ' \n' + ' \n' + ' \n' + ' \n' + @@ -28,7 +29,7 @@ angular.module('plunker', []) }; var scriptContent = function(content) { - return "angular.module('ui.bootstrap.demo', ['ngAnimate', 'ui.bootstrap']);" + "\n" + content; + return "angular.module('ui.bootstrap.demo', ['ngAnimate', 'ngSanitize', 'ui.bootstrap']);" + "\n" + content; }; addField('description', 'http://angular-ui.github.io/bootstrap/'); diff --git a/misc/demo/index.html b/misc/demo/index.html index e6f1252951..8f6baa7ac2 100644 --- a/misc/demo/index.html +++ b/misc/demo/index.html @@ -147,6 +147,13 @@

+

Angular 2

+

+ For Angular 2 support, check out + + ng-bootstrap + , created by the UI Bootstrap team. +

Dependencies

This repository contains a set of native AngularJS directives based on @@ -173,8 +180,9 @@

Files to download

Alternativelly, if you are only interested in a subset of directives, you can create your own build.

-

Whichever method you choose the good news that the overall size of a download is very small: - <76kB for all directives (~20kB with gzip compression!)

+

Whichever method you choose the good news that the overall size of a download is fairly small: + 122K minified for all directives with templates and 98K without (~31kB with gzip + compression, with templates, and 28K gzipped without)

Installation

As soon as you've got all the files downloaded and included in your page you just need to declare a dependency on the ui.bootstrap module:
diff --git a/package.json b/package.json index 559928ab92..ca5cfe407d 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,15 @@ { "author": "https://github.com/angular-ui/bootstrap/graphs/contributors", "name": "angular-ui-bootstrap", - "version": "2.0.0-SNAPSHOT", + "version": "2.5.4", + "description": "Native AngularJS (Angular) directives for Bootstrap", "homepage": "http://angular-ui.github.io/bootstrap/", + "keywords": [ + "angularjs", + "angular", + "bootstrap", + "ui" + ], "dependencies": {}, "directories": { "lib": "src/" @@ -15,6 +22,7 @@ ], "main": "index.js", "scripts": { + "demo": "grunt after-test && static dist -a 0.0.0.0 -H '{\"Cache-Control\": \"no-cache, must-revalidate\"}'", "test": "grunt" }, "repository": { @@ -22,10 +30,11 @@ "url": "https://github.com/angular-ui/bootstrap.git" }, "devDependencies": { - "angular": "1.5.5", - "angular-mocks": "1.5.5", - "angular-sanitize": "1.5.5", + "angular": "1.6.1", + "angular-mocks": "1.6.1", + "angular-sanitize": "1.6.1", "grunt": "^0.4.5", + "grunt-cli": "^1.2.0", "grunt-contrib-concat": "^1.0.0", "grunt-contrib-copy": "^1.0.0", "grunt-contrib-uglify": "^1.0.1", @@ -44,6 +53,7 @@ "load-grunt-tasks": "^3.3.0", "lodash": "^4.1.0", "marked": "^0.3.5", + "node-static": "^0.7.8", "semver": "^5.0.1", "shelljs": "^0.6.0" }, diff --git a/src/accordion/accordion.js b/src/accordion/accordion.js index 586314c9c6..9b07a96131 100644 --- a/src/accordion/accordion.js +++ b/src/accordion/accordion.js @@ -1,4 +1,4 @@ -angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) +angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse', 'ui.bootstrap.tabindex']) .constant('uibAccordionConfig', { closeOthers: true @@ -58,7 +58,7 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) return { require: '^uibAccordion', // We need this directive to be inside an accordion transclude: true, // It transcludes the contents of the directive into the template - replace: true, // The element containing the directive will be replaced with the template + restrict: 'A', templateUrl: function(element, attrs) { return attrs.templateUrl || 'uib/template/accordion/accordion-group.html'; }, @@ -74,6 +74,7 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) }; }, link: function(scope, element, attrs, accordionCtrl) { + element.addClass('panel'); accordionCtrl.addGroup(scope); scope.openClass = attrs.openClass || 'panel-open'; diff --git a/src/accordion/docs/demo.html b/src/accordion/docs/demo.html index 83f643fff7..2b119b3e5e 100644 --- a/src/accordion/docs/demo.html +++ b/src/accordion/docs/demo.html @@ -1,18 +1,16 @@

@@ -28,35 +26,35 @@

- +
This content is straight in the template. - - +
+
{{group.content}} - - +
+

The body of the uib-accordion group grows to fit the contents

{{item}}
- - +
+
Hello - - +
+
Custom template with custom header template World - - +
+

Please, to delete your account, click the button below

- - +
+
I can have markup, too! This is just some content to illustrate fancy headings. - +
diff --git a/src/accordion/docs/readme.md b/src/accordion/docs/readme.md index a20a951d6e..25f790c3fa 100644 --- a/src/accordion/docs/readme.md +++ b/src/accordion/docs/readme.md @@ -32,11 +32,6 @@ The body of each accordion group is transcluded into the body of the collapsible _(Default: `false`)_ - Whether accordion group is open or closed. -* `panel-class` - - _(Default: `panel-default`)_ - - Add ability to use Bootstrap's contextual panel classes (panel-primary, panel-success, panel-info, etc...) or your own. This must be a string. - * `template-url` _(Default: `uib/template/accordion/accordion-group.html`)_ - Add the ability to override the template used on the component. diff --git a/src/accordion/index.js b/src/accordion/index.js index 0209794844..491b4f518f 100644 --- a/src/accordion/index.js +++ b/src/accordion/index.js @@ -1,4 +1,5 @@ require('../collapse'); +require('../tabindex'); require('../../template/accordion/accordion-group.html.js'); require('../../template/accordion/accordion.html.js'); require('./accordion'); diff --git a/src/accordion/test/accordion.spec.js b/src/accordion/test/accordion.spec.js index 2cae4bd4da..bb64a4b2c6 100644 --- a/src/accordion/test/accordion.spec.js +++ b/src/accordion/test/accordion.spec.js @@ -176,12 +176,12 @@ describe('uib-accordion', function() { var tpl = '' + - '' + + '
' + '
'; element = $compile(tpl)(scope); scope.$digest(); - expect(element.find('[template-url]').html()).toBe('baz'); + expect(element.find('[template-url]').html()).toBe('
baz
'); })); describe('with static panels', function() { @@ -189,8 +189,8 @@ describe('uib-accordion', function() { spyOn(Math, 'random').and.returnValue(0.1); var tpl = '' + - 'Content 1' + - 'Content 2' + + '
Content 1
' + + '
Content 2
' + '
'; element = angular.element(tpl); $compile(element)(scope); @@ -288,8 +288,8 @@ describe('uib-accordion', function() { beforeEach(function() { var tpl = '' + - 'Content 1' + - 'Content 2' + + '
Content 1
' + + '
Content 2
' + '
'; element = angular.element(tpl); $compile(element)(scope); @@ -318,7 +318,7 @@ describe('uib-accordion', function() { beforeEach(function() { var tpl = '' + - '{{group.content}}' + + '
{{group.content}}
' + '
'; element = angular.element(tpl); model = [ @@ -363,8 +363,8 @@ describe('uib-accordion', function() { beforeEach(function() { var tpl = '' + - 'Content 1' + - 'Content 2' + + '
Content 1
' + + '
Content 2
' + '
'; element = angular.element(tpl); scope.open = { first: false, second: true }; @@ -393,8 +393,8 @@ describe('uib-accordion', function() { beforeEach(function() { var tpl = '' + - '
{{item}}
' + - 'Static content' + + '
{{item}}
' + + '
Static content
' + '
'; element = angular.element(tpl); scope.items = ['Item 1', 'Item 2', 'Item 3']; @@ -421,7 +421,7 @@ describe('uib-accordion', function() { beforeEach(function() { var tpl = '' + - '{{group.content}}' + + '
{{group.content}}
' + '
'; element = angular.element(tpl); scope.groups = [ @@ -456,7 +456,7 @@ describe('uib-accordion', function() { beforeEach(function() { var tpl = '' + - '{{group.content}}' + + '
{{group.content}}
' + '
'; element = angular.element(tpl); scope.groups = [ @@ -480,7 +480,7 @@ describe('uib-accordion', function() { beforeEach(function() { var tpl = '' + - 'Content 1' + + '
Content 1
' + '
'; element = angular.element(tpl); scope.disabled = true; @@ -519,10 +519,10 @@ describe('uib-accordion', function() { function isDisabledStyleCheck() { var tpl = '' + - '' + + '
' + 'Heading Element {{x}} ' + 'Body' + - '' + + '
' + '
'; scope.disabled = true; element = $compile(tpl)(scope); @@ -536,10 +536,10 @@ describe('uib-accordion', function() { beforeEach(function() { var tpl = '' + - '' + + '
' + 'Heading Element {{x}} ' + 'Body' + - '' + + '
' + '
'; element = $compile(tpl)(scope); scope.$digest(); @@ -551,7 +551,7 @@ describe('uib-accordion', function() { }); it('attaches the same scope to the transcluded heading and body', function() { - expect(findGroupLink(0).find('span.ng-scope').scope().$id).toBe(findGroupBody(0).find('span').scope().$id); + expect(findGroupLink(0).scope().$id).toBe(findGroupBody(0).scope().$id); }); it('should wrap the transcluded content in a span', function() { @@ -565,10 +565,10 @@ describe('uib-accordion', function() { beforeEach(function() { var tpl = '' + - '' + + '
' + '
Heading Element {{x}}
' + 'Body' + - '' + + '
' + '
'; element = $compile(tpl)(scope); scope.$digest(); @@ -580,7 +580,7 @@ describe('uib-accordion', function() { }); it('attaches the same scope to the transcluded heading and body', function() { - expect(findGroupLink(0).find('span.ng-scope').scope().$id).toBe(findGroupBody(0).find('span').scope().$id); + expect(findGroupLink(0).scope().$id).toBe(findGroupBody(0).scope().$id); }); it('should have disabled styling when is-disabled is true', isDisabledStyleCheck); @@ -588,7 +588,7 @@ describe('uib-accordion', function() { describe('uib-accordion-heading, with repeating uib-accordion-groups', function() { it('should clone the uib-accordion-heading for each group', function() { - element = $compile('{{x}}')(scope); + element = $compile('
{{x}}
')(scope); scope.$digest(); groups = element.find('.panel'); expect(groups.length).toBe(3); @@ -600,7 +600,7 @@ describe('uib-accordion', function() { describe('uib-accordion-heading attribute, with repeating uib-accordion-groups', function() { it('should clone the uib-accordion-heading for each group', function() { - element = $compile('
{{x}}
')(scope); + element = $compile('
{{x}}
')(scope); scope.$digest(); groups = element.find('.panel'); expect(groups.length).toBe(3); @@ -614,46 +614,11 @@ describe('uib-accordion', function() { it('should transclude heading to a template using data-uib-accordion-header', inject(function($templateCache) { $templateCache.put('foo/bar.html', '
'); - element = $compile('baz')(scope); + element = $compile('
baz
')(scope); scope.$digest(); groups = element.find('.panel'); expect(findGroupLink(0).text()).toBe('baz'); })); }); - - describe('uib-accordion group panel class', function() { - it('should use the default value when panel class is falsy - #3968', function() { - element = $compile('Content')(scope); - scope.$digest(); - groups = element.find('.panel'); - expect(groups.eq(0)).toHaveClass('panel-default'); - - element = $compile('Content')(scope); - scope.$digest(); - groups = element.find('.panel'); - expect(groups.eq(0)).toHaveClass('panel-default'); - }); - - it('should use the specified value when not falsy - #3968', function() { - element = $compile('Content')(scope); - scope.$digest(); - groups = element.find('.panel'); - expect(groups.eq(0)).toHaveClass('custom-class'); - expect(groups.eq(0)).not.toHaveClass('panel-default'); - }); - - it('should change class if panel-class is changed', function() { - element = $compile('Content')(scope); - scope.panelClass = 'custom-class'; - scope.$digest(); - groups = element.find('.panel'); - expect(groups.eq(0)).toHaveClass('custom-class'); - - scope.panelClass = 'different-class'; - scope.$digest(); - expect(groups.eq(0)).toHaveClass('different-class'); - expect(groups.eq(0)).not.toHaveClass('custom-class'); - }); - }); }); }); diff --git a/src/alert/alert.js b/src/alert/alert.js index 0ad230be7f..73387e55a5 100644 --- a/src/alert/alert.js +++ b/src/alert/alert.js @@ -1,7 +1,12 @@ angular.module('ui.bootstrap.alert', []) -.controller('UibAlertController', ['$scope', '$attrs', '$interpolate', '$timeout', function($scope, $attrs, $interpolate, $timeout) { +.controller('UibAlertController', ['$scope', '$element', '$attrs', '$interpolate', '$timeout', function($scope, $element, $attrs, $interpolate, $timeout) { $scope.closeable = !!$attrs.close; + $element.addClass('alert'); + $attrs.$set('role', 'alert'); + if ($scope.closeable) { + $element.addClass('alert-dismissible'); + } var dismissOnTimeout = angular.isDefined($attrs.dismissOnTimeout) ? $interpolate($attrs.dismissOnTimeout)($scope.$parent) : null; @@ -17,13 +22,12 @@ angular.module('ui.bootstrap.alert', []) return { controller: 'UibAlertController', controllerAs: 'alert', + restrict: 'A', templateUrl: function(element, attrs) { return attrs.templateUrl || 'uib/template/alert/alert.html'; }, transclude: true, - replace: true, scope: { - type: '@', close: '&' } }; diff --git a/src/alert/docs/demo.html b/src/alert/docs/demo.html index e2ce4b3870..b599f48091 100644 --- a/src/alert/docs/demo.html +++ b/src/alert/docs/demo.html @@ -1,11 +1,9 @@
- {{alert.msg}} - A happy alert! +
{{alert.msg}}
+
A happy alert!
diff --git a/src/alert/docs/readme.md b/src/alert/docs/readme.md index 3361f6418e..cf0bcad1ab 100644 --- a/src/alert/docs/readme.md +++ b/src/alert/docs/readme.md @@ -5,15 +5,11 @@ This directive can be used both to generate alerts from static and dynamic model * `close()` $ - A callback function that gets fired when an `alert` is closed. If the attribute exists, a close button is displayed as well. - + * `dismiss-on-timeout` _(Default: `none`)_ - Takes the number of milliseconds that specify the timeout duration, after which the alert will be closed. This attribute requires the presence of the `close` attribute. - + * `template-url` _(Default: `uib/template/alert/alert.html`)_ - Add the ability to override the template used in the component. - -* `type` - _(Default: `warning`)_ - - Defines the type of the alert. Go to [bootstrap page](http://getbootstrap.com/components/#alerts) to see the type of alerts available. diff --git a/src/alert/test/alert.spec.js b/src/alert/test/alert.spec.js index 752127ae51..bc87c7f7ad 100644 --- a/src/alert/test/alert.spec.js +++ b/src/alert/test/alert.spec.js @@ -12,9 +12,10 @@ describe('uib-alert', function() { element = angular.element( '
' + - '{{alert.msg}}' + - '' + + '
' + ''); scope.alerts = [ @@ -35,13 +36,13 @@ describe('uib-alert', function() { } function findContent(index) { - return element.find('div[ng-transclude] span').eq(index); + return element.find('div[ng-transclude]').eq(index); } it('should expose the controller to the view', function() { $templateCache.put('uib/template/alert/alert.html', '
{{alert.text}}
'); - element = $compile('')(scope); + element = $compile('
')(scope); scope.$digest(); var ctrl = element.controller('uib-alert'); @@ -50,16 +51,16 @@ describe('uib-alert', function() { ctrl.text = 'foo'; scope.$digest(); - expect(element.html()).toBe('foo'); + expect(element.html()).toBe('
foo
'); }); it('should support custom templates', function() { $templateCache.put('foo/bar.html', '
baz
'); - element = $compile('')(scope); + element = $compile('
')(scope); scope.$digest(); - expect(element.html()).toBe('baz'); + expect(element.html()).toBe('
baz
'); }); it('should generate alerts using ng-repeat', function() { @@ -67,23 +68,6 @@ describe('uib-alert', function() { expect(alerts.length).toEqual(3); }); - it('should use correct classes for different alert types', function() { - var alerts = createAlerts(); - expect(alerts.eq(0)).toHaveClass('alert-success'); - expect(alerts.eq(1)).toHaveClass('alert-error'); - expect(alerts.eq(2)).toHaveClass('alert-warning'); - }); - - it('should respect alert type binding', function() { - var alerts = createAlerts(); - expect(alerts.eq(0)).toHaveClass('alert-success'); - - scope.alerts[0].type = 'error'; - scope.$digest(); - - expect(alerts.eq(0)).toHaveClass('alert-error'); - }); - it('should show the alert content', function() { var alerts = createAlerts(); @@ -115,22 +99,15 @@ describe('uib-alert', function() { }); it('should not show close button and have the dismissible class if no close callback specified', function() { - element = $compile('No close')(scope); + element = $compile('
No close
')(scope); scope.$digest(); expect(findCloseButton(0)).toBeHidden(); expect(element).not.toHaveClass('alert-dismissible'); }); - it('should be possible to add additional classes for alert', function() { - var element = $compile('Default alert!')(scope); - scope.$digest(); - expect(element).toHaveClass('alert-block'); - expect(element).toHaveClass('alert-info'); - }); - it('should close automatically if dismiss-on-timeout is defined on the element', function() { scope.removeAlert = jasmine.createSpy(); - $compile('Default alert!')(scope); + $compile('
Default alert!
')(scope); scope.$digest(); $timeout.flush(); @@ -140,7 +117,7 @@ describe('uib-alert', function() { it('should not close immediately with a dynamic dismiss-on-timeout', function() { scope.removeAlert = jasmine.createSpy(); scope.dismissTime = 500; - $compile('Default alert!')(scope); + $compile('
Default alert!
')(scope); scope.$digest(); $timeout.flush(100); diff --git a/src/carousel/carousel.js b/src/carousel/carousel.js index f745229c76..aaa04f8a95 100644 --- a/src/carousel/carousel.js +++ b/src/carousel/carousel.js @@ -5,9 +5,10 @@ angular.module('ui.bootstrap.carousel', []) slides = self.slides = $scope.slides = [], SLIDE_DIRECTION = 'uib-slideDirection', currentIndex = $scope.active, - currentInterval, isPlaying, bufferedTransitions = []; + currentInterval, isPlaying; var destroyed = false; + $element.addClass('carousel'); self.addSlide = function(slide, element) { slides.push({ @@ -66,11 +67,6 @@ angular.module('ui.bootstrap.carousel', []) self.removeSlide = function(slide) { var index = findSlideIndex(slide); - var bufferedIndex = bufferedTransitions.indexOf(slides[index]); - if (bufferedIndex !== -1) { - bufferedTransitions.splice(bufferedIndex, 1); - } - //get the index of the slide inside the carousel slides.splice(index, 1); if (slides.length > 0 && currentIndex === index) { @@ -94,7 +90,6 @@ angular.module('ui.bootstrap.carousel', []) if (slides.length === 0) { currentIndex = null; $scope.active = null; - clearBufferedTransitions(); } }; @@ -109,8 +104,6 @@ angular.module('ui.bootstrap.carousel', []) if (nextSlide.slide.index !== currentIndex && !$scope.$currentTransition) { goNext(nextSlide.slide, nextIndex, direction); - } else if (nextSlide && nextSlide.slide.index !== currentIndex && $scope.$currentTransition) { - bufferedTransitions.push(slides[nextIndex]); } }; @@ -145,6 +138,9 @@ angular.module('ui.bootstrap.carousel', []) } }; + $element.on('mouseenter', $scope.pause); + $element.on('mouseleave', $scope.play); + $scope.$on('$destroy', function() { destroyed = true; resetTimer(); @@ -176,12 +172,6 @@ angular.module('ui.bootstrap.carousel', []) } }); - function clearBufferedTransitions() { - while (bufferedTransitions.length) { - bufferedTransitions.shift(); - } - } - function getSlideByIndex(index) { for (var i = 0, l = slides.length; i < l; ++i) { if (slides[i].index === index) { @@ -217,14 +207,6 @@ angular.module('ui.bootstrap.carousel', []) if (phase === 'close') { $scope.$currentTransition = null; $animate.off('addClass', element); - if (bufferedTransitions.length) { - var nextSlide = bufferedTransitions.pop().slide; - var nextIndex = nextSlide.index; - var nextDirection = nextIndex > self.getCurrentIndex() ? 'next' : 'prev'; - clearBufferedTransitions(); - - goNext(nextSlide, nextIndex, nextDirection); - } } }); } @@ -255,7 +237,6 @@ angular.module('ui.bootstrap.carousel', []) function resetTransition(slides) { if (!slides.length) { $scope.$currentTransition = null; - clearBufferedTransitions(); } } @@ -280,9 +261,9 @@ angular.module('ui.bootstrap.carousel', []) .directive('uibCarousel', function() { return { transclude: true, - replace: true, controller: 'UibCarouselController', controllerAs: 'carousel', + restrict: 'A', templateUrl: function(element, attrs) { return attrs.templateUrl || 'uib/template/carousel/carousel.html'; }, @@ -296,11 +277,11 @@ angular.module('ui.bootstrap.carousel', []) }; }) -.directive('uibSlide', function() { +.directive('uibSlide', ['$animate', function($animate) { return { require: '^uibCarousel', + restrict: 'A', transclude: true, - replace: true, templateUrl: function(element, attrs) { return attrs.templateUrl || 'uib/template/carousel/slide.html'; }, @@ -309,14 +290,19 @@ angular.module('ui.bootstrap.carousel', []) index: '=?' }, link: function (scope, element, attrs, carouselCtrl) { + element.addClass('item'); carouselCtrl.addSlide(scope, element); //when the scope is destroyed then remove the slide from the current slides array scope.$on('$destroy', function() { carouselCtrl.removeSlide(scope); }); + + scope.$watch('active', function(active) { + $animate[active ? 'addClass' : 'removeClass'](element, 'active'); + }); } }; -}) +}]) .animation('.item', ['$animateCss', function($animateCss) { diff --git a/src/carousel/docs/demo.html b/src/carousel/docs/demo.html index 16ef7fc9dd..f2a6f48672 100644 --- a/src/carousel/docs/demo.html +++ b/src/carousel/docs/demo.html @@ -1,14 +1,14 @@
- - +
+
- - +
+
diff --git a/src/carousel/docs/demo.js b/src/carousel/docs/demo.js index 30bf8455e9..79dd322dc6 100644 --- a/src/carousel/docs/demo.js +++ b/src/carousel/docs/demo.js @@ -8,7 +8,7 @@ angular.module('ui.bootstrap.demo').controller('CarouselDemoCtrl', function ($sc $scope.addSlide = function() { var newWidth = 600 + slides.length + 1; slides.push({ - image: 'http://lorempixel.com/' + newWidth + '/300', + image: '//unsplash.it/' + newWidth + '/300', text: ['Nice image','Awesome photograph','That is so cool','I love that'][slides.length % 4], id: currIndex++ }); diff --git a/src/carousel/test/carousel.spec.js b/src/carousel/test/carousel.spec.js index e43ce5f6dd..87b586dbd4 100644 --- a/src/carousel/test/carousel.spec.js +++ b/src/carousel/test/carousel.spec.js @@ -1,17 +1,5 @@ describe('carousel', function() { - beforeEach(module('ui.bootstrap.carousel', function($compileProvider, $provide) { - angular.forEach(['ngSwipeLeft', 'ngSwipeRight'], makeMock); - function makeMock(name) { - $provide.value(name + 'Directive', []); //remove existing directive if it exists - $compileProvider.directive(name, function() { - return function(scope, element, attr) { - element.on(name, function() { - scope.$apply(attr[name]); - }); - }; - }); - } - })); + beforeEach(module('ui.bootstrap.carousel')); beforeEach(module('ngAnimateMock')); beforeEach(module('uib/template/carousel/carousel.html', 'uib/template/carousel/slide.html')); @@ -36,11 +24,11 @@ describe('carousel', function() { {content: 'three', index: 2} ]; elm = $compile( - '' + - '' + + '
' + + '
' + '{{slide.content}}' + - '' + - '' + '
' + + '
' )(scope); scope.interval = 5000; scope.nopause = undefined; @@ -60,19 +48,19 @@ describe('carousel', function() { it('should allow overriding of the carousel template', function() { $templateCache.put('foo/bar.html', '
foo
'); - elm = $compile('')(scope); + elm = $compile('
')(scope); $rootScope.$digest(); - expect(elm.html()).toBe('foo'); + expect(elm.html()).toBe('
foo
'); }); it('should allow overriding of the slide template', function() { $templateCache.put('foo/bar.html', '
bar
'); elm = $compile( - '' + - '' + - '' + '
' + + '
' + + '
' )(scope); $rootScope.$digest(); @@ -101,11 +89,11 @@ describe('carousel', function() { it('should stop cycling slides forward when noWrap is truthy', function () { elm = $compile( - '' + - '' + + '
' + + '
' + '{{slide.content}}' + - '' + - '' + '
' + + '
' )(scope); scope.noWrap = true; @@ -124,11 +112,11 @@ describe('carousel', function() { it('should stop cycling slides backward when noWrap is truthy', function () { elm = $compile( - '' + - '' + + '
' + + '
' + '{{slide.content}}' + - '' + - '' + '
' + + '
' )(scope); scope.noWrap = true; @@ -147,11 +135,11 @@ describe('carousel', function() { scope.slides = [{active:false,content:'one'}]; scope.$apply(); elm = $compile( - '' + - '' + + '
' + + '
' + '{{slide.content}}' + - '' + - '' + '
' + + '
' )(scope); var indicators = elm.find('ol.carousel-indicators > li'); expect(indicators.length).toBe(0); @@ -228,20 +216,6 @@ describe('carousel', function() { testSlideActive(0); }); - describe('swiping', function() { - it('should go next on swipeLeft', function() { - testSlideActive(0); - elm.triggerHandler('ngSwipeLeft'); - testSlideActive(1); - }); - - it('should go prev on swipeRight', function() { - testSlideActive(0); - elm.triggerHandler('ngSwipeRight'); - testSlideActive(2); - }); - }); - it('should select a slide when clicking on slide indicators', function () { var indicators = elm.find('ol.carousel-indicators > li'); indicators.eq(1).click(); @@ -269,7 +243,7 @@ describe('carousel', function() { }); it('should bind the content to slides', function() { - var contents = elm.find('div.item'); + var contents = elm.find('div.item [ng-transclude]'); expect(contents.length).toBe(3); expect(contents.eq(0).text()).toBe('one'); @@ -343,7 +317,7 @@ describe('carousel', function() { {content:'new3', index: 6} ]; scope.$apply(); - var contents = elm.find('div.item'); + var contents = elm.find('div.item [ng-transclude]'); expect(contents.length).toBe(3); expect(contents.eq(0).text()).toBe('new1'); expect(contents.eq(1).text()).toBe('new2'); @@ -441,11 +415,11 @@ describe('carousel', function() { {content: 'three', id: 2} ]; elm = $compile( - '' + - '' + + '
' + + '
' + '{{slide.content}}' + - '' + - '' + '
' + + '
' )(scope); scope.$apply(); }); @@ -465,7 +439,7 @@ describe('carousel', function() { scope.slides[1].id = 2; scope.slides[2].id = 1; scope.$apply(); - var contents = elm.find('div.item'); + var contents = elm.find('div.item [ng-transclude]'); expect(contents.length).toBe(3); expect(contents.eq(0).text()).toBe('three'); expect(contents.eq(1).text()).toBe('two'); @@ -491,7 +465,7 @@ describe('carousel', function() { scope.slides[2].id = 4; scope.slides.push({content:'four', id: 5}); scope.$apply(); - var contents = elm.find('div.item'); + var contents = elm.find('div.item [ng-transclude]'); expect(contents.length).toBe(4); expect(contents.eq(0).text()).toBe('two'); expect(contents.eq(1).text()).toBe('one'); @@ -503,7 +477,7 @@ describe('carousel', function() { testSlideActive(1); scope.slides.splice(1, 1); scope.$apply(); - var contents = elm.find('div.item'); + var contents = elm.find('div.item [ng-transclude]'); expect(contents.length).toBe(2); expect(contents.eq(0).text()).toBe('three'); expect(contents.eq(1).text()).toBe('one'); @@ -583,7 +557,7 @@ describe('carousel', function() { $templateCache.put('uib/template/carousel/carousel.html', '
{{carousel.text}}
'); var scope = $rootScope.$new(); - var elm = $compile('')(scope); + var elm = $compile('
')(scope); $rootScope.$digest(); var ctrl = elm.controller('uibCarousel'); @@ -593,7 +567,7 @@ describe('carousel', function() { ctrl.text = 'foo'; $rootScope.$digest(); - expect(elm.html()).toBe('foo'); + expect(elm.html()).toBe('
foo
'); })); }); @@ -605,11 +579,11 @@ describe('carousel', function() { {active:false,content:'three'} ]; var elm = $compile( - '' + - '' + + '
' + + '
' + '{{slide.content}}' + - '' + - '' + '
' + + '
' )(scope); $rootScope.$digest(); diff --git a/src/collapse/collapse.js b/src/collapse/collapse.js index cafd93a3ea..7e605b641d 100644 --- a/src/collapse/collapse.js +++ b/src/collapse/collapse.js @@ -5,16 +5,42 @@ angular.module('ui.bootstrap.collapse', []) return { link: function(scope, element, attrs) { var expandingExpr = $parse(attrs.expanding), - expandedExpr = $parse(attrs.expanded), - collapsingExpr = $parse(attrs.collapsing), - collapsedExpr = $parse(attrs.collapsed); + expandedExpr = $parse(attrs.expanded), + collapsingExpr = $parse(attrs.collapsing), + collapsedExpr = $parse(attrs.collapsed), + horizontal = false, + css = {}, + cssTo = {}; - if (!scope.$eval(attrs.uibCollapse)) { - element.addClass('in') - .addClass('collapse') - .attr('aria-expanded', true) - .attr('aria-hidden', false) - .css({height: 'auto'}); + init(); + + function init() { + horizontal = !!('horizontal' in attrs); + if (horizontal) { + css = { + width: '' + }; + cssTo = {width: '0'}; + } else { + css = { + height: '' + }; + cssTo = {height: '0'}; + } + if (!scope.$eval(attrs.uibCollapse)) { + element.addClass('in') + .addClass('collapse') + .attr('aria-expanded', true) + .attr('aria-hidden', false) + .css(css); + } + } + + function getScrollFromElement(element) { + if (horizontal) { + return {width: element.scrollWidth + 'px'}; + } + return {height: element.scrollHeight + 'px'}; } function expand() { @@ -33,20 +59,26 @@ angular.module('ui.bootstrap.collapse', []) $animateCss(element, { addClass: 'in', easing: 'ease', - to: { height: element[0].scrollHeight + 'px' } + css: { + overflow: 'hidden' + }, + to: getScrollFromElement(element[0]) }).start()['finally'](expandDone); } else { $animate.addClass(element, 'in', { - to: { height: element[0].scrollHeight + 'px' } + css: { + overflow: 'hidden' + }, + to: getScrollFromElement(element[0]) }).then(expandDone); } - }); + }, angular.noop); } function expandDone() { element.removeClass('collapsing') .addClass('collapse') - .css({height: 'auto'}); + .css(css); expandedExpr(scope); } @@ -58,10 +90,10 @@ angular.module('ui.bootstrap.collapse', []) $q.resolve(collapsingExpr(scope)) .then(function() { element - // IMPORTANT: The height must be set before adding "collapsing" class. - // Otherwise, the browser attempts to animate from height 0 (in - // collapsing class) to the given height here. - .css({height: element[0].scrollHeight + 'px'}) + // IMPORTANT: The width must be set before adding "collapsing" class. + // Otherwise, the browser attempts to animate from width 0 (in + // collapsing class) to the given width here. + .css(getScrollFromElement(element[0])) // initially all panel collapse have the collapse class, this removal // prevents the animation from jumping to collapsed state .removeClass('collapse') @@ -72,18 +104,18 @@ angular.module('ui.bootstrap.collapse', []) if ($animateCss) { $animateCss(element, { removeClass: 'in', - to: {height: '0'} + to: cssTo }).start()['finally'](collapseDone); } else { $animate.removeClass(element, 'in', { - to: {height: '0'} + to: cssTo }).then(collapseDone); } - }); + }, angular.noop); } function collapseDone() { - element.css({height: '0'}); // Required so that collapse works when animation is disabled + element.css(cssTo); // Required so that collapse works when animation is disabled element.removeClass('collapsing') .addClass('collapse'); collapsedExpr(scope); diff --git a/src/collapse/docs/demo.html b/src/collapse/docs/demo.html index 462bda3ba0..f5e06e7fbc 100644 --- a/src/collapse/docs/demo.html +++ b/src/collapse/docs/demo.html @@ -1,7 +1,40 @@ +
- +

Resize window to less than 768 pixels to display mobile menu toggle button.

+ +
+
Some content
+ + +
+
+
Some content
+
diff --git a/src/collapse/docs/demo.js b/src/collapse/docs/demo.js index 897eecaf58..f685290001 100644 --- a/src/collapse/docs/demo.js +++ b/src/collapse/docs/demo.js @@ -1,3 +1,5 @@ angular.module('ui.bootstrap.demo').controller('CollapseDemoCtrl', function ($scope) { + $scope.isNavCollapsed = true; $scope.isCollapsed = false; + $scope.isCollapsedHorizontal = false; }); diff --git a/src/collapse/docs/readme.md b/src/collapse/docs/readme.md index 60237b9952..5cdacfd3d0 100644 --- a/src/collapse/docs/readme.md +++ b/src/collapse/docs/readme.md @@ -28,3 +28,10 @@ _(Default: `false`)_ - Whether the element should be collapsed or not. +* `horizontal` + $ - + An optional attribute that permit to collapse horizontally. + +### Known Issues + +When using the `horizontal` attribute with this directive, CSS can reflow as the collapse element goes from `0px` to its desired end width, which can result in height changes. This can cause animations to not appear to run. The best way around this is to set a fixed height via CSS on the horizontal collapse element so that this situation does not occur, and so the animation can run as expected. diff --git a/src/collapse/test/collapseHorizontally.spec.js b/src/collapse/test/collapseHorizontally.spec.js new file mode 100644 index 0000000000..e5b22bdb56 --- /dev/null +++ b/src/collapse/test/collapseHorizontally.spec.js @@ -0,0 +1,255 @@ +describe('collapse directive', function() { + var elementH, compileFnH, scope, $compile, $animate, $q; + + beforeEach(module('ui.bootstrap.collapse')); + beforeEach(module('ngAnimateMock')); + beforeEach(inject(function(_$rootScope_, _$compile_, _$animate_, _$q_) { + scope = _$rootScope_; + $compile = _$compile_; + $animate = _$animate_; + $q = _$q_; + })); + + beforeEach(function() { + elementH = angular.element( + '
' + + 'Some Content
'); + compileFnH = $compile(elementH); + angular.element(document.body).append(elementH); + }); + + afterEach(function() { + elementH.remove(); + }); + + function initCallbacks() { + scope.collapsing = jasmine.createSpy('scope.collapsing'); + scope.collapsed = jasmine.createSpy('scope.collapsed'); + scope.expanding = jasmine.createSpy('scope.expanding'); + scope.expanded = jasmine.createSpy('scope.expanded'); + } + + function assertCallbacks(expected) { + ['collapsing', 'collapsed', 'expanding', 'expanded'].forEach(function(cbName) { + if (expected[cbName]) { + expect(scope[cbName]).toHaveBeenCalled(); + } else { + expect(scope[cbName]).not.toHaveBeenCalled(); + } + }); + } + + it('should be hidden on initialization if isCollapsed = true', function() { + initCallbacks(); + scope.isCollapsed = true; + compileFnH(scope); + scope.$digest(); + expect(elementH.width()).toBe(0); + assertCallbacks({ collapsed: true }); + }); + + it('should not trigger any animation on initialization if isCollapsed = true', function() { + var wrapperFn = function() { + $animate.flush(); + }; + + scope.isCollapsed = true; + compileFnH(scope); + scope.$digest(); + + expect(wrapperFn).toThrowError(/No pending animations ready to be closed or flushed/); + }); + + it('should collapse if isCollapsed = true on subsequent use', function() { + scope.isCollapsed = false; + compileFnH(scope); + scope.$digest(); + initCallbacks(); + scope.isCollapsed = true; + scope.$digest(); + $animate.flush(); + expect(elementH.width()).toBe(0); + assertCallbacks({ collapsing: true, collapsed: true }); + }); + + it('should show after toggled from collapsed', function() { + initCallbacks(); + scope.isCollapsed = true; + compileFnH(scope); + scope.$digest(); + expect(elementH.width()).toBe(0); + assertCallbacks({ collapsed: true }); + scope.collapsed.calls.reset(); + + scope.isCollapsed = false; + scope.$digest(); + $animate.flush(); + expect(elementH.width()).not.toBe(0); + assertCallbacks({ expanding: true, expanded: true }); + }); + + it('should not trigger any animation on initialization if isCollapsed = false', function() { + var wrapperFn = function() { + $animate.flush(); + }; + + scope.isCollapsed = false; + compileFnH(scope); + scope.$digest(); + + expect(wrapperFn).toThrowError(/No pending animations ready to be closed or flushed/); + }); + + it('should expand if isCollapsed = false on subsequent use', function() { + scope.isCollapsed = false; + compileFnH(scope); + scope.$digest(); + scope.isCollapsed = true; + scope.$digest(); + $animate.flush(); + initCallbacks(); + scope.isCollapsed = false; + scope.$digest(); + $animate.flush(); + expect(elementH.width()).not.toBe(0); + assertCallbacks({ expanding: true, expanded: true }); + }); + + it('should collapse if isCollapsed = true on subsequent uses', function() { + scope.isCollapsed = false; + compileFnH(scope); + scope.$digest(); + scope.isCollapsed = true; + scope.$digest(); + $animate.flush(); + scope.isCollapsed = false; + scope.$digest(); + $animate.flush(); + initCallbacks(); + scope.isCollapsed = true; + scope.$digest(); + $animate.flush(); + expect(elementH.width()).toBe(0); + assertCallbacks({ collapsing: true, collapsed: true }); + }); + + it('should change aria-expanded attribute', function() { + scope.isCollapsed = false; + compileFnH(scope); + scope.$digest(); + expect(elementH.attr('aria-expanded')).toBe('true'); + + scope.isCollapsed = true; + scope.$digest(); + $animate.flush(); + expect(elementH.attr('aria-expanded')).toBe('false'); + }); + + it('should change aria-hidden attribute', function() { + scope.isCollapsed = false; + compileFnH(scope); + scope.$digest(); + expect(elementH.attr('aria-hidden')).toBe('false'); + + scope.isCollapsed = true; + scope.$digest(); + $animate.flush(); + expect(elementH.attr('aria-hidden')).toBe('true'); + }); + + describe('expanding callback returning a promise', function() { + var defer, collapsedWidth; + + beforeEach(function() { + defer = $q.defer(); + + scope.isCollapsed = true; + scope.expanding = function() { + return defer.promise; + }; + compileFnH(scope); + scope.$digest(); + collapsedWidth = elementH.width(); + + // set flag to expand ... + scope.isCollapsed = false; + scope.$digest(); + + // ... shouldn't expand yet ... + expect(elementH.attr('aria-expanded')).not.toBe('true'); + expect(elementH.width()).toBe(collapsedWidth); + }); + + it('should wait for it to resolve before animating', function() { + defer.resolve(); + + // should now expand + scope.$digest(); + $animate.flush(); + + expect(elementH.attr('aria-expanded')).toBe('true'); + expect(elementH.width()).toBeGreaterThan(collapsedWidth); + }); + + it('should not animate if it rejects', function() { + defer.reject(); + + // should NOT expand + scope.$digest(); + + expect(elementH.attr('aria-expanded')).not.toBe('true'); + expect(elementH.width()).toBe(collapsedWidth); + }); + }); + + describe('collapsing callback returning a promise', function() { + var defer, expandedWidth; + + beforeEach(function() { + defer = $q.defer(); + scope.isCollapsed = false; + scope.collapsing = function() { + return defer.promise; + }; + compileFnH(scope); + scope.$digest(); + + expandedWidth = elementH.width(); + + // set flag to collapse ... + scope.isCollapsed = true; + scope.$digest(); + + // ... but it shouldn't collapse yet ... + expect(elementH.attr('aria-expanded')).not.toBe('false'); + expect(elementH.width()).toBe(expandedWidth); + }); + + it('should wait for it to resolve before animating', function() { + defer.resolve(); + + // should now collapse + scope.$digest(); + $animate.flush(); + + expect(elementH.attr('aria-expanded')).toBe('false'); + expect(elementH.width()).toBeLessThan(expandedWidth); + }); + + it('should not animate if it rejects', function() { + defer.reject(); + + // should NOT collapse + scope.$digest(); + + expect(elementH.attr('aria-expanded')).not.toBe('false'); + expect(elementH.width()).toBe(expandedWidth); + }); + }); + +}); diff --git a/src/dateparser/dateparser.js b/src/dateparser/dateparser.js index 3dae13b051..d770dff31b 100644 --- a/src/dateparser/dateparser.js +++ b/src/dateparser/dateparser.js @@ -1,6 +1,6 @@ angular.module('ui.bootstrap.dateparser', []) -.service('uibDateParser', ['$log', '$locale', 'dateFilter', 'orderByFilter', function($log, $locale, dateFilter, orderByFilter) { +.service('uibDateParser', ['$log', '$locale', 'dateFilter', 'orderByFilter', 'filterFilter', function($log, $locale, dateFilter, orderByFilter, filterFilter) { // Pulled from https://github.com/mbostock/d3/blob/master/src/format/requote.js var SPECIAL_CHARACTERS_REGEXP = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g; @@ -230,11 +230,37 @@ angular.module('ui.bootstrap.dateparser', []) formatter: function(date) { return dateFilter(date, 'G'); } } ]; + + if (angular.version.major >= 1 && angular.version.minor > 4) { + formatCodeToRegex.push({ + key: 'LLLL', + regex: $locale.DATETIME_FORMATS.STANDALONEMONTH.join('|'), + apply: function(value) { this.month = $locale.DATETIME_FORMATS.STANDALONEMONTH.indexOf(value); }, + formatter: function(date) { return dateFilter(date, 'LLLL'); } + }); + } }; this.init(); - function createParser(format, func) { + function getFormatCodeToRegex(key) { + return filterFilter(formatCodeToRegex, {key: key}, true)[0]; + } + + this.getParser = function (key) { + var f = getFormatCodeToRegex(key); + return f && f.apply || null; + }; + + this.overrideParser = function (key, parser) { + var f = getFormatCodeToRegex(key); + if (f && angular.isFunction(parser)) { + this.parsers = {}; + f.apply = parser; + } + }.bind(this); + + function createParser(format) { var map = [], regex = format.split(''); // check for literal values @@ -283,7 +309,7 @@ angular.module('ui.bootstrap.dateparser', []) map.push({ index: index, key: data.key, - apply: data[func], + apply: data.apply, matcher: data.regex }); } @@ -295,6 +321,70 @@ angular.module('ui.bootstrap.dateparser', []) }; } + function createFormatter(format) { + var formatters = []; + var i = 0; + var formatter, literalIdx; + while (i < format.length) { + if (angular.isNumber(literalIdx)) { + if (format.charAt(i) === '\'') { + if (i + 1 >= format.length || format.charAt(i + 1) !== '\'') { + formatters.push(constructLiteralFormatter(format, literalIdx, i)); + literalIdx = null; + } + } else if (i === format.length) { + while (literalIdx < format.length) { + formatter = constructFormatterFromIdx(format, literalIdx); + formatters.push(formatter); + literalIdx = formatter.endIdx; + } + } + + i++; + continue; + } + + if (format.charAt(i) === '\'') { + literalIdx = i; + i++; + continue; + } + + formatter = constructFormatterFromIdx(format, i); + + formatters.push(formatter.parser); + i = formatter.endIdx; + } + + return formatters; + } + + function constructLiteralFormatter(format, literalIdx, endIdx) { + return function() { + return format.substr(literalIdx + 1, endIdx - literalIdx - 1); + }; + } + + function constructFormatterFromIdx(format, i) { + var currentPosStr = format.substr(i); + for (var j = 0; j < formatCodeToRegex.length; j++) { + if (new RegExp('^' + formatCodeToRegex[j].key).test(currentPosStr)) { + var data = formatCodeToRegex[j]; + return { + endIdx: i + data.key.length, + parser: data.formatter + }; + } + } + + return { + endIdx: i + 1, + parser: function() { + return currentPosStr.charAt(0); + } + }; + } + this.filter = function(date, format) { if (!angular.isDate(date) || isNaN(date) || !format) { return ''; @@ -307,28 +397,13 @@ angular.module('ui.bootstrap.dateparser', []) } if (!this.formatters[format]) { - this.formatters[format] = createParser(format, 'formatter'); + this.formatters[format] = createFormatter(format); } - var parser = this.formatters[format], - map = parser.map; - - var _format = format; - - return map.reduce(function(str, mapper, i) { - var match = _format.match(new RegExp('(.*)' + mapper.key)); - if (match && angular.isString(match[1])) { - str += match[1]; - _format = _format.replace(match[1] + mapper.key, ''); - } - - var endStr = i === map.length - 1 ? _format : ''; - - if (mapper.apply) { - return str + mapper.apply.call(null, date) + endStr; - } + var formatters = this.formatters[format]; - return str + endStr; + return formatters.reduce(function(str, formatter) { + return str + formatter(date); }, ''); }; diff --git a/src/dateparser/docs/README.md b/src/dateparser/docs/README.md index d5cc38fab9..f7a3b3434f 100644 --- a/src/dateparser/docs/README.md +++ b/src/dateparser/docs/README.md @@ -58,6 +58,9 @@ Certain format codes support i18n. Check this [guide](https://docs.angularjs.org _(Example: `3` or `03`)_ - Parses a numeric month, but allowing an optional leading zero +* `LLLL` + _(Example: `February`, i18n support)_ - Stand-alone month in year (January-December). Requires Angular version 1.5.1 or higher. + * `dd` _(Example: `05`, Leading 0)_ - Parses a numeric day. diff --git a/src/dateparser/test/dateparser.spec.js b/src/dateparser/test/dateparser.spec.js index 8b96a7c77a..70c4b3e4f8 100644 --- a/src/dateparser/test/dateparser.spec.js +++ b/src/dateparser/test/dateparser.spec.js @@ -84,6 +84,16 @@ describe('date parser', function() { expectFilter(new Date(2011, 4, 2, 0), 'dd-M!-yy', '02-05-11'); expectFilter(oldDate, 'yyyy/M!/dd', '0001/03/06'); }); + + it('should work correctly for `LLLL`', function() { + expectFilter(new Date(2013, 7, 24, 0), 'LLLL/dd/yyyy', 'August/24/2013'); + expectFilter(new Date(2004, 10, 7, 0), 'dd.LLLL.yy', '07.November.04'); + expectFilter(new Date(2011, 4, 18, 0), 'dd-LLLL-yy', '18-May-11'); + expectFilter(new Date(1980, 1, 5, 0), 'LLLL/dd/yyyy', 'February/05/1980'); + expectFilter(new Date(1955, 2, 5, 0), 'yyyy/LLLL/dd', '1955/March/05'); + expectFilter(new Date(2011, 5, 2, 0), 'dd-LLLL-yy', '02-June-11'); + expectFilter(oldDate, 'yyyy/LLLL/dd', '0001/March/06'); + }); it('should work correctly for `d`', function() { expectFilter(new Date(2013, 10, 17, 0), 'd.MMMM.yy', '17.November.13'); @@ -565,27 +575,27 @@ describe('date parser', function() { }); describe('with value literals', function() { - // describe('filter', function() { - // it('should work with multiple literals', function() { - // expect(dateParser.filter(new Date(2013, 0, 29), 'd \'de\' MMMM \'de\' y')).toEqual('29 de January de 2013'); - // }); - // - // it('should work with escaped single quote', function() { - // expect(dateParser.filter(new Date(2015, 2, 22, 12), 'd.MMMM.yy h \'o\'\'clock\'')).toEqual('22.March.15 12 o\'clock'); - // }); - // - // it('should work with only a single quote', function() { - // expect(dateParser.filter(new Date(2015, 2, 22), 'd.MMMM.yy \'\'\'')).toEqual('22.March.15 \''); - // }); - // - // it('should work with trailing literal', function() { - // expect(dateParser.filter(new Date(2013, 0, 1), '\'year\' y')).toEqual('year 2013'); - // }); - // - // it('should work without whitespace', function() { - // expect(dateParser.filter(new Date(2013, 0, 1), '\'year:\'y')).toEqual('year:2013'); - // }); - // }); + describe('filter', function() { + it('should work with multiple literals', function() { + expect(dateParser.filter(new Date(2013, 0, 29), 'd \'de\' MMMM \'de\' y')).toEqual('29 de January de 2013'); + }); + + it('should work with escaped single quote', function() { + expect(dateParser.filter(new Date(2015, 2, 22, 12), 'd.MMMM.yy h \'o\'\'clock\'')).toEqual('22.March.15 12 o\'clock'); + }); + + it('should work with only a single quote', function() { + expect(dateParser.filter(new Date(2015, 2, 22), 'd.MMMM.yy \'\'\'')).toEqual('22.March.15 \''); + }); + + it('should work with trailing literal', function() { + expect(dateParser.filter(new Date(2013, 0, 1), '\'year\' y')).toEqual('year 2013'); + }); + + it('should work without whitespace', function() { + expect(dateParser.filter(new Date(2013, 0, 1), '\'year:\'y')).toEqual('year:2013'); + }); + }); describe('parse', function() { it('should work with multiple literals', function() { @@ -781,4 +791,28 @@ describe('date parser', function() { }); }); }); + + describe('overrideParser', function() { + var twoDigitYearParser = function (value) { + this.year = +value + (+value > 30 ? 1900 : 2000); + }; + + it('should get the current parser', function() { + expect(dateParser.getParser('yy')).toBeTruthy(); + }); + + it('should override the parser', function() { + dateParser.overrideParser('yy', twoDigitYearParser); + expect(dateParser.parse('68', 'yy').getFullYear()).toEqual(1968); + expect(dateParser.parse('67', 'yy').getFullYear()).toEqual(1967); + expect(dateParser.parse('31', 'yy').getFullYear()).toEqual(1931); + expect(dateParser.parse('30', 'yy').getFullYear()).toEqual(2030); + }); + + it('should clear cached parsers', function() { + expect(dateParser.parse('68', 'yy').getFullYear()).toEqual(2068); + dateParser.overrideParser('yy', twoDigitYearParser); + expect(dateParser.parse('68', 'yy').getFullYear()).toEqual(1968); + }); + }); }); diff --git a/src/datepicker/datepicker.js b/src/datepicker/datepicker.js index cad3c5067a..fd4e263660 100644 --- a/src/datepicker/datepicker.js +++ b/src/datepicker/datepicker.js @@ -16,6 +16,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst maxMode: 'year', minDate: null, minMode: 'day', + monthColumns: 3, ngModelOptions: {}, shortcutPropagation: false, showWeeks: true, @@ -23,13 +24,15 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst yearRows: 4 }) -.controller('UibDatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$locale', '$log', 'dateFilter', 'uibDatepickerConfig', '$datepickerLiteralWarning', '$datepickerSuppressError', 'uibDateParser', - function($scope, $attrs, $parse, $interpolate, $locale, $log, dateFilter, datepickerConfig, $datepickerLiteralWarning, $datepickerSuppressError, dateParser) { +.controller('UibDatepickerController', ['$scope', '$element', '$attrs', '$parse', '$interpolate', '$locale', '$log', 'dateFilter', 'uibDatepickerConfig', '$datepickerLiteralWarning', '$datepickerSuppressError', 'uibDateParser', + function($scope, $element, $attrs, $parse, $interpolate, $locale, $log, dateFilter, datepickerConfig, $datepickerLiteralWarning, $datepickerSuppressError, dateParser) { var self = this, ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl; ngModelOptions = {}, - watchListeners = [], - optionsUsed = !!$attrs.datepickerOptions; + watchListeners = []; + + $element.addClass('uib-datepicker'); + $attrs.$set('role', 'application'); if (!$scope.datepickerOptions) { $scope.datepickerOptions = {}; @@ -52,6 +55,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst 'maxMode', 'minDate', 'minMode', + 'monthColumns', 'showWeeks', 'shortcutPropagation', 'startingDay', @@ -77,6 +81,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst $interpolate($scope.datepickerOptions[key])($scope.$parent) : datepickerConfig[key]; break; + case 'monthColumns': case 'showWeeks': case 'shortcutPropagation': case 'yearColumns': @@ -99,7 +104,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst $scope.$watch('datepickerOptions.' + key, function(value) { if (value) { if (angular.isDate(value)) { - self[key] = dateParser.fromTimezone(new Date(value), ngModelOptions.timezone); + self[key] = dateParser.fromTimezone(new Date(value), ngModelOptions.getOption('timezone')); } else { if ($datepickerLiteralWarning) { $log.warn('Literal date support has been deprecated, please switch to date object usage'); @@ -109,7 +114,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst } } else { self[key] = datepickerConfig[key] ? - dateParser.fromTimezone(new Date(datepickerConfig[key]), ngModelOptions.timezone) : + dateParser.fromTimezone(new Date(datepickerConfig[key]), ngModelOptions.getOption('timezone')) : null; } @@ -121,7 +126,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst case 'minMode': if ($scope.datepickerOptions[key]) { $scope.$watch(function() { return $scope.datepickerOptions[key]; }, function(value) { - self[key] = $scope[key] = angular.isDefined(value) ? value : datepickerOptions[key]; + self[key] = $scope[key] = angular.isDefined(value) ? value : $scope.datepickerOptions[key]; if (key === 'minMode' && self.modes.indexOf($scope.datepickerOptions.datepickerMode) < self.modes.indexOf(self[key]) || key === 'maxMode' && self.modes.indexOf($scope.datepickerOptions.datepickerMode) > self.modes.indexOf(self[key])) { $scope.datepickerMode = self[key]; @@ -156,14 +161,13 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst this.init = function(ngModelCtrl_) { ngModelCtrl = ngModelCtrl_; - ngModelOptions = ngModelCtrl_.$options || - $scope.datepickerOptions.ngModelOptions || - datepickerConfig.ngModelOptions; + ngModelOptions = extractOptions(ngModelCtrl); + if ($scope.datepickerOptions.initDate) { - self.activeDate = dateParser.fromTimezone($scope.datepickerOptions.initDate, ngModelOptions.timezone) || new Date(); + self.activeDate = dateParser.fromTimezone($scope.datepickerOptions.initDate, ngModelOptions.getOption('timezone')) || new Date(); $scope.$watch('datepickerOptions.initDate', function(initDate) { if (initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)) { - self.activeDate = dateParser.fromTimezone(initDate, ngModelOptions.timezone); + self.activeDate = dateParser.fromTimezone(initDate, ngModelOptions.getOption('timezone')); self.refreshView(); } }); @@ -173,8 +177,8 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst var date = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : new Date(); this.activeDate = !isNaN(date) ? - dateParser.fromTimezone(date, ngModelOptions.timezone) : - dateParser.fromTimezone(new Date(), ngModelOptions.timezone); + dateParser.fromTimezone(date, ngModelOptions.getOption('timezone')) : + dateParser.fromTimezone(new Date(), ngModelOptions.getOption('timezone')); ngModelCtrl.$render = function() { self.render(); @@ -187,7 +191,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst isValid = !isNaN(date); if (isValid) { - this.activeDate = dateParser.fromTimezone(date, ngModelOptions.timezone); + this.activeDate = dateParser.fromTimezone(date, ngModelOptions.getOption('timezone')); } else if (!$datepickerSuppressError) { $log.error('Datepicker directive: "ng-model" value must be a Date object'); } @@ -204,7 +208,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst } var date = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null; - date = dateParser.fromTimezone(date, ngModelOptions.timezone); + date = dateParser.fromTimezone(date, ngModelOptions.getOption('timezone')); ngModelCtrl.$setValidity('dateDisabled', !date || this.element && !this.isDisabled(date)); } @@ -212,9 +216,9 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst this.createDateObject = function(date, format) { var model = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null; - model = dateParser.fromTimezone(model, ngModelOptions.timezone); + model = dateParser.fromTimezone(model, ngModelOptions.getOption('timezone')); var today = new Date(); - today = dateParser.fromTimezone(today, ngModelOptions.timezone); + today = dateParser.fromTimezone(today, ngModelOptions.getOption('timezone')); var time = this.compare(date, today); var dt = { date: date, @@ -260,9 +264,9 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst $scope.select = function(date) { if ($scope.datepickerMode === self.minMode) { - var dt = ngModelCtrl.$viewValue ? dateParser.fromTimezone(new Date(ngModelCtrl.$viewValue), ngModelOptions.timezone) : new Date(0, 0, 0, 0, 0, 0, 0); + var dt = ngModelCtrl.$viewValue ? dateParser.fromTimezone(new Date(ngModelCtrl.$viewValue), ngModelOptions.getOption('timezone')) : new Date(0, 0, 0, 0, 0, 0, 0); dt.setFullYear(date.getFullYear(), date.getMonth(), date.getDate()); - dt = dateParser.toTimezone(dt, ngModelOptions.timezone); + dt = dateParser.toTimezone(dt, ngModelOptions.getOption('timezone')); ngModelCtrl.$setViewValue(dt); ngModelCtrl.$render(); } else { @@ -330,6 +334,12 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst } }; + $element.on('keydown', function(evt) { + $scope.$apply(function() { + $scope.keydown(evt); + }); + }); + $scope.$on('$destroy', function() { //Clear all watch listeners on destroy while (watchListeners.length) { @@ -341,6 +351,37 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst $scope.datepickerMode = mode; $scope.datepickerOptions.datepickerMode = mode; } + + function extractOptions(ngModelCtrl) { + var ngModelOptions; + + if (angular.version.minor < 6) { // in angular < 1.6 $options could be missing + // guarantee a value + ngModelOptions = ngModelCtrl.$options || + $scope.datepickerOptions.ngModelOptions || + datepickerConfig.ngModelOptions || + {}; + + // mimic 1.6+ api + ngModelOptions.getOption = function (key) { + return ngModelOptions[key]; + }; + } else { // in angular >=1.6 $options is always present + // ng-model-options defaults timezone to null; don't let its precedence squash a non-null value + var timezone = ngModelCtrl.$options.getOption('timezone') || + ($scope.datepickerOptions.ngModelOptions ? $scope.datepickerOptions.ngModelOptions.timezone : null) || + (datepickerConfig.ngModelOptions ? datepickerConfig.ngModelOptions.timezone : null); + + // values passed to createChild override existing values + ngModelOptions = ngModelCtrl.$options // start with a ModelOptions instance + .createChild(datepickerConfig.ngModelOptions) // lowest precedence + .createChild($scope.datepickerOptions.ngModelOptions) + .createChild(ngModelCtrl.$options) // highest precedence + .createChild({timezone: timezone}); // to keep from squashing a non-null value + } + + return ngModelOptions; + } }]) .controller('UibDaypickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) { @@ -480,7 +521,8 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst } scope.title = dateFilter(this.activeDate, this.formatMonthTitle); - scope.rows = this.split(months, 3); + scope.rows = this.split(months, this.monthColumns); + scope.yearHeaderColspan = this.monthColumns > 3 ? this.monthColumns - 2 : 1; }; this.compare = function(date1, date2) { @@ -497,11 +539,11 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst if (key === 'left') { date = date - 1; } else if (key === 'up') { - date = date - 3; + date = date - this.monthColumns; } else if (key === 'right') { date = date + 1; } else if (key === 'down') { - date = date + 3; + date = date + this.monthColumns; } else if (key === 'pageup' || key === 'pagedown') { var year = this.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1); this.activeDate.setFullYear(year); @@ -572,7 +614,6 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst .directive('uibDatepicker', function() { return { - replace: true, templateUrl: function(element, attrs) { return attrs.templateUrl || 'uib/template/datepicker/datepicker.html'; }, @@ -580,6 +621,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst datepickerOptions: '=?' }, require: ['uibDatepicker', '^ngModel'], + restrict: 'A', controller: 'UibDatepickerController', controllerAs: 'datepicker', link: function(scope, element, attrs, ctrls) { @@ -592,11 +634,11 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst .directive('uibDaypicker', function() { return { - replace: true, templateUrl: function(element, attrs) { return attrs.templateUrl || 'uib/template/datepicker/day.html'; }, require: ['^uibDatepicker', 'uibDaypicker'], + restrict: 'A', controller: 'UibDaypickerController', link: function(scope, element, attrs, ctrls) { var datepickerCtrl = ctrls[0], @@ -609,11 +651,11 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst .directive('uibMonthpicker', function() { return { - replace: true, templateUrl: function(element, attrs) { return attrs.templateUrl || 'uib/template/datepicker/month.html'; }, require: ['^uibDatepicker', 'uibMonthpicker'], + restrict: 'A', controller: 'UibMonthpickerController', link: function(scope, element, attrs, ctrls) { var datepickerCtrl = ctrls[0], @@ -626,11 +668,11 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst .directive('uibYearpicker', function() { return { - replace: true, templateUrl: function(element, attrs) { return attrs.templateUrl || 'uib/template/datepicker/year.html'; }, require: ['^uibDatepicker', 'uibYearpicker'], + restrict: 'A', controller: 'UibYearpickerController', link: function(scope, element, attrs, ctrls) { var ctrl = ctrls[0]; diff --git a/src/datepicker/docs/demo.html b/src/datepicker/docs/demo.html index 0f96d634d6..e083965698 100644 --- a/src/datepicker/docs/demo.html +++ b/src/datepicker/docs/demo.html @@ -15,7 +15,7 @@

Inline

- +

diff --git a/src/datepicker/docs/readme.md b/src/datepicker/docs/readme.md index e46d00f47b..d1928e272d 100644 --- a/src/datepicker/docs/readme.md +++ b/src/datepicker/docs/readme.md @@ -104,6 +104,11 @@ Apart from the previous settings, to configure the uib-datepicker you need to cr _(Default: `day`)_ - Sets a lower limit for mode. + * `monthColumns` + C + _(Default: `3`)_ - + Number of columns displayed in month selection. + * `ngModelOptions` C _(Default: `null`)_ - @@ -123,7 +128,7 @@ Apart from the previous settings, to configure the uib-datepicker you need to cr C *(Default: `$locale.DATETIME_FORMATS.FIRSTDAYOFWEEK`)* - Starting day of the week from 0-6 (0=Sunday, ..., 6=Saturday). - + * `yearRows` C _(Default: `4`)_ - diff --git a/src/datepicker/test/datepicker.spec.js b/src/datepicker/test/datepicker.spec.js index 1a1e06aa62..8dd924c8d8 100644 --- a/src/datepicker/test/datepicker.spec.js +++ b/src/datepicker/test/datepicker.spec.js @@ -26,8 +26,12 @@ describe('datepicker', function() { }); })); + function getTitleCell() { + return element.find('th').eq(1); + } + function getTitleButton() { - return element.find('th').eq(1).find('button').first(); + return getTitleCell().find('button').first(); } function getTitle() { @@ -155,7 +159,7 @@ describe('datepicker', function() { $scope.options = { minDate: '1984-01-01' }; - element = $compile('')($scope); + element = $compile('
')($scope); $scope.$digest(); expect($log.warn).toHaveBeenCalledWith('Literal date support has been deprecated, please switch to date object usage'); @@ -175,7 +179,7 @@ describe('datepicker', function() { $scope.options = { minDate: '1984-01-01' }; - element = $compile('')($scope); + element = $compile('
')($scope); $scope.$digest(); expect($log.warn).not.toHaveBeenCalled(); @@ -192,7 +196,7 @@ describe('datepicker', function() { $scope.options = { maxDate: '1984-01-01' }; - element = $compile('')($scope); + element = $compile('
')($scope); $scope.$digest(); expect($log.warn).toHaveBeenCalledWith('Literal date support has been deprecated, please switch to date object usage'); @@ -212,7 +216,7 @@ describe('datepicker', function() { $scope.options = { maxDate: '1984-01-01' }; - element = $compile('')($scope); + element = $compile('
')($scope); $scope.$digest(); expect($log.warn).not.toHaveBeenCalled(); @@ -232,7 +236,7 @@ describe('datepicker', function() { }); spyOn($log, 'error'); - element = $compile('')($scope); + element = $compile('
')($scope); $scope.locals = { date: 'lalala' @@ -253,7 +257,7 @@ describe('datepicker', function() { }); spyOn($log, 'error'); - element = $compile('')($scope); + element = $compile('
')($scope); $scope.locals = { date: 'lalala' @@ -274,7 +278,7 @@ describe('datepicker', function() { }); spyOn($log, 'error'); - element = $compile('')($scope); + element = $compile('
')($scope); $scope.locals = { date: 'lalala' @@ -305,7 +309,7 @@ describe('datepicker', function() { var baseTime = new Date(2015, 2, 23); jasmine.clock().mockDate(baseTime); - element = $compile('baz
'); - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); - expect(element.html()).toBe('baz'); + expect(element.html()).toBe('
baz
'); }); it('should support custom day, month and year templates', function() { @@ -327,15 +331,15 @@ describe('datepicker', function() { $templateCache.put('foo/year.html', '
year
'); $templateCache.put('foo/bar.html', '
' + - '' + - '' + - '' + + '
' + + '
' + + '
' + '
'); - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); - var expectedHtml = '
day
month
year
'; + var expectedHtml = '
day
month
year
'; expect(element.html()).toBe(expectedHtml); }); @@ -343,22 +347,22 @@ describe('datepicker', function() { it('should expose the controller in the template', function() { $templateCache.put('uib/template/datepicker/datepicker.html', '
{{datepicker.text}}
'); - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); var ctrl = element.controller('uib-datepicker'); expect(ctrl).toBeDefined(); - expect(element.html()).toBe(''); + expect(element.html()).toBe('
'); ctrl.text = 'baz'; $rootScope.$digest(); - expect(element.html()).toBe('baz'); + expect(element.html()).toBe('
baz
'); }); describe('basic functionality', function() { beforeEach(function() { - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); }); @@ -961,7 +965,7 @@ describe('datepicker', function() { timezone: '+600' } }; - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); })); @@ -975,7 +979,7 @@ describe('datepicker', function() { $rootScope.options = { startingDay: 1 }; - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); }); @@ -1004,7 +1008,7 @@ describe('datepicker', function() { $rootScope.options = { showWeeks: false }; - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); }); @@ -1021,7 +1025,7 @@ describe('datepicker', function() { beforeEach(function() { $rootScope.options = {}; $rootScope.date = new Date('September 10, 2010'); - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); }); @@ -1046,7 +1050,7 @@ describe('datepicker', function() { $rootScope.options = { minDate: new Date('September 12, 2010') }; - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); }); @@ -1131,7 +1135,7 @@ describe('datepicker', function() { it('accepts literals, \'yyyy-MM-dd\' case', function() { $rootScope.options.minDate = '2010-09-05'; - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); var buttons = getAllOptionsEl(); angular.forEach(buttons, function(button, index) { @@ -1144,7 +1148,7 @@ describe('datepicker', function() { beforeEach(function() { $rootScope.options = {}; $rootScope.date = new Date('September 10, 2010'); - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); }); @@ -1169,7 +1173,7 @@ describe('datepicker', function() { $rootScope.options = { maxDate: new Date('September 25, 2010') }; - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); }); @@ -1261,8 +1265,8 @@ describe('datepicker', function() { yearColumns: 4, yearRows: 3 }; - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); }); @@ -1325,8 +1329,9 @@ describe('datepicker', function() { uibDatepickerConfig.yearRows = 2; uibDatepickerConfig.yearColumns = 5; uibDatepickerConfig.startingDay = 6; + uibDatepickerConfig.monthColumns = 4; - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); })); afterEach(inject(function(uibDatepickerConfig) { @@ -1346,13 +1351,17 @@ describe('datepicker', function() { expect(getTitle()).toBe('10'); expect(getOptions()).toEqual([ - ['Jan', 'Feb', 'Mar'], - ['Apr', 'May', 'Jun'], - ['Jul', 'Aug', 'Sep'], - ['Oct', 'Nov', 'Dec'] + ['Jan', 'Feb', 'Mar', 'Apr'], + ['May', 'Jun', 'Jul', 'Aug'], + ['Sep', 'Oct', 'Nov', 'Dec'] ]); }); + it('shows title year button to expand to fill width in `month` mode', function() { + clickTitleButton(); + expect(getTitleCell().attr('colspan')).toBe('2'); + }); + it('changes the title, year format & range in `year` mode', function() { clickTitleButton(); clickTitleButton(); @@ -1387,7 +1396,7 @@ describe('datepicker', function() { describe('disabled', function() { beforeEach(function() { - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); }); @@ -1401,7 +1410,7 @@ describe('datepicker', function() { describe('ng-disabled', function() { beforeEach(function() { $rootScope.disabled = false; - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); }); @@ -1442,7 +1451,7 @@ describe('datepicker', function() { describe('basics', function() { beforeEach(function() { - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); }); @@ -1461,7 +1470,7 @@ describe('datepicker', function() { initDate: new Date('2006-01-01T00:00:00.000Z') }; $rootScope.date = null; - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); expect(getTitle()).toEqual('January 2006'); @@ -1471,7 +1480,7 @@ describe('datepicker', function() { $rootScope.options = { minDate: new Date('2010-10-01T00:00:00.000Z') }; - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); expect(getSelectedElement().prop('disabled')).toBe(true); @@ -1484,7 +1493,7 @@ describe('datepicker', function() { beforeEach(inject(function() { $rootScope.date = new Date('2005-11-07T10:00:00.000Z'); $rootScope.ngModelOptions = { timezone: '+600'}; - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); })); @@ -1502,7 +1511,7 @@ describe('datepicker', function() { describe('with empty initial state', function() { beforeEach(inject(function() { $rootScope.date = null; - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); })); @@ -1531,7 +1540,7 @@ describe('datepicker', function() { $rootScope.options = { initDate: new Date('November 9, 1980') }; - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); })); @@ -1550,7 +1559,7 @@ describe('datepicker', function() { $rootScope.options = { datepickerMode: 'month' }; - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); })); @@ -1571,7 +1580,7 @@ describe('datepicker', function() { minMode: 'month', datepickerMode: 'month' }; - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); })); @@ -1601,7 +1610,7 @@ describe('datepicker', function() { $rootScope.options = { maxMode: 'month' }; - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); })); @@ -1649,7 +1658,7 @@ describe('datepicker', function() { // Use dateModel directive to add formatters and parsers to the // ngModelController that translate the custom date object. - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); })); @@ -1674,7 +1683,7 @@ describe('datepicker', function() { })); it('with the default starting day (sunday)', function() { - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); expect(getWeeks()).toEqual(['23', '24', '25', '26', '27', '28']); @@ -1685,7 +1694,7 @@ describe('datepicker', function() { $rootScope.options = { startingDay: 1 }; - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); expect(getWeeks()).toEqual(['22', '23', '24', '25', '26', '27']); @@ -1695,7 +1704,7 @@ describe('datepicker', function() { $rootScope.options = { startingDay: 4 }; - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); expect(getWeeks()).toEqual(['22', '23', '24', '25', '26', '27']); @@ -1705,7 +1714,7 @@ describe('datepicker', function() { $rootScope.options = { startingDay: 6 }; - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); expect(getWeeks()).toEqual(['23', '24', '25', '26', '27', '28']); @@ -1715,7 +1724,7 @@ describe('datepicker', function() { describe('first week in january', function() { it('in current year', function() { $rootScope.date = new Date('January 07, 2014'); - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); expect(getWeeks()).toEqual(['1', '2', '3', '4', '5', '6']); @@ -1723,7 +1732,7 @@ describe('datepicker', function() { it('in last year', function() { $rootScope.date = new Date('January 07, 2010'); - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); expect(getWeeks()).toEqual(['53', '1', '2', '3', '4', '5']); @@ -1736,7 +1745,7 @@ describe('datepicker', function() { })); it('in next year', function() { - element = $compile('')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); expect(getWeeks()).toEqual(['49', '50', '51', '52', '1', '2']); diff --git a/src/datepickerPopup/index-nocss.js b/src/datepickerPopup/index-nocss.js index ad25a942e4..d9498dec39 100644 --- a/src/datepickerPopup/index-nocss.js +++ b/src/datepickerPopup/index-nocss.js @@ -5,6 +5,6 @@ require('./popup.js'); var MODULE_NAME = 'ui.bootstrap.module.datepickerPopup'; -angular.module(MODULE_NAME, ['ui.bootstrap.datepickerPopup', 'uib/template/datepickerPopup/popup.html']); +angular.module(MODULE_NAME, ['ui.bootstrap.datepickerPopup', 'uib/template/datepickerPopup/popup.html', 'ui.bootstrap.module.datepicker']); module.exports = MODULE_NAME; diff --git a/src/datepickerPopup/popup.js b/src/datepickerPopup/popup.js index 12ef4545a8..02c0e88a1c 100644 --- a/src/datepickerPopup/popup.js +++ b/src/datepickerPopup/popup.js @@ -32,7 +32,7 @@ function($scope, $element, $attrs, $compile, $log, $parse, $window, $document, $ this.init = function(_ngModel_) { ngModel = _ngModel_; - ngModelOptions = _ngModel_.$options; + ngModelOptions = extractOptions(ngModel); closeOnDateSelection = angular.isDefined($attrs.closeOnDateSelection) ? $scope.$parent.$eval($attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection; @@ -123,13 +123,13 @@ function($scope, $element, $attrs, $compile, $log, $parse, $window, $document, $ value = new Date(value); } - $scope.date = value; + $scope.date = dateParser.fromTimezone(value, ngModelOptions.getOption('timezone')); return dateParser.filter($scope.date, dateFormat); }); } else { ngModel.$formatters.push(function(value) { - $scope.date = value; + $scope.date = dateParser.fromTimezone(value, ngModelOptions.getOption('timezone')); return value; }); } @@ -181,7 +181,7 @@ function($scope, $element, $attrs, $compile, $log, $parse, $window, $document, $ $scope.isDisabled = function(date) { if (date === 'today') { - date = new Date(); + date = dateParser.fromTimezone(new Date(), ngModelOptions.getOption('timezone')); } var dates = {}; @@ -238,7 +238,8 @@ function($scope, $element, $attrs, $compile, $log, $parse, $window, $document, $ date = new Date($scope.date); date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate()); } else { - date = new Date(today.setHours(0, 0, 0, 0)); + date = dateParser.fromTimezone(today, ngModelOptions.getOption('timezone')); + date.setHours(0, 0, 0, 0); } } $scope.dateSelection(date); @@ -328,11 +329,11 @@ function($scope, $element, $attrs, $compile, $log, $parse, $window, $document, $ if (angular.isString(viewValue)) { var date = parseDateString(viewValue); if (!isNaN(date)) { - return date; + return dateParser.toTimezone(date, ngModelOptions.getOption('timezone')); } } - return ngModel.$options && ngModel.$options.allowInvalid ? viewValue : undefined; + return ngModelOptions.getOption('allowInvalid') ? viewValue : undefined; } function validator(modelValue, viewValue) { @@ -355,7 +356,7 @@ function($scope, $element, $attrs, $compile, $log, $parse, $window, $document, $ } if (angular.isString(value)) { - return !isNaN(parseDateString(viewValue)); + return !isNaN(parseDateString(value)); } return false; @@ -407,6 +408,28 @@ function($scope, $element, $attrs, $compile, $log, $parse, $window, $document, $ } } + function extractOptions(ngModelCtrl) { + var ngModelOptions; + + if (angular.version.minor < 6) { // in angular < 1.6 $options could be missing + // guarantee a value + ngModelOptions = angular.isObject(ngModelCtrl.$options) ? + ngModelCtrl.$options : + { + timezone: null + }; + + // mimic 1.6+ api + ngModelOptions.getOption = function (key) { + return ngModelOptions[key]; + }; + } else { // in angular >=1.6 $options is always present + ngModelOptions = ngModelCtrl.$options; + } + + return ngModelOptions; + } + $scope.$on('uib:datepicker.mode', function() { $timeout(positionPopup, 0, false); }); @@ -434,7 +457,7 @@ function($scope, $element, $attrs, $compile, $log, $parse, $window, $document, $ .directive('uibDatepickerPopupWrap', function() { return { - replace: true, + restrict: 'A', transclude: true, templateUrl: function(element, attrs) { return attrs.templateUrl || 'uib/template/datepickerPopup/popup.html'; diff --git a/src/datepickerPopup/test/popup.spec.js b/src/datepickerPopup/test/popup.spec.js index c26841c208..227fdb3596 100644 --- a/src/datepickerPopup/test/popup.spec.js +++ b/src/datepickerPopup/test/popup.spec.js @@ -397,7 +397,7 @@ describe('datepicker popup', function() { it('stops the ESC key from propagating if the dropdown is open, but not when closed', function() { var documentKey = -1; var getKey = function(evt) { documentKey = evt.which; }; - $document.bind('keydown', getKey); + $document.on('keydown', getKey); triggerKeyDown(inputEl, 'esc'); expect(documentKey).toBe(-1); @@ -405,7 +405,7 @@ describe('datepicker popup', function() { triggerKeyDown(inputEl, 'esc'); expect(documentKey).toBe(27); - $document.unbind('keydown', getKey); + $document.off('keydown', getKey); }); }); @@ -1344,7 +1344,7 @@ describe('datepicker popup', function() { $compile(elm)($rootScope); $rootScope.$digest(); - expect(elm.children().eq(1).html()).toBe('baz'); + expect(elm.children().eq(1).html()).toBe('
baz
'); }); }); @@ -1367,7 +1367,7 @@ describe('datepicker popup', function() { var datepicker = elm.find('[uib-datepicker]'); - expect(datepicker.html()).toBe('baz'); + expect(datepicker.html()).toBe('
baz
'); }); }); diff --git a/src/dropdown/docs/readme.md b/src/dropdown/docs/readme.md index 09b646d072..3026673ce2 100644 --- a/src/dropdown/docs/readme.md +++ b/src/dropdown/docs/readme.md @@ -25,7 +25,7 @@ Each of these parts need to be used as attribute directives. * `dropdown-append-to-body` B _(Default: `false`)_ - - Appends the inner dropdown-menu to the body element. + Appends the inner dropdown-menu to the body element if the attribute is present without a value, or with a non `false` value. * `is-open` $ diff --git a/src/dropdown/dropdown.js b/src/dropdown/dropdown.js index 40d2bbe0c1..2560e282e8 100644 --- a/src/dropdown/dropdown.js +++ b/src/dropdown/dropdown.js @@ -1,17 +1,35 @@ -angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) +angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.multiMap', 'ui.bootstrap.position']) .constant('uibDropdownConfig', { appendToOpenClass: 'uib-dropdown-open', openClass: 'open' }) -.service('uibDropdownService', ['$document', '$rootScope', function($document, $rootScope) { +.service('uibDropdownService', ['$document', '$rootScope', '$$multiMap', function($document, $rootScope, $$multiMap) { var openScope = null; + var openedContainers = $$multiMap.createNew(); + + this.isOnlyOpen = function(dropdownScope, appendTo) { + var openedDropdowns = openedContainers.get(appendTo); + if (openedDropdowns) { + var openDropdown = openedDropdowns.reduce(function(toClose, dropdown) { + if (dropdown.scope === dropdownScope) { + return dropdown; + } + + return toClose; + }, {}); + if (openDropdown) { + return openedDropdowns.length === 1; + } + } + + return false; + }; - this.open = function(dropdownScope, element) { + this.open = function(dropdownScope, element, appendTo) { if (!openScope) { $document.on('click', closeDropdown); - element.on('keydown', keybindFilter); } if (openScope && openScope !== dropdownScope) { @@ -19,20 +37,58 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) } openScope = dropdownScope; + + if (!appendTo) { + return; + } + + var openedDropdowns = openedContainers.get(appendTo); + if (openedDropdowns) { + var openedScopes = openedDropdowns.map(function(dropdown) { + return dropdown.scope; + }); + if (openedScopes.indexOf(dropdownScope) === -1) { + openedContainers.put(appendTo, { + scope: dropdownScope + }); + } + } else { + openedContainers.put(appendTo, { + scope: dropdownScope + }); + } }; - this.close = function(dropdownScope, element) { + this.close = function(dropdownScope, element, appendTo) { if (openScope === dropdownScope) { - openScope = null; $document.off('click', closeDropdown); - element.off('keydown', keybindFilter); + $document.off('keydown', this.keybindFilter); + openScope = null; + } + + if (!appendTo) { + return; + } + + var openedDropdowns = openedContainers.get(appendTo); + if (openedDropdowns) { + var dropdownToClose = openedDropdowns.reduce(function(toClose, dropdown) { + if (dropdown.scope === dropdownScope) { + return dropdown; + } + + return toClose; + }, {}); + if (dropdownToClose) { + openedContainers.remove(appendTo, dropdownToClose); + } } }; var closeDropdown = function(evt) { // This method may still be called during the same mouse event that // unbound this event handler. So check openScope before proceeding. - if (!openScope) { return; } + if (!openScope || !openScope.isOpen) { return; } if (evt && openScope.getAutoClose() === 'disabled') { return; } @@ -49,20 +105,29 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) return; } - openScope.isOpen = false; openScope.focusToggleElement(); + openScope.isOpen = false; if (!$rootScope.$$phase) { openScope.$apply(); } }; - var keybindFilter = function(evt) { + this.keybindFilter = function(evt) { + if (!openScope) { + // see this.close as ESC could have been pressed which kills the scope so we can not proceed + return; + } + + var dropdownElement = openScope.getDropdownElement(); + var toggleElement = openScope.getToggleElement(); + var dropdownElementTargeted = dropdownElement && dropdownElement[0].contains(evt.target); + var toggleElementTargeted = toggleElement && toggleElement[0].contains(evt.target); if (evt.which === 27) { evt.stopPropagation(); openScope.focusToggleElement(); closeDropdown(); - } else if (openScope.isKeynavEnabled() && [38, 40].indexOf(evt.which) !== -1 && openScope.isOpen) { + } else if (openScope.isKeynavEnabled() && [38, 40].indexOf(evt.which) !== -1 && openScope.isOpen && (dropdownElementTargeted || toggleElementTargeted)) { evt.preventDefault(); evt.stopPropagation(); openScope.focusDropdownEntry(evt.which); @@ -79,8 +144,6 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) getIsOpen, setIsOpen = angular.noop, toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop, - appendToBody = false, - appendTo = null, keynavEnabled = false, selectedOption = null, body = $document.find('body'); @@ -97,26 +160,7 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) }); } - if (angular.isDefined($attrs.dropdownAppendTo)) { - var appendToEl = $parse($attrs.dropdownAppendTo)(scope); - if (appendToEl) { - appendTo = angular.element(appendToEl); - } - } - - appendToBody = angular.isDefined($attrs.dropdownAppendToBody); keynavEnabled = angular.isDefined($attrs.keyboardNav); - - if (appendToBody && !appendTo) { - appendTo = body; - } - - if (appendTo && self.dropdownMenu) { - appendTo.append(self.dropdownMenu); - $element.on('$destroy', function handleDestroyEvent() { - self.dropdownMenu.remove(); - }); - } }; this.toggle = function(open) { @@ -188,12 +232,48 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) } }; + function removeDropdownMenu() { + $element.append(self.dropdownMenu); + } + scope.$watch('isOpen', function(isOpen, wasOpen) { + var appendTo = null, + appendToBody = false; + + if (angular.isDefined($attrs.dropdownAppendTo)) { + var appendToEl = $parse($attrs.dropdownAppendTo)(scope); + if (appendToEl) { + appendTo = angular.element(appendToEl); + } + } + + if (angular.isDefined($attrs.dropdownAppendToBody)) { + var appendToBodyValue = $parse($attrs.dropdownAppendToBody)(scope); + if (appendToBodyValue !== false) { + appendToBody = true; + } + } + + if (appendToBody && !appendTo) { + appendTo = body; + } + + if (appendTo && self.dropdownMenu) { + if (isOpen) { + appendTo.append(self.dropdownMenu); + $element.on('$destroy', removeDropdownMenu); + } else { + $element.off('$destroy', removeDropdownMenu); + removeDropdownMenu(); + } + } + if (appendTo && self.dropdownMenu) { var pos = $position.positionElements($element, self.dropdownMenu, 'bottom-left', true), css, rightalign, - scrollbarWidth; + scrollbarPadding, + scrollbarWidth = 0; css = { top: pos.top + 'px', @@ -206,7 +286,12 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) css.right = 'auto'; } else { css.left = 'auto'; - scrollbarWidth = $position.scrollbarWidth(true); + scrollbarPadding = $position.scrollbarPadding(appendTo); + + if (scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) { + scrollbarWidth = scrollbarPadding.scrollbarWidth; + } + css.right = window.innerWidth - scrollbarWidth - (pos.left + $element.prop('offsetWidth')) + 'px'; } @@ -230,10 +315,18 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) } var openContainer = appendTo ? appendTo : $element; - var hasOpenClass = openContainer.hasClass(appendTo ? appendToOpenClass : openClass); + var dropdownOpenClass = appendTo ? appendToOpenClass : openClass; + var hasOpenClass = openContainer.hasClass(dropdownOpenClass); + var isOnlyOpen = uibDropdownService.isOnlyOpen($scope, appendTo); if (hasOpenClass === !isOpen) { - $animate[isOpen ? 'addClass' : 'removeClass'](openContainer, appendTo ? appendToOpenClass : openClass).then(function() { + var toggleClass; + if (appendTo) { + toggleClass = !isOnlyOpen ? 'addClass' : 'removeClass'; + } else { + toggleClass = isOpen ? 'addClass' : 'removeClass'; + } + $animate[toggleClass](openContainer, dropdownOpenClass).then(function() { if (angular.isDefined(isOpen) && isOpen !== wasOpen) { toggleInvoker($scope, { open: !!isOpen }); } @@ -248,13 +341,17 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) var newEl = dropdownElement; self.dropdownMenu.replaceWith(newEl); self.dropdownMenu = newEl; + $document.on('keydown', uibDropdownService.keybindFilter); }); }); + } else { + $document.on('keydown', uibDropdownService.keybindFilter); } scope.focusToggleElement(); - uibDropdownService.open(scope, $element); + uibDropdownService.open(scope, $element, appendTo); } else { + uibDropdownService.close(scope, $element, appendTo); if (self.dropdownMenuTemplateUrl) { if (templateScope) { templateScope.$destroy(); @@ -264,7 +361,6 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) self.dropdownMenu = newEl; } - uibDropdownService.close(scope, $element); self.selectedOption = null; } @@ -328,7 +424,7 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) } }; - element.bind('click', toggleDropdown); + element.on('click', toggleDropdown); // WAI-ARIA element.attr({ 'aria-haspopup': true, 'aria-expanded': false }); @@ -337,7 +433,7 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) }); scope.$on('$destroy', function() { - element.unbind('click', toggleDropdown); + element.off('click', toggleDropdown); }); } }; diff --git a/src/dropdown/index-nocss.js b/src/dropdown/index-nocss.js index 3234abba0a..690d7008ff 100644 --- a/src/dropdown/index-nocss.js +++ b/src/dropdown/index-nocss.js @@ -1,3 +1,4 @@ +require('../multiMap'); require('../position/index-nocss.js'); require('./dropdown'); diff --git a/src/dropdown/test/dropdown.spec.js b/src/dropdown/test/dropdown.spec.js index 197d148580..17226b5c5e 100644 --- a/src/dropdown/test/dropdown.spec.js +++ b/src/dropdown/test/dropdown.spec.js @@ -35,7 +35,7 @@ describe('uib-dropdown', function() { describe('basic', function() { function dropdown() { - return $compile('
  • ')($rootScope); + return $compile('
  • ')($rootScope); } beforeEach(function() { @@ -69,9 +69,10 @@ describe('uib-dropdown', function() { }); it('should close on escape key & focus toggle element', function() { + var dropdownMenu = element.find('[uib-dropdown-menu]'); $document.find('body').append(element); clickDropdownToggle(); - var event = triggerKeyDown(element, 27); + var event = triggerKeyDown(dropdownMenu, 27); expect(element).not.toHaveClass(dropdownConfig.openClass); expect(element.find('a')).toHaveFocus(); expect(event.stopPropagation).toHaveBeenCalled(); @@ -98,17 +99,17 @@ describe('uib-dropdown', function() { expect(elm1).not.toHaveClass(dropdownConfig.openClass); expect(elm2).not.toHaveClass(dropdownConfig.openClass); - clickDropdownToggle( elm1 ); + clickDropdownToggle(elm1); expect(elm1).toHaveClass(dropdownConfig.openClass); expect(elm2).not.toHaveClass(dropdownConfig.openClass); - clickDropdownToggle( elm2 ); + clickDropdownToggle(elm2); expect(elm1).not.toHaveClass(dropdownConfig.openClass); expect(elm2).toHaveClass(dropdownConfig.openClass); }); it('should not toggle if the element has `disabled` class', function() { - var elm = $compile('
    • Hello
  • ')($rootScope); + var elm = $compile('
    • Hello
  • ')($rootScope); clickDropdownToggle( elm ); expect(elm).not.toHaveClass(dropdownConfig.openClass); }); @@ -121,7 +122,7 @@ describe('uib-dropdown', function() { it('should not toggle if the element has `ng-disabled` as true', function() { $rootScope.isdisabled = true; - var elm = $compile('
    • Hello
  • ')($rootScope); + var elm = $compile('
    • Hello
  • ')($rootScope); $rootScope.$digest(); elm.find('div').click(); expect(elm).not.toHaveClass(dropdownConfig.openClass); @@ -134,7 +135,7 @@ describe('uib-dropdown', function() { it('should unbind events on scope destroy', function() { var $scope = $rootScope.$new(); - var elm = $compile('
    • Hello
  • ')($scope); + var elm = $compile('
    • Hello
  • ')($scope); $scope.$digest(); var buttonEl = elm.find('button'); @@ -214,52 +215,288 @@ describe('uib-dropdown', function() { }); describe('using dropdown-append-to-body', function() { + describe('with no value', function() { + function dropdown() { + return $compile('
  • ')($rootScope); + } + + beforeEach(function() { + element = dropdown(); + $document.find('body').append(element); + }); + + afterEach(function() { + element.remove(); + }); + + it('does not add the menu to the body', function() { + expect($document.find('#dropdown-menu').parent()[0]).not.toBe($document.find('body')[0]); + }); + + describe('when toggled open', function() { + var toggle; + beforeEach(function() { + toggle = element.find('[uib-dropdown-toggle]'); + toggle.trigger('click'); + }); + it('adds the menu to the body', function() { + expect($document.find('#dropdown-menu').parent()[0]).toBe($document.find('body')[0]); + }); + + describe('when toggled closed', function() { + beforeEach(function() { + toggle.trigger('click'); + }); + it('removes the menu from body', function() { + expect($document.find('#dropdown-menu').parent()[0]).not.toBe($document.find('body')[0]); + }); + }); + + describe('when closed by clicking on menu', function() { + var menu; + beforeEach(function() { + menu = $document.find('#dropdown-menu a'); + menu.focus(); + menu.trigger('click'); + }); + it('focuses the dropdown element on close', function() { + expect(document.activeElement).toBe(toggle[0]); + }); + it('removes the menu from body', function() { + expect($document.find('#dropdown-menu').parent()[0]).not.toBe($document.find('body')[0]); + }); + }); + describe('when the dropdown is removed', function() { + beforeEach(function() { + element.remove(); + $rootScope.$digest(); + }); + it('removes the menu from body', function() { + expect($document.find('#dropdown-menu').parent()[0]).not.toBe($document.find('body')[0]); + }); + }); + }); + }); + + describe('with a value', function() { + function dropdown() { + return $compile('
  • ')($rootScope); + } + describe('that is not false', function() { + beforeEach(function() { + $rootScope.appendToBody = 'sure'; + + element = dropdown(); + $document.find('body').append(element); + }); + + afterEach(function() { + element.remove(); + }); + it('does not add the menu to the body', function() { + expect($document.find('#dropdown-menu').parent()[0]).not.toBe($document.find('body')[0]); + }); + + describe('when toggled open', function() { + var toggle; + beforeEach(function() { + toggle = element.find('[uib-dropdown-toggle]'); + toggle.trigger('click'); + }); + it('adds the menu to the body', function() { + expect($document.find('#dropdown-menu').parent()[0]).toBe($document.find('body')[0]); + }); + + describe('when toggled closed', function() { + beforeEach(function() { + toggle.trigger('click'); + }); + it('removes the menu from body', function() { + expect($document.find('#dropdown-menu').parent()[0]).not.toBe($document.find('body')[0]); + }); + }); + + describe('when closed by clicking on menu', function() { + var menu; + beforeEach(function() { + menu = $document.find('#dropdown-menu a'); + menu.focus(); + menu.trigger('click'); + }); + it('focuses the dropdown element on close', function() { + expect(document.activeElement).toBe(toggle[0]); + }); + it('removes the menu from body', function() { + expect($document.find('#dropdown-menu').parent()[0]).not.toBe($document.find('body')[0]); + }); + }); + describe('when the dropdown is removed', function() { + beforeEach(function() { + element.remove(); + $rootScope.$digest(); + }); + it('removes the menu from body', function() { + expect($document.find('#dropdown-menu').parent()[0]).not.toBe($document.find('body')[0]); + }); + }); + }); + }); + + describe('that is false', function() { + beforeEach(function() { + $rootScope.appendToBody = false; + + element = dropdown(); + $document.find('body').append(element); + }); + + afterEach(function() { + element.remove(); + }); + + it('does not add the menu to the body', function() { + expect($document.find('#dropdown-menu').parent()[0]).not.toBe($document.find('body')[0]); + }); + + describe('when toggled open', function() { + var toggle; + beforeEach(function() { + toggle = element.find('[uib-dropdown-toggle]'); + toggle.trigger('click'); + }); + it('does not add the menu to the body', function() { + expect($document.find('#dropdown-menu').parent()[0]).not.toBe($document.find('body')[0]); + }); + + describe('when toggled closed', function() { + beforeEach(function() { + toggle.trigger('click'); + }); + it('does not remove the menu', function() { + expect($document.find('#dropdown-menu').length).not.toEqual(0); + }); + }); + + describe('when closed by clicking on menu', function() { + var menu; + beforeEach(function() { + menu = $document.find('#dropdown-menu a'); + menu.focus(); + menu.trigger('click'); + }); + it('focuses the dropdown element on close', function() { + expect(document.activeElement).toBe(toggle[0]); + }); + it('does not removes the menu from body', function() { + expect($document.find('#dropdown-menu').parent()[0]).not.toBe($document.find('body')[0]); + }); + }); + describe('when the dropdown is removed', function() { + beforeEach(function() { + element.remove(); + $rootScope.$digest(); + }); + it('removes the menu from body', function() { + expect($document.find('#dropdown-menu').parent()[0]).not.toBe($document.find('body')[0]); + }); + }); + }); + }); + }); + }); + + describe('using dropdown-append-to', function() { + var initialPage, container; + function dropdown() { - return $compile('
  • ')($rootScope); + return $compile('
  • ')($rootScope); } beforeEach(function() { + $document.find('body').append(angular.element('')); + + $rootScope.appendTo = container = $document.find('#dropdown-container'); + element = dropdown(); $document.find('body').append(element); }); afterEach(function() { - element.remove(); + // Cleanup the extra elements we appended + $document.find('#dropdown-container').remove(); }); - it('adds the menu to the body', function() { - expect($document.find('#dropdown-menu').parent()[0]).toBe($document.find('body')[0]); + it('does not add the menu to the container', function() { + expect($document.find('#dropdown-menu').parent()[0]).not.toBe(container[0]); + }); + it('does not add open class on container', function() { + expect(container).not.toHaveClass('uib-dropdown-open'); }); - it('focuses the dropdown element on close', function() { - var toggle = element.find('[uib-dropdown-toggle]'); - var menu = $document.find('#dropdown-menu a'); - toggle.trigger('click'); - menu.focus(); - - menu.trigger('click'); + describe('when toggled open', function() { + var toggle; + beforeEach(function() { + toggle = element.find('[uib-dropdown-toggle]'); + toggle.trigger('click'); + }); + it('adds the menu to the container', function() { + expect($document.find('#dropdown-menu').parent()[0]).toBe(container[0]); + }); + it('adds open class on container', function() { + expect(container).toHaveClass('uib-dropdown-open'); + }); - expect(document.activeElement).toBe(toggle[0]); - }); + describe('when toggled closed', function() { + beforeEach(function() { + toggle.trigger('click'); + }); + it('removes the menu from the container', function() { + expect($document.find('#dropdown-menu').parent()[0]).not.toBe($document.find('body')[0]); + }); + it('removes open class from container', function() { + expect(container).not.toHaveClass('uib-dropdown-open'); + }); + }); - it('removes the menu when the dropdown is removed', function() { - element.remove(); - $rootScope.$digest(); - expect($document.find('#dropdown-menu').length).toEqual(0); + describe('when closed by clicking on menu', function() { + var menu; + beforeEach(function() { + menu = $document.find('#dropdown-menu a'); + menu.focus(); + menu.trigger('click'); + }); + it('focuses the dropdown element on close', function() { + expect(document.activeElement).toBe(toggle[0]); + }); + it('removes the menu from the container', function() { + expect($document.find('#dropdown-menu').parent()[0]).not.toBe($document.find('body')[0]); + }); + it('removes open class from container', function() { + expect(container).not.toHaveClass('uib-dropdown-open'); + }); + }); + describe('when the dropdown is removed', function() { + beforeEach(function() { + element.remove(); + $rootScope.$digest(); + }); + it('removes the menu from the container', function() { + expect($document.find('#dropdown-menu').parent()[0]).not.toBe($document.find('body')[0]); + }); + }); }); }); - describe('using dropdown-append-to', function() { - var initialPage; - + describe('using dropdown-append-to with two dropdowns', function() { function dropdown() { - return $compile('
  • ')($rootScope); + return $compile('')($rootScope); } beforeEach(function() { $document.find('body').append(angular.element('')); $rootScope.appendTo = $document.find('#dropdown-container'); + $rootScope.log = jasmine.createSpy('log'); element = dropdown(); $document.find('body').append(element); @@ -270,35 +507,14 @@ describe('uib-dropdown', function() { $document.find('#dropdown-container').remove(); }); - it('appends to container', function() { - expect($document.find('#dropdown-menu').parent()[0].id).toBe('dropdown-container'); - }); - - it('toggles open class on container', function() { + it('should keep the class when toggling from one dropdown to another with the same container', function() { var container = $document.find('#dropdown-container'); expect(container).not.toHaveClass('uib-dropdown-open'); - element.find('[uib-dropdown-toggle]').click(); + element.find('.dropdown1 [uib-dropdown-toggle]').click(); + expect(container).toHaveClass('uib-dropdown-open'); + element.find('.dropdown2 [uib-dropdown-toggle]').click(); expect(container).toHaveClass('uib-dropdown-open'); - element.find('[uib-dropdown-toggle]').click(); - expect(container).not.toHaveClass('uib-dropdown-open'); - }); - - it('focuses the dropdown element on close', function() { - var toggle = element.find('[uib-dropdown-toggle]'); - var menu = $document.find('#dropdown-menu a'); - toggle.trigger('click'); - menu.focus(); - - menu.trigger('click'); - - expect(document.activeElement).toBe(toggle[0]); - }); - - it('removes the menu when the dropdown is removed', function() { - element.remove(); - $rootScope.$digest(); - expect($document.find('#dropdown-menu').length).toEqual(0); }); }); @@ -306,7 +522,7 @@ describe('uib-dropdown', function() { describe('with uib-dropdown-toggle', function() { beforeEach(function() { $rootScope.isopen = true; - element = $compile('
    • Hello
  • ')($rootScope); + element = $compile('
    • Hello
  • ')($rootScope); $rootScope.$digest(); }); @@ -340,7 +556,7 @@ describe('uib-dropdown', function() { describe('without uib-dropdown-toggle', function() { beforeEach(function() { $rootScope.isopen = true; - element = $compile('
    • Hello
  • ')($rootScope); + element = $compile('
    • Hello
  • ')($rootScope); $rootScope.$digest(); }); @@ -361,7 +577,7 @@ describe('uib-dropdown', function() { beforeEach(function() { $rootScope.toggleHandler = jasmine.createSpy('toggleHandler'); $rootScope.isopen = false; - element = $compile('
    • Hello
  • ')($rootScope); + element = $compile('
    • Hello
  • ')($rootScope); $rootScope.$digest(); }); @@ -388,7 +604,7 @@ describe('uib-dropdown', function() { beforeEach(function() { $rootScope.toggleHandler = jasmine.createSpy('toggleHandler'); $rootScope.isopen = true; - element = $compile('
    • Hello
  • ')($rootScope); + element = $compile('
    • Hello
  • ')($rootScope); $rootScope.$digest(); }); @@ -416,7 +632,7 @@ describe('uib-dropdown', function() { describe('without is-open', function() { beforeEach(function() { $rootScope.toggleHandler = jasmine.createSpy('toggleHandler'); - element = $compile('
    • Hello
  • ')($rootScope); + element = $compile('
    • Hello
  • ')($rootScope); $rootScope.$digest(); }); @@ -444,7 +660,7 @@ describe('uib-dropdown', function() { function dropdown(autoClose) { return $compile('
  • ')($rootScope); + '>')($rootScope); } describe('always', function() { @@ -476,7 +692,7 @@ describe('uib-dropdown', function() { it('control with is-open', function() { $rootScope.isopen = true; - element = $compile('
    • Hello
  • ')($rootScope); + element = $compile('
    • Hello
  • ')($rootScope); $rootScope.$digest(); expect(element).toHaveClass(dropdownConfig.openClass); @@ -499,9 +715,10 @@ describe('uib-dropdown', function() { it('should close anyway if esc is pressed', function() { element = dropdown('disabled'); + var dropdownMenu = element.find('[uib-dropdown-menu]'); $document.find('body').append(element); clickDropdownToggle(); - triggerKeyDown(element, 27); + triggerKeyDown(dropdownMenu, 27); expect(element).not.toHaveClass(dropdownConfig.openClass); expect(element.find('a')).toHaveFocus(); }); @@ -546,7 +763,10 @@ describe('uib-dropdown', function() { describe('using keyboard-nav', function() { function dropdown() { - return $compile('
  • ')($rootScope); + return $compile('
  • ')($rootScope); + } + function getFocusedElement() { + return angular.element(document.activeElement); } beforeEach(function() { element = dropdown(); @@ -555,7 +775,7 @@ describe('uib-dropdown', function() { it('should focus first list element when down arrow pressed', function() { $document.find('body').append(element); clickDropdownToggle(); - triggerKeyDown(element, 40); + triggerKeyDown(getFocusedElement(), 40); expect(element).toHaveClass(dropdownConfig.openClass); var optionEl = element.find('ul').eq(0).find('a').eq(0); @@ -564,7 +784,7 @@ describe('uib-dropdown', function() { it('should not focus first list element when down arrow pressed if closed', function() { $document.find('body').append(element); - triggerKeyDown(element, 40); + triggerKeyDown(getFocusedElement(), 40); expect(element).not.toHaveClass(dropdownConfig.openClass); var focusEl = element.find('ul').eq(0).find('a').eq(0); @@ -574,8 +794,8 @@ describe('uib-dropdown', function() { it('should focus second list element when down arrow pressed twice', function() { $document.find('body').append(element); clickDropdownToggle(); - triggerKeyDown(element, 40); - triggerKeyDown(element, 40); + triggerKeyDown(getFocusedElement(), 40); + triggerKeyDown(getFocusedElement(), 40); expect(element).toHaveClass(dropdownConfig.openClass); var focusEl = element.find('ul').eq(0).find('a').eq(1); @@ -587,7 +807,7 @@ describe('uib-dropdown', function() { clickDropdownToggle(); expect(element).toHaveClass(dropdownConfig.openClass); - triggerKeyDown(element, 38); + triggerKeyDown(getFocusedElement(), 38); var focusEl = element.find('ul').eq(0).find('a').eq(0); expect(focusEl).not.toHaveFocus(); }); @@ -595,7 +815,7 @@ describe('uib-dropdown', function() { it('should focus last list element when up arrow pressed after dropdown toggled', function() { $document.find('body').append(element); clickDropdownToggle(); - triggerKeyDown(element, 38); + triggerKeyDown(getFocusedElement(), 38); expect(element).toHaveClass(dropdownConfig.openClass); var focusEl = element.find('ul').eq(0).find('a').eq(1); @@ -605,7 +825,7 @@ describe('uib-dropdown', function() { it('should not change focus when other keys are pressed', function() { $document.find('body').append(element); clickDropdownToggle(); - triggerKeyDown(element, 37); + triggerKeyDown(getFocusedElement(), 37); expect(element).toHaveClass(dropdownConfig.openClass); var focusEl = element.find('ul').eq(0).find('a'); @@ -616,10 +836,10 @@ describe('uib-dropdown', function() { it('should focus first list element when down arrow pressed 2x and up pressed 1x', function() { $document.find('body').append(element); clickDropdownToggle(); - triggerKeyDown(element, 40); - triggerKeyDown(element, 40); + triggerKeyDown(getFocusedElement(), 40); + triggerKeyDown(getFocusedElement(), 40); - triggerKeyDown(element, 38); + triggerKeyDown(getFocusedElement(), 38); expect(element).toHaveClass(dropdownConfig.openClass); var focusEl = element.find('ul').eq(0).find('a').eq(0); @@ -629,8 +849,8 @@ describe('uib-dropdown', function() { it('should stay focused on final list element if down pressed at list end', function() { $document.find('body').append(element); clickDropdownToggle(); - triggerKeyDown(element, 40); - triggerKeyDown(element, 40); + triggerKeyDown(getFocusedElement(), 40); + triggerKeyDown(getFocusedElement(), 40); expect(element).toHaveClass(dropdownConfig.openClass); var focusEl = element.find('ul').eq(0).find('a').eq(1); @@ -645,19 +865,19 @@ describe('uib-dropdown', function() { $document.find('body').append(element); clickDropdownToggle(); - triggerKeyDown(element, 40); + triggerKeyDown(getFocusedElement(), 40); expect(element).toHaveClass(dropdownConfig.openClass); var focusEl = element.find('ul').eq(0).find('a').eq(0); expect(focusEl).toHaveFocus(); - triggerKeyDown(element, 27); + triggerKeyDown(getFocusedElement(), 27); expect(element).not.toHaveClass(dropdownConfig.openClass); }); describe('with dropdown-append-to-body', function() { function dropdown() { - return $compile('
  • ')($rootScope); + return $compile('
  • foo
  • ')($rootScope); } beforeEach(function() { @@ -665,24 +885,25 @@ describe('uib-dropdown', function() { }); it('should focus first list element when down arrow pressed', function() { + $document.find('body').append(element); clickDropdownToggle(); - triggerKeyDown(element, 40); - var dropdownMenu = $document.find('#dropdown-menu'); + triggerKeyDown(getFocusedElement(), 40); + expect(dropdownMenu.parent()).toHaveClass(dropdownConfig.appendToOpenClass); var focusEl = $document.find('ul').eq(0).find('a'); expect(focusEl).toHaveFocus(); }); it('should focus second list element when down arrow pressed twice', function() { + $document.find('body').append(element); clickDropdownToggle(); - triggerKeyDown(element, 40); - triggerKeyDown(element, 40); - triggerKeyDown(element, 40); - var dropdownMenu = $document.find('#dropdown-menu'); + triggerKeyDown(getFocusedElement(), 40); + triggerKeyDown(getFocusedElement(), 40); + triggerKeyDown(getFocusedElement(), 40); expect(dropdownMenu.parent()).toHaveClass(dropdownConfig.appendToOpenClass); var elem1 = $document.find('ul'); @@ -692,4 +913,34 @@ describe('uib-dropdown', function() { }); }); }); + + // issue #5942 + describe('using dropdown-append-to-body with dropdown-menu-right class', function() { + function dropdown() { + return $compile('
  • Toggle menu
  • ')($rootScope); + } + + beforeEach(function() { + element = dropdown(); + $document.find('body').append(element); + + var menu = $document.find('#dropdown-menu'); + menu.css('position', 'absolute'); + }); + + afterEach(function() { + element.remove(); + }); + + it('should align the menu correctly when the body has no vertical scrollbar', function() { + var toggle = element.find('[uib-dropdown-toggle]'); + var menu = $document.find('#dropdown-menu'); + toggle.trigger('click'); + + // Get the offsets of the rightmost position of both the toggle and the menu (offset from the left of the window) + var toggleRight = Math.round(toggle.offset().left + toggle.outerWidth()); + var menuRight = Math.round(menu.offset().left + menu.outerWidth()); + expect(menuRight).toBe(toggleRight); + }); + }); }); diff --git a/src/modal/docs/demo.html b/src/modal/docs/demo.html index ac96e0780b..616b1a0c03 100644 --- a/src/modal/docs/demo.html +++ b/src/modal/docs/demo.html @@ -1,25 +1,44 @@ -
    + \ No newline at end of file + + + + + + + +
    Selection from a modal: {{ $ctrl.selected }}
    + +
    diff --git a/src/modal/docs/demo.js b/src/modal/docs/demo.js index 1055775653..53b335dad1 100644 --- a/src/modal/docs/demo.js +++ b/src/modal/docs/demo.js @@ -1,51 +1,126 @@ -angular.module('ui.bootstrap.demo').controller('ModalDemoCtrl', function ($scope, $uibModal, $log) { +angular.module('ui.bootstrap.demo').controller('ModalDemoCtrl', function ($uibModal, $log, $document) { + var $ctrl = this; + $ctrl.items = ['item1', 'item2', 'item3']; - $scope.items = ['item1', 'item2', 'item3']; - - $scope.animationsEnabled = true; - - $scope.open = function (size) { + $ctrl.animationsEnabled = true; + $ctrl.open = function (size, parentSelector) { + var parentElem = parentSelector ? + angular.element($document[0].querySelector('.modal-demo ' + parentSelector)) : undefined; var modalInstance = $uibModal.open({ - animation: $scope.animationsEnabled, + animation: $ctrl.animationsEnabled, + ariaLabelledBy: 'modal-title', + ariaDescribedBy: 'modal-body', templateUrl: 'myModalContent.html', controller: 'ModalInstanceCtrl', + controllerAs: '$ctrl', size: size, + appendTo: parentElem, resolve: { items: function () { - return $scope.items; + return $ctrl.items; } } }); modalInstance.result.then(function (selectedItem) { - $scope.selected = selectedItem; + $ctrl.selected = selectedItem; }, function () { $log.info('Modal dismissed at: ' + new Date()); }); }; - $scope.toggleAnimation = function () { - $scope.animationsEnabled = !$scope.animationsEnabled; + $ctrl.openComponentModal = function () { + var modalInstance = $uibModal.open({ + animation: $ctrl.animationsEnabled, + component: 'modalComponent', + resolve: { + items: function () { + return $ctrl.items; + } + } + }); + + modalInstance.result.then(function (selectedItem) { + $ctrl.selected = selectedItem; + }, function () { + $log.info('modal-component dismissed at: ' + new Date()); + }); }; + $ctrl.openMultipleModals = function () { + $uibModal.open({ + animation: $ctrl.animationsEnabled, + ariaLabelledBy: 'modal-title-bottom', + ariaDescribedBy: 'modal-body-bottom', + templateUrl: 'stackedModal.html', + size: 'sm', + controller: function($scope) { + $scope.name = 'bottom'; + } + }); + + $uibModal.open({ + animation: $ctrl.animationsEnabled, + ariaLabelledBy: 'modal-title-top', + ariaDescribedBy: 'modal-body-top', + templateUrl: 'stackedModal.html', + size: 'sm', + controller: function($scope) { + $scope.name = 'top'; + } + }); + }; + + $ctrl.toggleAnimation = function () { + $ctrl.animationsEnabled = !$ctrl.animationsEnabled; + }; }); // Please note that $uibModalInstance represents a modal window (instance) dependency. // It is not the same as the $uibModal service used above. -angular.module('ui.bootstrap.demo').controller('ModalInstanceCtrl', function ($scope, $uibModalInstance, items) { - - $scope.items = items; - $scope.selected = { - item: $scope.items[0] +angular.module('ui.bootstrap.demo').controller('ModalInstanceCtrl', function ($uibModalInstance, items) { + var $ctrl = this; + $ctrl.items = items; + $ctrl.selected = { + item: $ctrl.items[0] }; - $scope.ok = function () { - $uibModalInstance.close($scope.selected.item); + $ctrl.ok = function () { + $uibModalInstance.close($ctrl.selected.item); }; - $scope.cancel = function () { + $ctrl.cancel = function () { $uibModalInstance.dismiss('cancel'); }; }); + +// Please note that the close and dismiss bindings are from $uibModalInstance. + +angular.module('ui.bootstrap.demo').component('modalComponent', { + templateUrl: 'myModalContent.html', + bindings: { + resolve: '<', + close: '&', + dismiss: '&' + }, + controller: function () { + var $ctrl = this; + + $ctrl.$onInit = function () { + $ctrl.items = $ctrl.resolve.items; + $ctrl.selected = { + item: $ctrl.items[0] + }; + }; + + $ctrl.ok = function () { + $ctrl.close({$value: $ctrl.selected.item}); + }; + + $ctrl.cancel = function () { + $ctrl.dismiss({$value: 'cancel'}); + }; + } +}); diff --git a/src/modal/docs/readme.md b/src/modal/docs/readme.md index 50f3fc9105..66b6f36803 100644 --- a/src/modal/docs/readme.md +++ b/src/modal/docs/readme.md @@ -1,5 +1,5 @@ `$uibModal` is a service to create modal windows. -Creating modals is straightforward: create a template, a controller and reference them when using `$uibModal`. +Creating modals is straightforward: create a template and controller, and reference them when using `$uibModal`. The `$uibModal` service has only one method: `open(options)`. @@ -10,15 +10,23 @@ The `$uibModal` service has only one method: `open(options)`. * `animation` _(Type: `boolean`, Default: `true`)_ - Set to false to disable animations on new modal/backdrop. Does not toggle animations for modals/backdrops that are already displayed. - -* `appendTo` + +* `appendTo` _(Type: `angular.element`, Default: `body`: Example: `$document.find('aside').eq(0)`)_ - Appends the modal to a specific element. - + +* `ariaDescribedBy` + _(Type: `string`, `my-modal-description`)_ - + Sets the [`aria-describedby`](https://www.w3.org/TR/wai-aria/states_and_properties#aria-describedby) property on the modal. The value should be an id (without the leading `#`) pointing to the element that describes your modal. Typically, this will be the text on your modal, but does not include something the user would interact with, like buttons or a form. Omitting this option will not impact sighted users but will weaken your accessibility support. + +* `ariaLabelledBy` + _(Type: `string`, `my-modal-title`)_ - + Sets the [`aria-labelledby`](https://www.w3.org/TR/wai-aria/states_and_properties#aria-labelledby) property on the modal. The value should be an id (without the leading `#`) pointing to the element that labels your modal. Typically, this will be a header element. Omitting this option will not impact sighted users but will weaken your accessibility support. + * `backdrop` _(Type: `boolean|string`, Default: `true`)_ - Controls presence of a backdrop. Allowed values: `true` (default), `false` (no backdrop), `'static'` (disables modal closing by click on the backdrop). - + * `backdropClass` _(Type: `string`)_ - Additional CSS class(es) to be added to a modal backdrop template. @@ -27,15 +35,29 @@ The `$uibModal` service has only one method: `open(options)`. _(Type: `boolean`, Default: `false`)_ - When used with `controllerAs` & set to `true`, it will bind the $scope properties onto the controller. +* `component` + _(Type: `string`, Example: `myComponent`)_ - + A string reference to the component to be rendered that is registered with Angular's compiler. If using a directive, the directive must have `restrict: 'E'` and a template or templateUrl set. + + It supports these bindings: + + * `close` - A method that can be used to close a modal, passing a result. The result must be passed in this format: `{$value: myResult}` + + * `dismiss` - A method that can be used to dismiss a modal, passing a result. The result must be passed in this format: `{$value: myRejectedResult}` + + * `modalInstance` - The modal instance. This is the same `$uibModalInstance` injectable found when using `controller`. + + * `resolve` - An object of the modal resolve values. See [UI Router resolves](#ui-router-resolves) for details. + * `controller` _(Type: `function|string|array`, Example: `MyModalController`)_ - A controller for the modal instance, either a controller name as a string, or an inline controller function, optionally wrapped in array notation for dependency injection. Allows the controller-as syntax. Has a special `$uibModalInstance` injectable to access the modal instance. * `controllerAs` - _(Type: `string`, Example: `ctrl`)_ - + _(Type: `string`, Example: `ctrl`)_ - An alternative to the controller-as syntax. Requires the `controller` option to be provided as well. -* `keyboard` - +* `keyboard` - _(Type: `boolean`, Default: `true`)_ - Indicates whether the dialog should be closable by hitting the ESC key. @@ -76,7 +98,7 @@ The `$uibModal` service has only one method: `open(options)`. CSS class(es) to be added to the top modal window. Global defaults may be set for `$uibModal` via `$uibModalProvider.options`. - + #### return The `open` method returns a modal instance, an object with the following properties: @@ -103,8 +125,8 @@ The `open` method returns a modal instance, an object with the following propert * `rendered` _(Type: `promise`)_ - - Is resolved when a modal is rendered. - + Is resolved when a modal is rendered. + --- The scope associated with modal's content is augmented with: @@ -125,9 +147,9 @@ Also, when using `bindToController`, you can define an `$onInit` method in the c Events fired: -* `$uibUnscheduledDestruction` - +* `$uibUnscheduledDestruction` - This event is fired if the $scope is destroyed via unexpected mechanism, such as it being passed in the modal options and a $route/$state transition occurs. The modal will also be dismissed. - + * `modal.closing` - This event is broadcast to the modal scope before the modal closes. If the listener calls preventDefault() on the event, then the modal will remain open. Also, the `$close` and `$dismiss` methods returns true if the event was executed. This event also includes a parameter for the result/reason and a boolean that indicates whether the modal is being closed (true) or dismissed. @@ -136,4 +158,4 @@ Events fired: If one wants to have the modal resolve using [UI Router's](https://github.com/angular-ui/ui-router) pre-1.0 resolve mechanism, one can call `$uibResolve.setResolver('$resolve')` in the configuration phase of the application. One can also provide a custom resolver as well, as long as the signature conforms to UI Router's [$resolve](http://angular-ui.github.io/ui-router/site/#/api/ui.router.util.$resolve). -When the modal is opened with a controller, a `$resolve` object is exposed on the template with the resolved values from the resolve object. +When the modal is opened with a controller, a `$resolve` object is exposed on the template with the resolved values from the resolve object. If using the component option, see details on how to access this object in component section of the modal documentation. diff --git a/src/modal/index-nocss.js b/src/modal/index-nocss.js index 10473fae44..d3f8a94b7d 100644 --- a/src/modal/index-nocss.js +++ b/src/modal/index-nocss.js @@ -1,11 +1,11 @@ +require('../multiMap'); require('../position/index-nocss.js'); require('../stackedMap'); -require('../../template/modal/backdrop.html.js'); require('../../template/modal/window.html.js'); require('./modal'); var MODULE_NAME = 'ui.bootstrap.module.modal'; -angular.module(MODULE_NAME, ['ui.bootstrap.modal', 'uib/template/modal/backdrop.html', 'uib/template/modal/window.html']); +angular.module(MODULE_NAME, ['ui.bootstrap.modal', 'uib/template/modal/window.html']); module.exports = MODULE_NAME; diff --git a/src/modal/modal.js b/src/modal/modal.js index 356299da8b..679ff188e8 100644 --- a/src/modal/modal.js +++ b/src/modal/modal.js @@ -1,59 +1,4 @@ -angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.position']) -/** - * A helper, internal data structure that stores all references attached to key - */ - .factory('$$multiMap', function() { - return { - createNew: function() { - var map = {}; - - return { - entries: function() { - return Object.keys(map).map(function(key) { - return { - key: key, - value: map[key] - }; - }); - }, - get: function(key) { - return map[key]; - }, - hasKey: function(key) { - return !!map[key]; - }, - keys: function() { - return Object.keys(map); - }, - put: function(key, value) { - if (!map[key]) { - map[key] = []; - } - - map[key].push(value); - }, - remove: function(key, value) { - var values = map[key]; - - if (!values) { - return; - } - - var idx = values.indexOf(value); - - if (idx !== -1) { - values.splice(idx, 1); - } - - if (!values.length) { - delete map[key]; - } - } - }; - } - }; - }) - +angular.module('ui.bootstrap.modal', ['ui.bootstrap.multiMap', 'ui.bootstrap.stackedMap', 'ui.bootstrap.position']) /** * Pluggable resolve mechanism for the modal resolve resolution * Supports UI Router's $resolve service @@ -106,8 +51,7 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p .directive('uibModalBackdrop', ['$animate', '$injector', '$uibModalStack', function($animate, $injector, $modalStack) { return { - replace: true, - templateUrl: 'uib/template/modal/backdrop.html', + restrict: 'A', compile: function(tElement, tAttrs) { tElement.addClass(tAttrs.backdropClass); return linkFn; @@ -136,13 +80,12 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p scope: { index: '@' }, - replace: true, + restrict: 'A', transclude: true, templateUrl: function(tElement, tAttrs) { return tAttrs.templateUrl || 'uib/template/modal/window.html'; }, link: function(scope, element, attrs) { - element.addClass(attrs.windowClass || ''); element.addClass(attrs.windowTopClass || ''); scope.size = attrs.size; @@ -165,14 +108,11 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p // {@link Attribute#$observe} on it. For more details please see {@link TableColumnResize}. scope.$isRendered = true; - // Deferred object that will be resolved when this modal is render. + // Deferred object that will be resolved when this modal is rendered. var modalRenderDeferObj = $q.defer(); - // Observe function will be called on next digest cycle after compilation, ensuring that the DOM is ready. - // In order to use this way of finding whether DOM is ready, we need to observe a scope property used in modal's template. - attrs.$observe('modalRender', function(value) { - if (value === 'true') { - modalRenderDeferObj.resolve(); - } + // Resolve render promise post-digest + scope.$$postDigest(function() { + modalRenderDeferObj.resolve(); }); modalRenderDeferObj.promise.then(function() { @@ -201,7 +141,7 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p /** * If something within the freshly-opened modal already has focus (perhaps via a - * directive that causes focus). then no need to try and focus anything. + * directive that causes focus) then there's no need to try to focus anything. */ if (!($document[0].activeElement && element[0].contains($document[0].activeElement))) { var inputWithAutofocus = element[0].querySelector('[autofocus]'); @@ -235,16 +175,16 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p }; }) - .directive('uibModalTransclude', function() { + .directive('uibModalTransclude', ['$animate', function($animate) { return { link: function(scope, element, attrs, controller, transclude) { transclude(scope.$parent, function(clone) { element.empty(); - element.append(clone); + $animate.enter(clone, element); }); } }; - }) + }]) .factory('$uibModalStack', ['$animate', '$animateCss', '$document', '$compile', '$rootScope', '$q', '$$multiMap', '$$stackedMap', '$uibPosition', @@ -259,12 +199,22 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p }; var topModalIndex = 0; var previousTopOpenedModal = null; + var ARIA_HIDDEN_ATTRIBUTE_NAME = 'data-bootstrap-modal-aria-hidden-count'; //Modal focus behavior - var tabableSelector = 'a[href], area[href], input:not([disabled]), ' + - 'button:not([disabled]),select:not([disabled]), textarea:not([disabled]), ' + - 'iframe, object, embed, *[tabindex], *[contenteditable=true]'; + var tabbableSelector = 'a[href], area[href], input:not([disabled]):not([tabindex=\'-1\']), ' + + 'button:not([disabled]):not([tabindex=\'-1\']),select:not([disabled]):not([tabindex=\'-1\']), textarea:not([disabled]):not([tabindex=\'-1\']), ' + + 'iframe, object, embed, *[tabindex]:not([tabindex=\'-1\']), *[contenteditable=true]'; var scrollbarPadding; + var SNAKE_CASE_REGEXP = /[A-Z]/g; + + // TODO: extract into common dependency with tooltip + function snake_case(name) { + var separator = '-'; + return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) { + return (pos ? separator : '') + letter.toLowerCase(); + }); + } function isVisible(element) { return !!(element.offsetWidth || @@ -380,6 +330,10 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p afterAnimating.done = true; $animate.leave(domEl).then(function() { + if (done) { + done(); + } + domEl.remove(); if (closedDeferred) { closedDeferred.resolve(); @@ -387,9 +341,6 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p }); scope.$destroy(); - if (done) { - done(); - } } } @@ -468,66 +419,152 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p var appendToElement = modal.appendTo, currBackdropIndex = backdropIndex(); - if (!appendToElement.length) { - throw new Error('appendTo element not found. Make sure that the element passed is in DOM.'); - } - if (currBackdropIndex >= 0 && !backdropDomEl) { backdropScope = $rootScope.$new(true); backdropScope.modalOptions = modal; backdropScope.index = currBackdropIndex; backdropDomEl = angular.element('
    '); - backdropDomEl.attr('backdrop-class', modal.backdropClass); + backdropDomEl.attr({ + 'class': 'modal-backdrop', + 'ng-style': '{\'z-index\': 1040 + (index && 1 || 0) + index*10}', + 'uib-modal-animation-class': 'fade', + 'modal-in-class': 'in' + }); + if (modal.backdropClass) { + backdropDomEl.addClass(modal.backdropClass); + } + if (modal.animation) { backdropDomEl.attr('modal-animation', 'true'); } $compile(backdropDomEl)(backdropScope); $animate.enter(backdropDomEl, appendToElement); - scrollbarPadding = $uibPosition.scrollbarPadding(appendToElement); - if (scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) { - appendToElement.css({paddingRight: scrollbarPadding.right + 'px'}); + if ($uibPosition.isScrollable(appendToElement)) { + scrollbarPadding = $uibPosition.scrollbarPadding(appendToElement); + if (scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) { + appendToElement.css({paddingRight: scrollbarPadding.right + 'px'}); + } } } + var content; + if (modal.component) { + content = document.createElement(snake_case(modal.component.name)); + content = angular.element(content); + content.attr({ + resolve: '$resolve', + 'modal-instance': '$uibModalInstance', + close: '$close($value)', + dismiss: '$dismiss($value)' + }); + } else { + content = modal.content; + } + // Set the top modal index based on the index of the previous top modal topModalIndex = previousTopOpenedModal ? parseInt(previousTopOpenedModal.value.modalDomEl.attr('index'), 10) + 1 : 0; var angularDomEl = angular.element('
    '); angularDomEl.attr({ + 'class': 'modal', 'template-url': modal.windowTemplateUrl, - 'window-class': modal.windowClass, 'window-top-class': modal.windowTopClass, + 'role': 'dialog', + 'aria-labelledby': modal.ariaLabelledBy, + 'aria-describedby': modal.ariaDescribedBy, 'size': modal.size, 'index': topModalIndex, - 'animate': 'animate' - }).html(modal.content); + 'animate': 'animate', + 'ng-style': '{\'z-index\': 1050 + $$topModalIndex*10, display: \'block\'}', + 'tabindex': -1, + 'uib-modal-animation-class': 'fade', + 'modal-in-class': 'in' + }).append(content); + if (modal.windowClass) { + angularDomEl.addClass(modal.windowClass); + } + if (modal.animation) { angularDomEl.attr('modal-animation', 'true'); } appendToElement.addClass(modalBodyClass); + if (modal.scope) { + // we need to explicitly add the modal index to the modal scope + // because it is needed by ngStyle to compute the zIndex property. + modal.scope.$$topModalIndex = topModalIndex; + } $animate.enter($compile(angularDomEl)(modal.scope), appendToElement); openedWindows.top().value.modalDomEl = angularDomEl; openedWindows.top().value.modalOpener = modalOpener; + + applyAriaHidden(angularDomEl); + + function applyAriaHidden(el) { + if (!el || el[0].tagName === 'BODY') { + return; + } + + getSiblings(el).forEach(function(sibling) { + var elemIsAlreadyHidden = sibling.getAttribute('aria-hidden') === 'true', + ariaHiddenCount = parseInt(sibling.getAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME), 10); + + if (!ariaHiddenCount) { + ariaHiddenCount = elemIsAlreadyHidden ? 1 : 0; + } + + sibling.setAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME, ariaHiddenCount + 1); + sibling.setAttribute('aria-hidden', 'true'); + }); + + return applyAriaHidden(el.parent()); + + function getSiblings(el) { + var children = el.parent() ? el.parent().children() : []; + + return Array.prototype.filter.call(children, function(child) { + return child !== el[0]; + }); + } + } }; function broadcastClosing(modalWindow, resultOrReason, closing) { return !modalWindow.value.modalScope.$broadcast('modal.closing', resultOrReason, closing).defaultPrevented; } + function unhideBackgroundElements() { + Array.prototype.forEach.call( + document.querySelectorAll('[' + ARIA_HIDDEN_ATTRIBUTE_NAME + ']'), + function(hiddenEl) { + var ariaHiddenCount = parseInt(hiddenEl.getAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME), 10), + newHiddenCount = ariaHiddenCount - 1; + hiddenEl.setAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME, newHiddenCount); + + if (!newHiddenCount) { + hiddenEl.removeAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME); + hiddenEl.removeAttribute('aria-hidden'); + } + } + ); + } + $modalStack.close = function(modalInstance, result) { var modalWindow = openedWindows.get(modalInstance); + unhideBackgroundElements(); if (modalWindow && broadcastClosing(modalWindow, result, true)) { modalWindow.value.modalScope.$$uibDestructionScheduled = true; modalWindow.value.deferred.resolve(result); removeModalWindow(modalInstance, modalWindow.value.modalOpener); return true; } + return !modalWindow; }; $modalStack.dismiss = function(modalInstance, reason) { var modalWindow = openedWindows.get(modalInstance); + unhideBackgroundElements(); if (modalWindow && broadcastClosing(modalWindow, reason, false)) { modalWindow.value.modalScope.$$uibDestructionScheduled = true; modalWindow.value.deferred.reject(reason); @@ -599,7 +636,7 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p if (modalWindow) { var modalDomE1 = modalWindow.value.modalDomEl; if (modalDomE1 && modalDomE1.length) { - var elements = modalDomE1[0].querySelectorAll(tabableSelector); + var elements = modalDomE1[0].querySelectorAll(tabbableSelector); return elements ? Array.prototype.filter.call(elements, function(element) { return isVisible(element); @@ -658,13 +695,22 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p modalOptions.resolve = modalOptions.resolve || {}; modalOptions.appendTo = modalOptions.appendTo || $document.find('body').eq(0); + if (!modalOptions.appendTo.length) { + throw new Error('appendTo element not found. Make sure that the element passed is in DOM.'); + } + //verify options - if (!modalOptions.template && !modalOptions.templateUrl) { - throw new Error('One of template or templateUrl options is required.'); + if (!modalOptions.component && !modalOptions.template && !modalOptions.templateUrl) { + throw new Error('One of component or template or templateUrl options is required.'); } - var templateAndResolvePromise = - $q.all([getTemplatePromise(modalOptions), $uibResolve.resolve(modalOptions.resolve, {}, null, null)]); + var templateAndResolvePromise; + if (modalOptions.component) { + templateAndResolvePromise = $q.when($uibResolve.resolve(modalOptions.resolve, {}, null, null)); + } else { + templateAndResolvePromise = + $q.all([getTemplatePromise(modalOptions), $uibResolve.resolve(modalOptions.resolve, {}, null, null)]); + } function resolveWithTemplate() { return templateAndResolvePromise; @@ -690,17 +736,34 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p } }); + var modal = { + scope: modalScope, + deferred: modalResultDeferred, + renderDeferred: modalRenderDeferred, + closedDeferred: modalClosedDeferred, + animation: modalOptions.animation, + backdrop: modalOptions.backdrop, + keyboard: modalOptions.keyboard, + backdropClass: modalOptions.backdropClass, + windowTopClass: modalOptions.windowTopClass, + windowClass: modalOptions.windowClass, + windowTemplateUrl: modalOptions.windowTemplateUrl, + ariaLabelledBy: modalOptions.ariaLabelledBy, + ariaDescribedBy: modalOptions.ariaDescribedBy, + size: modalOptions.size, + openedClass: modalOptions.openedClass, + appendTo: modalOptions.appendTo + }; + + var component = {}; var ctrlInstance, ctrlInstantiate, ctrlLocals = {}; - //controllers - if (modalOptions.controller) { - ctrlLocals.$scope = modalScope; - ctrlLocals.$scope.$resolve = {}; - ctrlLocals.$uibModalInstance = modalInstance; - angular.forEach(tplAndVars[1], function(value, key) { - ctrlLocals[key] = value; - ctrlLocals.$scope.$resolve[key] = value; - }); + if (modalOptions.component) { + constructLocals(component, false, true, false); + component.name = modalOptions.component; + modal.component = component; + } else if (modalOptions.controller) { + constructLocals(ctrlLocals, true, false, true); // the third param will make the controller instantiate later,private api // @see https://github.com/angular/angular.js/blob/master/src/ng/controller.js#L126 @@ -721,25 +784,31 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p } } - $modalStack.open(modalInstance, { - scope: modalScope, - deferred: modalResultDeferred, - renderDeferred: modalRenderDeferred, - closedDeferred: modalClosedDeferred, - content: tplAndVars[0], - animation: modalOptions.animation, - backdrop: modalOptions.backdrop, - keyboard: modalOptions.keyboard, - backdropClass: modalOptions.backdropClass, - windowTopClass: modalOptions.windowTopClass, - windowClass: modalOptions.windowClass, - windowTemplateUrl: modalOptions.windowTemplateUrl, - size: modalOptions.size, - openedClass: modalOptions.openedClass, - appendTo: modalOptions.appendTo - }); + if (!modalOptions.component) { + modal.content = tplAndVars[0]; + } + + $modalStack.open(modalInstance, modal); modalOpenedDeferred.resolve(true); + function constructLocals(obj, template, instanceOnScope, injectable) { + obj.$scope = modalScope; + obj.$scope.$resolve = {}; + if (instanceOnScope) { + obj.$scope.$uibModalInstance = modalInstance; + } else { + obj.$uibModalInstance = modalInstance; + } + + var resolves = template ? tplAndVars[1] : tplAndVars; + angular.forEach(resolves, function(value, key) { + if (injectable) { + obj[key] = value; + } + + obj.$scope.$resolve[key] = value; + }); + } }, function resolveError(reason) { modalOpenedDeferred.reject(reason); modalResultDeferred.reject(reason); diff --git a/src/modal/test/modal.spec.js b/src/modal/test/modal.spec.js index 1e43b037d2..4bf85243da 100644 --- a/src/modal/test/modal.spec.js +++ b/src/modal/test/modal.spec.js @@ -46,13 +46,68 @@ describe('$uibResolve', function() { }); }); +describe('uibModalTransclude', function() { + var uibModalTranscludeDDO, + $animate; + + beforeEach(module('ui.bootstrap.modal')); + beforeEach(module(function($provide) { + $animate = jasmine.createSpyObj('$animate', ['enter']); + $provide.value('$animate', $animate); + })); + + beforeEach(inject(function(uibModalTranscludeDirective) { + uibModalTranscludeDDO = uibModalTranscludeDirective[0]; + })); + + describe('when initialised', function() { + var scope, + element, + transcludeSpy, + transcludeFn; + + beforeEach(function() { + scope = { + $parent: 'parentScope' + }; + + element = jasmine.createSpyObj('containerElement', ['empty']); + transcludeSpy = jasmine.createSpy('transcludeSpy').and.callFake(function(scope, fn) { + transcludeFn = fn; + }); + + uibModalTranscludeDDO.link(scope, element, {}, {}, transcludeSpy); + }); + + it('should call the transclusion function', function() { + expect(transcludeSpy).toHaveBeenCalledWith(scope.$parent, jasmine.any(Function)); + }); + + describe('transclusion callback', function() { + var transcludedContent; + + beforeEach(function() { + transcludedContent = 'my transcluded content'; + transcludeFn(transcludedContent); + }); + + it('should empty the element', function() { + expect(element.empty).toHaveBeenCalledWith(); + }); + + it('should append the transcluded content', function() { + expect($animate.enter).toHaveBeenCalledWith(transcludedContent, element); + }); + }); + }); +}); + describe('$uibModal', function() { var $animate, $controllerProvider, $rootScope, $document, $compile, $templateCache, $timeout, $q; var $uibModal, $uibModalStack, $uibModalProvider; beforeEach(module('ngAnimateMock')); beforeEach(module('ui.bootstrap.modal')); - beforeEach(module('uib/template/modal/backdrop.html')); beforeEach(module('uib/template/modal/window.html')); beforeEach(module(function(_$controllerProvider_, _$uibModalProvider_, $compileProvider) { $controllerProvider = _$controllerProvider_; @@ -76,6 +131,16 @@ describe('$uibModal', function() { elem.focus(); } }; + }).component('fooBar', { + bindings: { + resolve: '<', + modalInstance: '<', + close: '&', + dismiss: '&' + }, + controller: angular.noop, + controllerAs: 'foobar', + template: '
    Foo Bar
    ' }); })); @@ -96,6 +161,7 @@ describe('$uibModal', function() { toBeResolvedWith: function(util, customEqualityTesters) { return { compare: function(promise, expected) { + var called = false; promise.then(function(result) { expect(result).toEqual(expected); @@ -104,10 +170,18 @@ describe('$uibModal', function() { } else { result.message = 'Expected "' + angular.mock.dump(result) + '" to be resolved with "' + expected + '".'; } + }, function(result) { + fail('Expected "' + angular.mock.dump(result) + '" to be resolved with "' + expected + '".'); + })['finally'](function() { + called = true; }); $rootScope.$digest(); + if (!called) { + fail('Expected "' + angular.mock.dump(result) + '" to be resolved with "' + expected + '".'); + } + return {pass: true}; } }; @@ -116,9 +190,10 @@ describe('$uibModal', function() { return { compare: function(promise, expected) { var result = {}; + var called = false; - promise.then(function() { - + promise.then(function(result) { + fail('Expected "' + angular.mock.dump(result) + '" to be rejected with "' + expected + '".'); }, function(result) { expect(result).toEqual(expected); @@ -127,10 +202,16 @@ describe('$uibModal', function() { } else { result.message = 'Expected "' + angular.mock.dump(result) + '" to be rejected with "' + expected + '".'; } + })['finally'](function() { + called = true; }); $rootScope.$digest(); + if (!called) { + fail('Expected "' + angular.mock.dump(result) + '" to be rejected with "' + expected + '".'); + } + return {pass: true}; } }; @@ -215,6 +296,8 @@ describe('$uibModal', function() { function open(modalOptions, noFlush, noDigest) { var modal = $uibModal.open(modalOptions); + modal.opened['catch'](angular.noop); + modal.result['catch'](angular.noop); if (!noDigest) { $rootScope.$digest(); @@ -445,7 +528,7 @@ describe('$uibModal', function() { var modal = open({template: '
    Content
    '}); $rootScope.$digest(); - expect(document.activeElement.tagName).toBe('DIV'); + expect(document.activeElement.className.split(' ')).toContain('modal'); expect($document).toHaveModalsOpen(1); triggerKeyDown($document, 27); @@ -575,7 +658,7 @@ describe('$uibModal', function() { it('should not focus on the element that has autofocus attribute when the modal is opened and something in the modal already has focus and the animations have finished', function() { function openAndCloseModalWithAutofocusElement() { - var modal = open({template: '
    '}); + var modal = open({template: '
    '}); $rootScope.$digest(); expect(angular.element('#auto-focus-element')).not.toHaveFocus(); expect(angular.element('#pre-focus-element')).toHaveFocus(); @@ -617,7 +700,7 @@ describe('$uibModal', function() { $rootScope.$digest(); $animate.flush(); - expect(document.activeElement.tagName).toBe('DIV'); + expect(document.activeElement.className.split(' ')).toContain('modal'); close(modal, 'closed ok'); @@ -807,6 +890,52 @@ describe('$uibModal', function() { initialPage.remove(); }); + + it('should change focus to next tabbable element when tab is pressed', function() { + var initialPage = angular.element('Outland link'); + angular.element(document.body).append(initialPage); + initialPage.focus(); + + open({ + template:'a' + + 'bc' + + '', + keyboard: false + }); + $rootScope.$digest(); + expect($document).toHaveModalsOpen(1); + + $('#tab-focus-link3').focus(); + expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link3'); + + triggerKeyDown(angular.element(document.activeElement), 9, false); + expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link1'); + + initialPage.remove(); + }); + + it('should change focus to previous tabbable element when shift+tab is pressed', function() { + var initialPage = angular.element('Outland link'); + angular.element(document.body).append(initialPage); + initialPage.focus(); + + open({ + template:'a' + + 'bc' + + '', + keyboard: false + }); + $rootScope.$digest(); + expect($document).toHaveModalsOpen(1); + + $('#tab-focus-link1').focus(); + expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link1'); + + triggerKeyDown(angular.element(document.activeElement), 9, true); + expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link3'); + + initialPage.remove(); + }); }); describe('default options can be changed in a provider', function() { @@ -829,16 +958,89 @@ describe('$uibModal', function() { }); }); - describe('option by option', function () { - describe('template and templateUrl', function () { - it('should throw an error if none of template and templateUrl are provided', function() { + describe('option by option', function() { + describe('component', function() { + function getModalComponent($document) { + return $document.find('body > div.modal > div.modal-dialog > div.modal-content foo-bar'); + } + + it('should use as modal content', function() { + open({ + component: 'fooBar' + }); + + var component = getModalComponent($document); + expect(component.html()).toBe('
    Foo Bar
    '); + }); + + it('should bind expected values', function() { + var modal = open({ + component: 'fooBar', + resolve: { + foo: function() { + return 'bar'; + } + } + }); + + var component = getModalComponent($document); + var componentScope = component.isolateScope(); + + expect(componentScope.foobar.resolve.foo).toBe('bar'); + expect(componentScope.foobar.modalInstance).toBe(modal); + expect(componentScope.foobar.close).toEqual(jasmine.any(Function)); + expect(componentScope.foobar.dismiss).toEqual(jasmine.any(Function)); + }); + + it('should close the modal', function() { + var modal = open({ + component: 'fooBar', + resolve: { + foo: function() { + return 'bar'; + } + } + }); + + var component = getModalComponent($document); + var componentScope = component.isolateScope(); + + componentScope.foobar.close({ + $value: 'baz' + }); + + expect(modal.result).toBeResolvedWith('baz'); + }); + + it('should dismiss the modal', function() { + var modal = open({ + component: 'fooBar', + resolve: { + foo: function() { + return 'bar'; + } + } + }); + + var component = getModalComponent($document); + var componentScope = component.isolateScope(); + + componentScope.foobar.dismiss({ + $value: 'baz' + }); + + expect(modal.result).toBeRejectedWith('baz'); + }); + }); + + describe('template and templateUrl', function() { + it('should throw an error if none of component, template and templateUrl are provided', function() { expect(function(){ var modal = open({}); - }).toThrow(new Error('One of template or templateUrl options is required.')); + }).toThrow(new Error('One of component or template or templateUrl options is required.')); }); it('should not fail if a templateUrl contains leading / trailing white spaces', function() { - $templateCache.put('whitespace.html', '
    Whitespaces
    '); open({templateUrl: 'whitespace.html'}); expect($document).toHaveModalOpenWithContent('Whitespaces', 'div'); @@ -1289,7 +1491,7 @@ describe('$uibModal', function() { expect(body).not.toHaveClass('modal-open'); }); - it('should remove the custom class on closing of modal', function() { + it('should remove the custom class on closing of modal after animations have completed', function() { var modal = open({ template: '
    dummy modal
    ', openedClass: 'foo' @@ -1297,7 +1499,13 @@ describe('$uibModal', function() { expect(body).toHaveClass('foo'); - close(modal); + close(modal, null, true); + expect(body).toHaveClass('foo'); + + $animate.flush(); + $rootScope.$digest(); + $animate.flush(); + $rootScope.$digest(); expect(body).not.toHaveClass('foo'); }); @@ -1358,6 +1566,28 @@ describe('$uibModal', function() { expect(body).not.toHaveClass('modal-open'); }); }); + + describe('ariaLabelledBy', function() { + it('should add the aria-labelledby property to the modal', function() { + open({ + template: '
    ', + ariaLabelledBy: 'modal-label' + }); + + expect($document.find('.modal').attr('aria-labelledby')).toEqual('modal-label'); + }); + }); + + describe('ariaDescribedBy', function() { + it('should add the aria-describedby property to the modal', function() { + open({ + template: '
    ', + ariaDescribedBy: 'modal-description' + }); + + expect($document.find('.modal').attr('aria-describedby')).toEqual('modal-description'); + }); + }); }); describe('modal window', function() { @@ -1369,15 +1599,6 @@ describe('$uibModal', function() { expect($rootScope.foo).toBeTruthy(); }); - it('should support custom CSS classes as string', function() { - $rootScope.animate = false; - var windowEl = $compile('
    content
    ')($rootScope); - $rootScope.$digest(); - - expect(windowEl).toHaveClass('test'); - expect(windowEl).toHaveClass('foo'); - }); - it('should support window top class', function () { $rootScope.animate = false; var windowEl = $compile('
    content
    ')($rootScope); @@ -1388,13 +1609,12 @@ describe('$uibModal', function() { }); it('should support custom template url', inject(function($templateCache) { - $templateCache.put('window.html', '
    '); + $templateCache.put('window.html', '
    '); - var windowEl = $compile('
    content
    ')($rootScope); + var windowEl = $compile('
    content
    ')($rootScope); $rootScope.$digest(); - expect(windowEl).toHaveClass('mywindow'); - expect(windowEl).toHaveClass('test'); + expect(windowEl.html()).toBe('
    content
    '); })); }); @@ -1518,16 +1738,19 @@ describe('$uibModal', function() { ds[x] = {index: i, deferred: $q.defer(), reject: reject}; var scope = $rootScope.$new(); + var failed = false; scope.index = i; open({ template: '
    ' + i + '
    ', scope: scope, resolve: { - x: function() { return ds[x].deferred.promise; } + x: function() { return ds[x].deferred.promise['catch'](function () { + failed = true; + }); } } }, true).opened.then(function() { expect($uibModalStack.getTop().value.modalScope.index).toEqual(i); - actual += i; + if (!failed) { actual += i; } }); }); @@ -1680,7 +1903,6 @@ describe('$uibModal', function() { content: '
    Modal1
    ' }); - expect($document).toHaveModalsOpen(0); $rootScope.$digest(); $animate.flush(); expect($document).toHaveModalsOpen(1); @@ -1700,7 +1922,6 @@ describe('$uibModal', function() { modal2Index = parseInt($uibModalStack.getTop().value.modalDomEl.attr('index'), 10); }); - expect($document).toHaveModalsOpen(1); $rootScope.$digest(); $animate.flush(); expect($document).toHaveModalsOpen(2); @@ -1722,7 +1943,6 @@ describe('$uibModal', function() { modal3Index = parseInt($uibModalStack.getTop().value.modalDomEl.attr('index'), 10); }); - expect($document).toHaveModalsOpen(1); $rootScope.$digest(); $animate.flush(); expect($document).toHaveModalsOpen(2); @@ -1730,6 +1950,104 @@ describe('$uibModal', function() { expect(modal3Index).toEqual(2); expect(modal2Index).toBeLessThan(modal3Index); }); + + it('should have top modal with highest z-index', function() { + var modal2zIndex = null; + var modal3zIndex = null; + + var modal1Instance = { + result: $q.defer(), + opened: $q.defer(), + closed: $q.defer(), + rendered: $q.defer(), + close: function(result) { + return $uibModalStack.close(modal1Instance, result); + }, + dismiss: function(reason) { + return $uibModalStack.dismiss(modal1Instance, reason); + } + }; + var modal2Instance = { + result: $q.defer(), + opened: $q.defer(), + closed: $q.defer(), + rendered: $q.defer(), + close: function(result) { + return $uibModalStack.close(modal2Instance, result); + }, + dismiss: function(reason) { + return $uibModalStack.dismiss(modal2Instance, reason); + } + }; + var modal3Instance = { + result: $q.defer(), + opened: $q.defer(), + closed: $q.defer(), + rendered: $q.defer(), + close: function(result) { + return $uibModalStack.close(modal3Instance, result); + }, + dismiss: function(reason) { + return $uibModalStack.dismiss(modal3Instance, reason); + } + }; + + var modal1 = $uibModalStack.open(modal1Instance, { + appendTo: angular.element(document.body), + scope: $rootScope.$new(), + deferred: modal1Instance.result, + renderDeferred: modal1Instance.rendered, + closedDeferred: modal1Instance.closed, + content: '
    Modal1
    ' + }); + + $rootScope.$digest(); + $animate.flush(); + expect($document).toHaveModalsOpen(1); + + expect(+$uibModalStack.getTop().value.modalDomEl[0].style.zIndex).toBe(1050); + + var modal2 = $uibModalStack.open(modal2Instance, { + appendTo: angular.element(document.body), + scope: $rootScope.$new(), + deferred: modal2Instance.result, + renderDeferred: modal2Instance.rendered, + closedDeferred: modal2Instance.closed, + content: '
    Modal2
    ' + }); + + modal2Instance.rendered.promise.then(function() { + modal2zIndex = +$uibModalStack.getTop().value.modalDomEl[0].style.zIndex; + }); + + $rootScope.$digest(); + $animate.flush(); + expect($document).toHaveModalsOpen(2); + + expect(modal2zIndex).toBe(1060); + close(modal1Instance); + expect($document).toHaveModalsOpen(1); + + var modal3 = $uibModalStack.open(modal3Instance, { + appendTo: angular.element(document.body), + scope: $rootScope.$new(), + deferred: modal3Instance.result, + renderDeferred: modal3Instance.rendered, + closedDeferred: modal3Instance.closed, + content: '
    Modal3
    ' + }); + + modal3Instance.rendered.promise.then(function() { + modal3zIndex = +$uibModalStack.getTop().value.modalDomEl[0].style.zIndex; + }); + + $rootScope.$digest(); + $animate.flush(); + expect($document).toHaveModalsOpen(2); + + expect(modal3zIndex).toBe(1070); + expect(modal2zIndex).toBeLessThan(modal3zIndex); + }); }); describe('modal.closing event', function() { diff --git a/src/multiMap/index.js b/src/multiMap/index.js new file mode 100644 index 0000000000..d3db908589 --- /dev/null +++ b/src/multiMap/index.js @@ -0,0 +1 @@ +require('./multiMap.js'); diff --git a/src/multiMap/multiMap.js b/src/multiMap/multiMap.js new file mode 100644 index 0000000000..47bf6d4639 --- /dev/null +++ b/src/multiMap/multiMap.js @@ -0,0 +1,55 @@ +angular.module('ui.bootstrap.multiMap', []) +/** + * A helper, internal data structure that stores all references attached to key + */ + .factory('$$multiMap', function() { + return { + createNew: function() { + var map = {}; + + return { + entries: function() { + return Object.keys(map).map(function(key) { + return { + key: key, + value: map[key] + }; + }); + }, + get: function(key) { + return map[key]; + }, + hasKey: function(key) { + return !!map[key]; + }, + keys: function() { + return Object.keys(map); + }, + put: function(key, value) { + if (!map[key]) { + map[key] = []; + } + + map[key].push(value); + }, + remove: function(key, value) { + var values = map[key]; + + if (!values) { + return; + } + + var idx = values.indexOf(value); + + if (idx !== -1) { + values.splice(idx, 1); + } + + if (!values.length) { + delete map[key]; + } + } + }; + } + }; + }); diff --git a/src/modal/test/multiMap.spec.js b/src/multiMap/test/multiMap.spec.js similarity index 96% rename from src/modal/test/multiMap.spec.js rename to src/multiMap/test/multiMap.spec.js index c89624e914..40345144c9 100644 --- a/src/modal/test/multiMap.spec.js +++ b/src/multiMap/test/multiMap.spec.js @@ -1,7 +1,7 @@ describe('multi map', function() { var multiMap; - beforeEach(module('ui.bootstrap.modal')); + beforeEach(module('ui.bootstrap.multiMap')); beforeEach(inject(function($$multiMap) { multiMap = $$multiMap.createNew(); })); diff --git a/src/pager/docs/demo.html b/src/pager/docs/demo.html index 06e7900208..c88db56e31 100644 --- a/src/pager/docs/demo.html +++ b/src/pager/docs/demo.html @@ -1,5 +1,5 @@

    Pager

    You are currently on page {{currentPage}}
    - +
      diff --git a/src/pager/index.js b/src/pager/index.js index 57cd4e36fc..8f99dbcf81 100644 --- a/src/pager/index.js +++ b/src/pager/index.js @@ -1,4 +1,5 @@ require('../paging'); +require('../tabindex'); require('../../template/pager/pager.html.js'); require('./pager'); diff --git a/src/pager/pager.js b/src/pager/pager.js index 5d871e1ddd..8906183086 100644 --- a/src/pager/pager.js +++ b/src/pager/pager.js @@ -1,4 +1,4 @@ -angular.module('ui.bootstrap.pager', ['ui.bootstrap.paging']) +angular.module('ui.bootstrap.pager', ['ui.bootstrap.paging', 'ui.bootstrap.tabindex']) .controller('UibPagerController', ['$scope', '$attrs', 'uibPaging', 'uibPagerConfig', function($scope, $attrs, uibPaging, uibPagerConfig) { $scope.align = angular.isDefined($attrs.align) ? $scope.$parent.$eval($attrs.align) : uibPagerConfig.align; @@ -22,13 +22,14 @@ angular.module('ui.bootstrap.pager', ['ui.bootstrap.paging']) ngDisabled: '=' }, require: ['uibPager', '?ngModel'], + restrict: 'A', controller: 'UibPagerController', controllerAs: 'pager', templateUrl: function(element, attrs) { return attrs.templateUrl || 'uib/template/pager/pager.html'; }, - replace: true, link: function(scope, element, attrs, ctrls) { + element.addClass('pager'); var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; if (!ngModelCtrl) { diff --git a/src/pager/test/pager.spec.js b/src/pager/test/pager.spec.js index 9676f0a062..5491a5f54f 100644 --- a/src/pager/test/pager.spec.js +++ b/src/pager/test/pager.spec.js @@ -10,7 +10,7 @@ describe('pager directive', function() { $document = _$document_; $templateCache = _$templateCache_; body = $document.find('body'); - element = $compile('')($rootScope); + element = $compile('
        ')($rootScope); $rootScope.$digest(); })); @@ -56,7 +56,7 @@ describe('pager directive', function() { it('exposes the controller on the template', function() { $templateCache.put('uib/template/pager/pager.html', '
        {{pager.text}}
        '); - element = $compile('')($rootScope); + element = $compile('
          ')($rootScope); $rootScope.$digest(); var ctrl = element.controller('uibPager'); @@ -65,7 +65,7 @@ describe('pager directive', function() { ctrl.text = 'foo'; $rootScope.$digest(); - expect(element.html()).toBe('foo'); + expect(element.html()).toBe('
          foo
          '); }); it('disables the "previous" link if current page is 1', function() { @@ -102,7 +102,7 @@ describe('pager directive', function() { it('executes the `ng-change` expression when an element is clicked', function() { $rootScope.selectPageHandler = jasmine.createSpy('selectPageHandler'); - element = $compile('')($rootScope); + element = $compile('
            ')($rootScope); $rootScope.$digest(); clickPaginationEl(-1); @@ -147,16 +147,16 @@ describe('pager directive', function() { it('allows custom templates', function() { $templateCache.put('foo/bar.html', '
            baz
            '); - element = $compile('')($rootScope); + element = $compile('
              ')($rootScope); $rootScope.$digest(); - expect(element.html()).toBe('baz'); + expect(element.html()).toBe('
              baz
              '); }); describe('`items-per-page`', function() { beforeEach(function() { $rootScope.perpage = 5; - element = $compile('')($rootScope); + element = $compile('
                ')($rootScope); $rootScope.$digest(); }); @@ -190,7 +190,7 @@ describe('pager directive', function() { describe('`num-pages`', function() { beforeEach(function() { $rootScope.numpg = null; - element = $compile('')($rootScope); + element = $compile('
                  ')($rootScope); $rootScope.$digest(); }); @@ -206,7 +206,7 @@ describe('pager directive', function() { uibPagerConfig.previousText = 'PR'; uibPagerConfig.nextText = 'NE'; uibPagerConfig.align = false; - element = $compile('')($rootScope); + element = $compile('
                    ')($rootScope); $rootScope.$digest(); })); afterEach(inject(function(uibPagerConfig) { @@ -227,7 +227,7 @@ describe('pager directive', function() { describe('override configuration from attributes', function() { beforeEach(function() { - element = $compile('')($rootScope); + element = $compile('
                      ')($rootScope); $rootScope.$digest(); }); @@ -248,7 +248,7 @@ describe('pager directive', function() { it('changes "previous" & "next" text from interpolated attributes', function() { $rootScope.previousText = '<<'; $rootScope.nextText = '>>'; - element = $compile('')($rootScope); + element = $compile('
                        ')($rootScope); $rootScope.$digest(); expect(getPaginationEl(0).text()).toBe('<<'); @@ -259,7 +259,7 @@ describe('pager directive', function() { it('disables the component when ng-disabled is true', function() { $rootScope.disable = true; - element = $compile('')($rootScope); + element = $compile('
                          ')($rootScope); $rootScope.$digest(); updateCurrentPage(2); diff --git a/src/pagination/docs/demo.html b/src/pagination/docs/demo.html index 9fe06af177..68dab010ed 100644 --- a/src/pagination/docs/demo.html +++ b/src/pagination/docs/demo.html @@ -1,24 +1,24 @@

                          Default

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

                                  Limit the maximum visible buttons

                                  rotate defaulted to true:
                                  - +
                                    rotate defaulted to true and force-ellipses set to true:
                                    - +
                                      rotate set to false:
                                      - +
                                        boundary-link-numbers set to true and rotate defaulted to true:
                                        - +
                                          boundary-link-numbers set to true and rotate set to false:
                                          - +
                                            Page: {{bigCurrentPage}} / {{numPages}}
                                            diff --git a/src/pagination/index.js b/src/pagination/index.js index 0262f24388..07899c79b0 100644 --- a/src/pagination/index.js +++ b/src/pagination/index.js @@ -1,4 +1,5 @@ require('../paging'); +require('../tabindex'); require('../../template/pagination/pagination.html.js'); require('./pagination'); diff --git a/src/pagination/pagination.js b/src/pagination/pagination.js index 6706795c84..eaed65ca07 100644 --- a/src/pagination/pagination.js +++ b/src/pagination/pagination.js @@ -1,4 +1,4 @@ -angular.module('ui.bootstrap.pagination', ['ui.bootstrap.paging']) +angular.module('ui.bootstrap.pagination', ['ui.bootstrap.paging', 'ui.bootstrap.tabindex']) .controller('UibPaginationController', ['$scope', '$attrs', '$parse', 'uibPaging', 'uibPaginationConfig', function($scope, $attrs, $parse, uibPaging, uibPaginationConfig) { var ctrl = this; // Setup configuration parameters @@ -9,6 +9,7 @@ angular.module('ui.bootstrap.pagination', ['ui.bootstrap.paging']) pageLabel = angular.isDefined($attrs.pageLabel) ? function(idx) { return $scope.$parent.$eval($attrs.pageLabel, {$page: idx}); } : angular.identity; $scope.boundaryLinks = angular.isDefined($attrs.boundaryLinks) ? $scope.$parent.$eval($attrs.boundaryLinks) : uibPaginationConfig.boundaryLinks; $scope.directionLinks = angular.isDefined($attrs.directionLinks) ? $scope.$parent.$eval($attrs.directionLinks) : uibPaginationConfig.directionLinks; + $attrs.$set('role', 'menu'); uibPaging.create(this, $scope, $attrs); @@ -132,13 +133,14 @@ angular.module('ui.bootstrap.pagination', ['ui.bootstrap.paging']) ngDisabled:'=' }, require: ['uibPagination', '?ngModel'], + restrict: 'A', controller: 'UibPaginationController', controllerAs: 'pagination', templateUrl: function(element, attrs) { return attrs.templateUrl || 'uib/template/pagination/pagination.html'; }, - replace: true, link: function(scope, element, attrs, ctrls) { + element.addClass('pagination'); var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; if (!ngModelCtrl) { diff --git a/src/pagination/test/pagination.spec.js b/src/pagination/test/pagination.spec.js index 7afa7f4ef2..bc4f6eee6e 100644 --- a/src/pagination/test/pagination.spec.js +++ b/src/pagination/test/pagination.spec.js @@ -11,7 +11,7 @@ describe('pagination directive', function() { $document = _$document_; $templateCache = _$templateCache_; body = $document.find('body'); - element = $compile('')($rootScope); + element = $compile('
                                              ')($rootScope); $rootScope.$digest(); })); @@ -54,11 +54,20 @@ describe('pagination directive', function() { expect(element.hasClass('pagination')).toBe(true); }); + it('has accessibility attributes', function() { + expect(element.attr('role')).toEqual('menu'); + + var li = element.find('li'); + for (var i = 0; i < li.length; i++) { + expect(li.eq(i).attr('role')).toEqual('menuitem'); + } + }); + it('exposes the controller to the template', function() { $templateCache.put('uib/template/pagination/pagination.html', '
                                              {{pagination.randomText}}
                                              '); var scope = $rootScope.$new(); - element = $compile('')(scope); + element = $compile('
                                                ')(scope); $rootScope.$digest(); var ctrl = element.controller('uibPagination'); @@ -68,17 +77,17 @@ describe('pagination directive', function() { ctrl.randomText = 'foo'; $rootScope.$digest(); - expect(element.html()).toBe('foo'); + expect(element.html()).toBe('
                                                foo
                                                '); }); it('allows custom templates', function() { $templateCache.put('foo/bar.html', '
                                                baz
                                                '); var scope = $rootScope.$new(); - element = $compile('')(scope); + element = $compile('
                                                  ')(scope); $rootScope.$digest(); - expect(element.html()).toBe('baz'); + expect(element.html()).toBe('
                                                  baz
                                                  '); }); it('contains num-pages + 2 li elements', function() { @@ -213,7 +222,7 @@ describe('pagination directive', function() { describe('`items-per-page`', function() { beforeEach(function() { $rootScope.perpage = 5; - element = $compile('')($rootScope); + element = $compile('
                                                    ')($rootScope); $rootScope.$digest(); }); @@ -256,7 +265,7 @@ describe('pagination directive', function() { describe('executes `ng-change` expression', function() { beforeEach(function() { $rootScope.selectPageHandler = jasmine.createSpy('selectPageHandler'); - element = $compile('')($rootScope); + element = $compile('
                                                      ')($rootScope); $rootScope.$digest(); }); @@ -286,7 +295,7 @@ describe('pagination directive', function() { $rootScope.total = 98; // 10 pages $rootScope.currentPage = 3; $rootScope.maxSize = 5; - element = $compile('')($rootScope); + element = $compile('
                                                        ')($rootScope); $rootScope.$digest(); }); @@ -367,7 +376,7 @@ describe('pagination directive', function() { $rootScope.total = 98; // 10 pages $rootScope.currentPage = 3; $rootScope.maxSize = 5; - element = $compile('')($rootScope); + element = $compile('
                                                          ')($rootScope); $rootScope.$digest(); }); @@ -429,7 +438,7 @@ describe('pagination directive', function() { $rootScope.total = 98; // 10 pages $rootScope.currentPage = 3; $rootScope.maxSize = 5; - element = $compile('')($rootScope); + element = $compile('
                                                            ')($rootScope); $rootScope.$digest(); }); @@ -522,7 +531,7 @@ describe('pagination directive', function() { $rootScope.currentPage = 7; $rootScope.maxSize = 5; $rootScope.rotate = false; - element = $compile('')($rootScope); + element = $compile('
                                                              ')($rootScope); $rootScope.$digest(); }); @@ -581,7 +590,7 @@ describe('pagination directive', function() { describe('pagination directive with `boundary-links`', function() { beforeEach(function() { - element = $compile('')($rootScope); + element = $compile('
                                                                ')($rootScope); $rootScope.$digest(); }); @@ -636,7 +645,7 @@ describe('pagination directive', function() { }); it('changes "first" & "last" text from attributes', function() { - element = $compile('')($rootScope); + element = $compile('
                                                                  ')($rootScope); $rootScope.$digest(); expect(getPaginationEl(0).text()).toBe('<<<'); @@ -644,7 +653,7 @@ describe('pagination directive', function() { }); it('changes "previous" & "next" text from attributes', function() { - element = $compile('')($rootScope); + element = $compile('
                                                                    ')($rootScope); $rootScope.$digest(); expect(getPaginationEl(1).text()).toBe('<<'); @@ -654,7 +663,7 @@ describe('pagination directive', function() { it('changes "first" & "last" text from interpolated attributes', function() { $rootScope.myfirstText = '<<<'; $rootScope.mylastText = '>>>'; - element = $compile('')($rootScope); + element = $compile('
                                                                      ')($rootScope); $rootScope.$digest(); expect(getPaginationEl(0).text()).toBe('<<<'); @@ -664,7 +673,7 @@ describe('pagination directive', function() { it('changes "previous" & "next" text from interpolated attributes', function() { $rootScope.previousText = '<<'; $rootScope.nextText = '>>'; - element = $compile('')($rootScope); + element = $compile('
                                                                        ')($rootScope); $rootScope.$digest(); expect(getPaginationEl(1).text()).toBe('<<'); @@ -700,7 +709,7 @@ describe('pagination directive', function() { describe('pagination directive with just number links', function() { beforeEach(function() { - element = $compile('')($rootScope); + element = $compile('
                                                                          ')($rootScope); $rootScope.$digest(); }); @@ -752,7 +761,7 @@ describe('pagination directive', function() { describe('with just boundary & number links', function() { beforeEach(function() { $rootScope.directions = false; - element = $compile('')($rootScope); + element = $compile('
                                                                            ')($rootScope); $rootScope.$digest(); }); @@ -784,7 +793,7 @@ describe('pagination directive', function() { describe('`num-pages`', function() { beforeEach(function() { $rootScope.numpg = null; - element = $compile('')($rootScope); + element = $compile('
                                                                              ')($rootScope); $rootScope.$digest(); }); @@ -827,7 +836,7 @@ describe('pagination directive', function() { paginationConfig.previousText = 'PR'; paginationConfig.nextText = 'NE'; paginationConfig.lastText = 'LA'; - element = $compile('')($rootScope); + element = $compile('
                                                                                ')($rootScope); $rootScope.$digest(); expect(getPaginationEl(0).text()).toBe('FI'); @@ -838,7 +847,7 @@ describe('pagination directive', function() { it('contains number of pages + 2 li elements', function() { paginationConfig.itemsPerPage = 5; - element = $compile('')($rootScope); + element = $compile('
                                                                                  ')($rootScope); $rootScope.$digest(); expect(getPaginationBarSize()).toBe(12); @@ -846,7 +855,7 @@ describe('pagination directive', function() { it('should take maxSize defaults into account', function() { paginationConfig.maxSize = 2; - element = $compile('')($rootScope); + element = $compile('
                                                                                    ')($rootScope); $rootScope.$digest(); expect(getPaginationBarSize()).toBe(4); @@ -854,7 +863,7 @@ describe('pagination directive', function() { it('should take forceEllipses defaults into account', function () { paginationConfig.forceEllipses = true; - element = $compile('')($rootScope); + element = $compile('
                                                                                      ')($rootScope); $rootScope.$digest(); // Should contain 2 nav buttons, 2 pages, and 2 ellipsis since the currentPage defaults to 3, which is in the middle @@ -865,7 +874,7 @@ describe('pagination directive', function() { paginationConfig.boundaryLinkNumbers = true; $rootScope.total = 88; // 9 pages $rootScope.currentPage = 5; - element = $compile('')($rootScope); + element = $compile('
                                                                                        ')($rootScope); $rootScope.$digest(); // Should contain 2 nav buttons, 2 pages, 2 ellipsis, and 2 extra end numbers since the currentPage is in the middle @@ -879,7 +888,7 @@ describe('pagination directive', function() { $rootScope.pageLabel = function(id) { return 'test_'+ id; }; - element = $compile('')($rootScope); + element = $compile('
                                                                                          ')($rootScope); $rootScope.$digest(); }); @@ -904,7 +913,7 @@ describe('pagination directive', function() { describe('disabled with ngDisable', function() { beforeEach(function() { - element = $compile('')($rootScope); + element = $compile('
                                                                                            ')($rootScope); $rootScope.currentPage = 3; $rootScope.$digest(); }); @@ -941,7 +950,7 @@ describe('pagination directive', function() { it('should retain the model value when total-items starts as undefined', function() { $rootScope.currentPage = 5; $rootScope.total = undefined; - element = $compile('')($rootScope); + element = $compile('
                                                                                              ')($rootScope); $rootScope.$digest(); expect($rootScope.currentPage).toBe(5); diff --git a/src/popover/docs/demo.html b/src/popover/docs/demo.html index 389c0cf133..1cd405176f 100644 --- a/src/popover/docs/demo.html +++ b/src/popover/docs/demo.html @@ -44,4 +44,5 @@

                                                                                              Other

                                                                                              +
                                                                                              diff --git a/src/popover/docs/readme.md b/src/popover/docs/readme.md index 59a9982326..1e12a35b50 100644 --- a/src/popover/docs/readme.md +++ b/src/popover/docs/readme.md @@ -34,6 +34,9 @@ All these settings are available for the three types of popovers. _(Default: `false`, Config: `appendToBody`)_ - Should the popover be appended to '$body' instead of the parent element? +* `popover-class` - + Custom class to be applied to the popover. + * `popover-enable` $ _(Default: `true`)_ - diff --git a/src/popover/popover.js b/src/popover/popover.js index af30188b24..df94a4c96f 100644 --- a/src/popover/popover.js +++ b/src/popover/popover.js @@ -7,9 +7,8 @@ angular.module('ui.bootstrap.popover', ['ui.bootstrap.tooltip']) .directive('uibPopoverTemplatePopup', function() { return { - replace: true, - scope: { uibTitle: '@', contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&', - originScope: '&' }, + restrict: 'A', + scope: { uibTitle: '@', contentExp: '&', originScope: '&' }, templateUrl: 'uib/template/popover/popover-template.html' }; }) @@ -22,8 +21,8 @@ angular.module('ui.bootstrap.popover', ['ui.bootstrap.tooltip']) .directive('uibPopoverHtmlPopup', function() { return { - replace: true, - scope: { contentExp: '&', uibTitle: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' }, + restrict: 'A', + scope: { contentExp: '&', uibTitle: '@' }, templateUrl: 'uib/template/popover/popover-html.html' }; }) @@ -36,8 +35,8 @@ angular.module('ui.bootstrap.popover', ['ui.bootstrap.tooltip']) .directive('uibPopoverPopup', function() { return { - replace: true, - scope: { uibTitle: '@', content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' }, + restrict: 'A', + scope: { uibTitle: '@', content: '@' }, templateUrl: 'uib/template/popover/popover.html' }; }) diff --git a/src/popover/test/popover-html.spec.js b/src/popover/test/popover-html.spec.js index fb25ba969e..fd43e51f15 100644 --- a/src/popover/test/popover-html.spec.js +++ b/src/popover/test/popover-html.spec.js @@ -21,6 +21,7 @@ describe('popover', function() { scope.template = $sce.trustAsHtml('My template'); $compile(elmBody)(scope); scope.$digest(); + $document.find('body').append(elmBody); elm = elmBody.find('span'); elmScope = elm.scope(); tooltipScope = elmScope.$$childTail; @@ -87,6 +88,7 @@ describe('popover', function() { it('should hide popover when template becomes empty', inject(function($timeout) { elm.trigger('click'); tooltipScope.$digest(); + $timeout.flush(0); expect(tooltipScope.isOpen).toBe(true); scope.template = ''; diff --git a/src/popover/test/popover-template.spec.js b/src/popover/test/popover-template.spec.js index e52c2f8aa8..b46e668b06 100644 --- a/src/popover/test/popover-template.spec.js +++ b/src/popover/test/popover-template.spec.js @@ -25,6 +25,7 @@ describe('popover template', function() { scope = $rootScope; $compile(elmBody)(scope); + $document.find('body').append(elmBody); scope.templateUrl = 'myUrl'; scope.$digest(); @@ -35,6 +36,7 @@ describe('popover template', function() { afterEach(function() { $document.off('keypress'); + elmBody.remove(); }); it('should open on click', inject(function() { @@ -75,6 +77,7 @@ describe('popover template', function() { it('should hide popover when template becomes empty', inject(function($timeout) { elm.trigger('click'); tooltipScope.$digest(); + $timeout.flush(0); expect(tooltipScope.isOpen).toBe(true); scope.templateUrl = ''; diff --git a/src/position/position.js b/src/position/position.js index 341b0a8209..3ab066abb5 100644 --- a/src/position/position.js +++ b/src/position/position.js @@ -130,7 +130,7 @@ angular.module('ui.bootstrap.position', []) var paddingRight = this.parseStyle(elemStyle.paddingRight); var paddingBottom = this.parseStyle(elemStyle.paddingBottom); var scrollParent = this.scrollParent(elem, false, true); - var scrollbarWidth = this.scrollbarWidth(scrollParent, BODY_REGEX.test(scrollParent.tagName)); + var scrollbarWidth = this.scrollbarWidth(BODY_REGEX.test(scrollParent.tagName)); return { scrollbarWidth: scrollbarWidth, @@ -529,13 +529,33 @@ angular.module('ui.bootstrap.position', []) }, /** - * Provides a way for positioning tooltip & dropdown - * arrows when using placement options beyond the standard - * left, right, top, or bottom. - * - * @param {element} elem - The tooltip/dropdown element. - * @param {string} placement - The placement for the elem. - */ + * Provides a way to adjust the top positioning after first + * render to correctly align element to top after content + * rendering causes resized element height + * + * @param {array} placementClasses - The array of strings of classes + * element should have. + * @param {object} containerPosition - The object with container + * position information + * @param {number} initialHeight - The initial height for the elem. + * @param {number} currentHeight - The current height for the elem. + */ + adjustTop: function(placementClasses, containerPosition, initialHeight, currentHeight) { + if (placementClasses.indexOf('top') !== -1 && initialHeight !== currentHeight) { + return { + top: containerPosition.top - currentHeight + 'px' + }; + } + }, + + /** + * Provides a way for positioning tooltip & dropdown + * arrows when using placement options beyond the standard + * left, right, top, or bottom. + * + * @param {element} elem - The tooltip/dropdown element. + * @param {string} placement - The placement for the elem. + */ positionArrow: function(elem, placement) { elem = this.getRawNode(elem); diff --git a/src/rating/docs/demo.html b/src/rating/docs/demo.html index 44f4100413..bac7d70b61 100644 --- a/src/rating/docs/demo.html +++ b/src/rating/docs/demo.html @@ -1,6 +1,6 @@

                                                                                              Default

                                                                                              - + {{percent}}%
                                                                                              Rate: {{rate}} - Readonly is: {{isReadonly}} - Hovering over: {{overStar || "none"}}
                                                                                              @@ -10,6 +10,6 @@

                                                                                              Default


                                                                                              Custom icons

                                                                                              -
                                                                                              (Rate: {{x}})
                                                                                              -
                                                                                              (Rate: {{y}})
                                                                                              +
                                                                                              (Rate: {{x}})
                                                                                              +
                                                                                              (Rate: {{y}})
                                                                                              diff --git a/src/rating/rating.js b/src/rating/rating.js index d1b90b0335..831c515432 100644 --- a/src/rating/rating.js +++ b/src/rating/rating.js @@ -5,7 +5,7 @@ angular.module('ui.bootstrap.rating', []) stateOn: null, stateOff: null, enableReset: true, - titles : ['one', 'two', 'three', 'four', 'five'] + titles: ['one', 'two', 'three', 'four', 'five'] }) .controller('UibRatingController', ['$scope', '$attrs', 'uibRatingConfig', function($scope, $attrs, ratingConfig) { @@ -90,6 +90,7 @@ angular.module('ui.bootstrap.rating', []) .directive('uibRating', function() { return { require: ['uibRating', 'ngModel'], + restrict: 'A', scope: { readonly: '=?readOnly', onHover: '&', @@ -97,7 +98,6 @@ angular.module('ui.bootstrap.rating', []) }, controller: 'UibRatingController', templateUrl: 'uib/template/rating/rating.html', - replace: true, link: function(scope, element, attrs, ctrls) { var ratingCtrl = ctrls[0], ngModelCtrl = ctrls[1]; ratingCtrl.init(ngModelCtrl); diff --git a/src/rating/test/rating.spec.js b/src/rating/test/rating.spec.js index abbf7b5508..6e2b3e85a6 100644 --- a/src/rating/test/rating.spec.js +++ b/src/rating/test/rating.spec.js @@ -1,17 +1,18 @@ describe('rating directive', function() { - var $rootScope, $compile, element; + var $rootScope, $compile, element, innerElem; beforeEach(module('ui.bootstrap.rating')); beforeEach(module('uib/template/rating/rating.html')); beforeEach(inject(function(_$compile_, _$rootScope_) { $compile = _$compile_; $rootScope = _$rootScope_; $rootScope.rate = 3; - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); + innerElem = element.children().eq(0); })); function getStars() { - return element.find('i'); + return innerElem.find('i'); } function getStar(number) { @@ -38,17 +39,17 @@ describe('rating directive', function() { function triggerKeyDown(keyCode) { var e = $.Event('keydown'); e.which = keyCode; - element.trigger(e); + innerElem.trigger(e); } it('contains the default number of icons', function() { expect(getStars().length).toBe(5); - expect(element.attr('aria-valuemax')).toBe('5'); + expect(innerElem.attr('aria-valuemax')).toBe('5'); }); it('initializes the default star icons as selected', function() { expect(getState()).toEqual([true, true, true, false, false]); - expect(element.attr('aria-valuenow')).toBe('3'); + expect(innerElem.attr('aria-valuenow')).toBe('3'); }); it('handles correctly the click event', function() { @@ -56,19 +57,19 @@ describe('rating directive', function() { $rootScope.$digest(); expect(getState()).toEqual([true, true, false, false, false]); expect($rootScope.rate).toBe(2); - expect(element.attr('aria-valuenow')).toBe('2'); + expect(innerElem.attr('aria-valuenow')).toBe('2'); getStar(5).click(); $rootScope.$digest(); expect(getState()).toEqual([true, true, true, true, true]); expect($rootScope.rate).toBe(5); - expect(element.attr('aria-valuenow')).toBe('5'); + expect(innerElem.attr('aria-valuenow')).toBe('5'); getStar(5).click(); $rootScope.$digest(); expect(getState()).toEqual([false, false, false, false, false]); expect($rootScope.rate).toBe(0); - expect(element.attr('aria-valuenow')).toBe('0'); + expect(innerElem.attr('aria-valuenow')).toBe('0'); }); it('handles correctly the hover event', function() { @@ -82,7 +83,7 @@ describe('rating directive', function() { expect(getState()).toEqual([true, true, true, true, true]); expect($rootScope.rate).toBe(3); - element.trigger('mouseout'); + innerElem.trigger('mouseout'); expect(getState()).toEqual([true, true, true, false, false]); expect($rootScope.rate).toBe(3); }); @@ -92,13 +93,13 @@ describe('rating directive', function() { $rootScope.$digest(); expect(getState()).toEqual([true, true, false, false, false]); - expect(element.attr('aria-valuenow')).toBe('2'); + expect(innerElem.attr('aria-valuenow')).toBe('2'); $rootScope.rate = 2.5; $rootScope.$digest(); expect(getState()).toEqual([true, true, true, false, false]); - expect(element.attr('aria-valuenow')).toBe('3'); + expect(innerElem.attr('aria-valuenow')).toBe('3'); }); it('changes the number of selected icons when value changes', function() { @@ -106,30 +107,33 @@ describe('rating directive', function() { $rootScope.$digest(); expect(getState()).toEqual([true, true, false, false, false]); - expect(element.attr('aria-valuenow')).toBe('2'); - expect(element.attr('aria-valuetext')).toBe('two'); + expect(innerElem.attr('aria-valuenow')).toBe('2'); + expect(innerElem.attr('aria-valuetext')).toBe('two'); }); it('shows different number of icons when `max` attribute is set', function() { - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); + innerElem = element.children().eq(0); expect(getStars().length).toBe(7); - expect(element.attr('aria-valuemax')).toBe('7'); + expect(innerElem.attr('aria-valuemax')).toBe('7'); }); it('shows different number of icons when `max` attribute is from scope variable', function() { $rootScope.max = 15; - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); + innerElem = element.children().eq(0); expect(getStars().length).toBe(15); - expect(element.attr('aria-valuemax')).toBe('15'); + expect(innerElem.attr('aria-valuemax')).toBe('15'); }); it('handles read-only attribute', function() { $rootScope.isReadonly = true; - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); + innerElem = element.children().eq(0); expect(getState()).toEqual([true, true, true, false, false]); @@ -148,8 +152,9 @@ describe('rating directive', function() { it('handles enable-reset attribute', function() { $rootScope.canReset = false; - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); + innerElem = element.children().eq(0); var star = { states: [true, true, true, true, true], @@ -162,19 +167,20 @@ describe('rating directive', function() { $rootScope.$digest(); expect(getState()).toEqual(star.states); expect($rootScope.rate).toBe(5); - expect(element.attr('aria-valuenow')).toBe('5'); + expect(innerElem.attr('aria-valuenow')).toBe('5'); selectStar.click(); $rootScope.$digest(); expect(getState()).toEqual(star.states); expect($rootScope.rate).toBe(5); - expect(element.attr('aria-valuenow')).toBe('5'); + expect(innerElem.attr('aria-valuenow')).toBe('5'); }); it('should fire onHover', function() { $rootScope.hoveringOver = jasmine.createSpy('hoveringOver'); - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); + innerElem = element.children().eq(0); getStar(3).trigger('mouseover'); $rootScope.$digest(); @@ -183,10 +189,11 @@ describe('rating directive', function() { it('should fire onLeave', function() { $rootScope.leaving = jasmine.createSpy('leaving'); - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); + innerElem = element.children().eq(0); - element.trigger('mouseleave'); + innerElem.trigger('mouseleave'); $rootScope.$digest(); expect($rootScope.leaving).toHaveBeenCalled(); }); @@ -243,8 +250,9 @@ describe('rating directive', function() { beforeEach(inject(function() { $rootScope.classOn = 'icon-ok-sign'; $rootScope.classOff = 'icon-ok-circle'; - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); + innerElem = element.children().eq(0); })); it('changes the default icons', function() { @@ -260,13 +268,14 @@ describe('rating directive', function() { {stateOn: 'heart'}, {stateOff: 'off'} ]; - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); + innerElem = element.children().eq(0); })); it('should define number of icon elements', function() { expect(getStars().length).toBe(4); - expect(element.attr('aria-valuemax')).toBe('4'); + expect(innerElem.attr('aria-valuemax')).toBe('4'); }); it('handles each icon', function() { @@ -291,8 +300,9 @@ describe('rating directive', function() { uibRatingConfig.max = 10; uibRatingConfig.stateOn = 'on'; uibRatingConfig.stateOff = 'off'; - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); + innerElem = element.children().eq(0); })); afterEach(inject(function(uibRatingConfig) { // return it to the original state @@ -320,8 +330,9 @@ describe('rating directive', function() { $rootScope.rate = 5; angular.extend(originalConfig, uibRatingConfig); uibRatingConfig.max = 10; - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); + innerElem = element.children().eq(0); })); afterEach(inject(function(uibRatingConfig) { // return it to the original state @@ -336,18 +347,20 @@ describe('rating directive', function() { describe('shows custom titles ', function() { it('should return the custom title for each star', function() { $rootScope.titles = [44,45,46]; - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); + innerElem = element.children().eq(0); expect(getTitles()).toEqual(['44', '45', '46', '4', '5']); }); it('should return the default title if the custom title is empty', function() { $rootScope.titles = []; - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); + innerElem = element.children().eq(0); expect(getTitles()).toEqual(['one', 'two', 'three', 'four', 'five']); }); it('should return the default title if the custom title is not an array', function() { - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); expect(getTitles()).toEqual(['one', 'two', 'three', 'four', 'five']); }); diff --git a/src/tabindex/index.js b/src/tabindex/index.js new file mode 100644 index 0000000000..5d6b5663dc --- /dev/null +++ b/src/tabindex/index.js @@ -0,0 +1,7 @@ +require('./tabindex'); + +var MODULE_NAME = 'ui.bootstrap.module.tabindex'; + +angular.module(MODULE_NAME, ['ui.bootstrap.tabindex']); + +module.exports = MODULE_NAME; diff --git a/src/tabindex/tabindex.js b/src/tabindex/tabindex.js new file mode 100644 index 0000000000..a00a0aff3b --- /dev/null +++ b/src/tabindex/tabindex.js @@ -0,0 +1,12 @@ +angular.module('ui.bootstrap.tabindex', []) + +.directive('uibTabindexToggle', function() { + return { + restrict: 'A', + link: function(scope, elem, attrs) { + attrs.$observe('disabled', function(disabled) { + attrs.$set('tabindex', disabled ? -1 : null); + }); + } + }; +}); diff --git a/src/tabindex/test/tabindex.spec.js b/src/tabindex/test/tabindex.spec.js new file mode 100644 index 0000000000..b1a2159d82 --- /dev/null +++ b/src/tabindex/test/tabindex.spec.js @@ -0,0 +1,23 @@ +describe('tabindex toggle directive', function() { + var $rootScope, element; + beforeEach(module('ui.bootstrap.tabindex')); + beforeEach(inject(function($compile, _$rootScope_) { + $rootScope = _$rootScope_; + element = $compile('foo')($rootScope); + $rootScope.$digest(); + })); + + it('should toggle the tabindex on disabled toggle', function() { + expect(element.prop('tabindex')).toBe(0); + + $rootScope.disabled = true; + $rootScope.$digest(); + + expect(element.prop('tabindex')).toBe(-1); + + $rootScope.disabled = false; + $rootScope.$digest(); + + expect(element.prop('tabindex')).toBe(0); + }); +}); diff --git a/src/timepicker/docs/demo.html b/src/timepicker/docs/demo.html index 8bf9e9f64c..fc610a5a6e 100644 --- a/src/timepicker/docs/demo.html +++ b/src/timepicker/docs/demo.html @@ -1,6 +1,6 @@
                                                                                              - +
                                                                                              Time is: {{mytime | date:'shortTime' }}
                                                                                              diff --git a/src/timepicker/test/timepicker.spec.js b/src/timepicker/test/timepicker.spec.js index 8a8cac4768..74ae439b0c 100644 --- a/src/timepicker/test/timepicker.spec.js +++ b/src/timepicker/test/timepicker.spec.js @@ -9,7 +9,7 @@ describe('timepicker directive', function() { $rootScope.time = newTime(14, 40, 25); $templateCache = _$templateCache_; - element = $compile('')($rootScope); + element = $compile('
                                                                                              ')($rootScope); $rootScope.$digest(); modelCtrl = element.controller('ngModel'); @@ -130,7 +130,7 @@ describe('timepicker directive', function() { it('has `selected` current time when model is initially cleared', function() { $rootScope.time = null; - element = $compile('')($rootScope); + element = $compile('
                                                                                              ')($rootScope); $rootScope.$digest(); expect($rootScope.time).toBe(null); @@ -385,7 +385,7 @@ describe('timepicker directive', function() { }); it('changes only the time part when minutes change', function() { - element = $compile('')($rootScope); + element = $compile('
                                                                                              ')($rootScope); $rootScope.time = newTime(0, 0, 0); $rootScope.$digest(); @@ -648,7 +648,7 @@ describe('timepicker directive', function() { $rootScope.mstep = 30; $rootScope.sstep = 30; $rootScope.time = newTime(14, 0 , 0); - element = $compile('')($rootScope); + element = $compile('
                                                                                              ')($rootScope); $rootScope.$digest(); }); @@ -840,7 +840,7 @@ describe('timepicker directive', function() { beforeEach(function(){ $rootScope.displaysSeconds = false; $rootScope.time = newTime(14,40,35); - element = $compile('')($rootScope); + element = $compile('
                                                                                              ')($rootScope); $rootScope.$digest(); }); @@ -914,7 +914,7 @@ describe('timepicker directive', function() { beforeEach(function() { $rootScope.meridian = false; $rootScope.time = newTime(14, 10, 20); - element = $compile('')($rootScope); + element = $compile('
                                                                                              ')($rootScope); $rootScope.$digest(); }); @@ -946,7 +946,7 @@ describe('timepicker directive', function() { it('handles correctly initially empty model on parent element', function() { $rootScope.time = null; - element = $compile('')($rootScope); + element = $compile('
                                                                                              ')($rootScope); $rootScope.$digest(); expect($rootScope.time).toBe(null); @@ -956,7 +956,7 @@ describe('timepicker directive', function() { describe('`meridians` attribute', function() { beforeEach(inject(function() { $rootScope.meridiansArray = ['am', 'pm']; - element = $compile('')($rootScope); + element = $compile('
                                                                                              ')($rootScope); $rootScope.$digest(); })); @@ -974,7 +974,7 @@ describe('timepicker directive', function() { describe('`readonly-input` attribute', function() { beforeEach(inject(function() { $rootScope.meridiansArray = ['am', 'pm']; - element = $compile('')($rootScope); + element = $compile('
                                                                                              ')($rootScope); $rootScope.$digest(); })); @@ -993,7 +993,7 @@ describe('timepicker directive', function() { } it('should pad the hours by default', function() { - element = $compile('')($rootScope); + element = $compile('
                                                                                              ')($rootScope); $rootScope.$digest(); var inputs = element.find('input'); @@ -1005,7 +1005,7 @@ describe('timepicker directive', function() { }); it('should not pad the hours', function() { - element = $compile('')($rootScope); + element = $compile('
                                                                                              ')($rootScope); $rootScope.$digest(); var inputs = element.find('input'); @@ -1025,7 +1025,7 @@ describe('timepicker directive', function() { uibTimepickerConfig.minuteStep = 10; uibTimepickerConfig.secondStep = 10; uibTimepickerConfig.showMeridian = false; - element = $compile('')($rootScope); + element = $compile('
                                                                                              ')($rootScope); $rootScope.$digest(); })); @@ -1086,7 +1086,7 @@ describe('timepicker directive', function() { angular.extend(originalConfig, uibTimepickerConfig); uibTimepickerConfig.meridians = ['π.μ.', 'μ.μ.']; uibTimepickerConfig.showMeridian = true; - element = $compile('')($rootScope); + element = $compile('
                                                                                              ')($rootScope); $rootScope.$digest(); })); afterEach(inject(function(uibTimepickerConfig) { @@ -1116,7 +1116,7 @@ describe('timepicker directive', function() { $templateCache.put(newTemplateUrl, '
                                                                                              baz
                                                                                              '); uibTimepickerConfig.templateUrl = newTemplateUrl; - element = $compile('')($rootScope); + element = $compile('
                                                                                              ')($rootScope); $rootScope.$digest(); })); afterEach(inject(function(uibTimepickerConfig) { @@ -1126,7 +1126,7 @@ describe('timepicker directive', function() { it('should use a custom template', function() { expect(element[0].tagName.toLowerCase()).toBe('div'); - expect(element.html()).toBe('baz'); + expect(element.html()).toBe('
                                                                                              baz
                                                                                              '); }); }); @@ -1230,6 +1230,7 @@ describe('timepicker directive', function() { changeInputValueTo(el, 'pizza'); expect($rootScope.time).toBe(null); expect(el.parent().hasClass('has-error')).toBe(true); + expect(el.hasClass('ng-invalid-hours')); expect(element.hasClass('ng-invalid-time')).toBe(true); changeInputValueTo(el, 8); @@ -1247,6 +1248,7 @@ describe('timepicker directive', function() { changeInputValueTo(el, '8a'); expect($rootScope.time).toBe(null); expect(el.parent().hasClass('has-error')).toBe(true); + expect(el.hasClass('ng-invalid-minutes')); expect(element.hasClass('ng-invalid-time')).toBe(true); changeInputValueTo(el, 22); @@ -1262,6 +1264,7 @@ describe('timepicker directive', function() { changeInputValueTo(el, 'pizza'); expect($rootScope.time).toBe(null); expect(el.parent().hasClass('has-error')).toBe(true); + expect(el.hasClass('ng-invalid-seconds')); expect(element.hasClass('ng-invalid-time')).toBe(true); changeInputValueTo(el, 13); @@ -1291,6 +1294,9 @@ describe('timepicker directive', function() { elS.blur(); $rootScope.$digest(); + expect(elH.hasClass('ng-valid')); + expect(elM.hasClass('ng-valid')); + expect(elS.hasClass('ng-valid')); expect(element.hasClass('ng-invalid-time')).toBe(false); }); @@ -1328,7 +1334,7 @@ describe('timepicker directive', function() { it('handles 12/24H mode change', function() { $rootScope.meridian = true; - element = $compile('')($rootScope); + element = $compile('
                                                                                              ')($rootScope); $rootScope.$digest(); var el = getHoursInputEl(); @@ -1346,14 +1352,14 @@ describe('timepicker directive', function() { }); it('should have a default tabindex of 0', function() { - element = $compile('')($rootScope); + element = $compile('
                                                                                              ')($rootScope); $rootScope.$digest(); expect(element.isolateScope().tabindex).toBe(0); }); it('should have the correct tabindex', function() { - element = $compile('')($rootScope); + element = $compile('
                                                                                              ')($rootScope); $rootScope.$digest(); expect(element.attr('tabindex')).toBe(undefined); @@ -1363,7 +1369,7 @@ describe('timepicker directive', function() { describe('when model is not a Date', function() { beforeEach(inject(function() { - element = $compile('')($rootScope); + element = $compile('
                                                                                              ')($rootScope); })); it('should not be invalid when the model is null', function() { @@ -1415,7 +1421,7 @@ describe('timepicker directive', function() { describe('use with `ng-required` directive', function() { beforeEach(inject(function() { $rootScope.time = null; - element = $compile('')($rootScope); + element = $compile('
                                                                                              ')($rootScope); $rootScope.$digest(); })); @@ -1434,7 +1440,7 @@ describe('timepicker directive', function() { beforeEach(inject(function() { $rootScope.changeHandler = jasmine.createSpy('changeHandler'); $rootScope.time = new Date(); - element = $compile('')($rootScope); + element = $compile('
                                                                                              ')($rootScope); $rootScope.$digest(); })); @@ -1465,7 +1471,7 @@ describe('timepicker directive', function() { describe('when used with min', function() { var changeInputValueTo; beforeEach(inject(function($sniffer) { - element = $compile('')($rootScope); + element = $compile('
                                                                                              ')($rootScope); $rootScope.$digest(); changeInputValueTo = function(inputEl, value) { inputEl.val(value); @@ -1792,7 +1798,7 @@ describe('timepicker directive', function() { describe('when used with max', function() { var changeInputValueTo; beforeEach(inject(function($sniffer) { - element = $compile('')($rootScope); + element = $compile('
                                                                                              ')($rootScope); $rootScope.$digest(); changeInputValueTo = function(inputEl, value) { inputEl.val(value); @@ -2121,16 +2127,16 @@ describe('timepicker directive', function() { it('should allow custom templates', function() { $templateCache.put('foo/bar.html', '
                                                                                              baz
                                                                                              '); - element = $compile('')($rootScope); + element = $compile('
                                                                                              ')($rootScope); $rootScope.$digest(); expect(element[0].tagName.toLowerCase()).toBe('div'); - expect(element.html()).toBe('baz'); + expect(element.html()).toBe('
                                                                                              baz
                                                                                              '); }); it('should expose the controller on the view', function() { $templateCache.put('uib/template/timepicker/timepicker.html', '
                                                                                              {{timepicker.text}}
                                                                                              '); - element = $compile('')($rootScope); + element = $compile('
                                                                                              ')($rootScope); $rootScope.$digest(); var ctrl = element.controller('uibTimepicker'); @@ -2139,14 +2145,14 @@ describe('timepicker directive', function() { ctrl.text = 'foo'; $rootScope.$digest(); - expect(element.html()).toBe('
                                                                                              foo
                                                                                              '); + expect(element.html()).toBe('
                                                                                              foo
                                                                                              '); }); }); describe('ngDisabled', function() { it('prevents modifying date via controls when true', function() { $rootScope.disabled = false; - element = $compile('
                                                                                              ')($rootScope); $rootScope.$digest(); var inputs = element.find('input'); @@ -2250,7 +2256,7 @@ describe('timepicker directive', function() { var $scope; beforeEach(inject(function() { $scope = $rootScope.$new(); - element = $compile('')($scope); + element = $compile('
                                                                                              ')($scope); $rootScope.$digest(); })); diff --git a/src/timepicker/timepicker.js b/src/timepicker/timepicker.js index 8c62024a1c..4e77901cdd 100644 --- a/src/timepicker/timepicker.js +++ b/src/timepicker/timepicker.js @@ -15,6 +15,7 @@ angular.module('ui.bootstrap.timepicker', []) }) .controller('UibTimepickerController', ['$scope', '$element', '$attrs', '$parse', '$log', '$locale', 'uibTimepickerConfig', function($scope, $element, $attrs, $parse, $log, $locale, timepickerConfig) { + var hoursModelCtrl, minutesModelCtrl, secondsModelCtrl; var selected = new Date(), watchers = [], ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl @@ -36,6 +37,10 @@ angular.module('ui.bootstrap.timepicker', []) minutesInputEl = inputs.eq(1), secondsInputEl = inputs.eq(2); + hoursModelCtrl = hoursInputEl.controller('ngModel'); + minutesModelCtrl = minutesInputEl.controller('ngModel'); + secondsModelCtrl = secondsInputEl.controller('ngModel'); + var mousewheel = angular.isDefined($attrs.mousewheel) ? $scope.$parent.$eval($attrs.mousewheel) : timepickerConfig.mousewheel; if (mousewheel) { @@ -215,21 +220,21 @@ angular.module('ui.bootstrap.timepicker', []) return e.detail || delta > 0; }; - hoursInputEl.bind('mousewheel wheel', function(e) { + hoursInputEl.on('mousewheel wheel', function(e) { if (!disabled) { $scope.$apply(isScrollingUp(e) ? $scope.incrementHours() : $scope.decrementHours()); } e.preventDefault(); }); - minutesInputEl.bind('mousewheel wheel', function(e) { + minutesInputEl.on('mousewheel wheel', function(e) { if (!disabled) { $scope.$apply(isScrollingUp(e) ? $scope.incrementMinutes() : $scope.decrementMinutes()); } e.preventDefault(); }); - secondsInputEl.bind('mousewheel wheel', function(e) { + secondsInputEl.on('mousewheel wheel', function(e) { if (!disabled) { $scope.$apply(isScrollingUp(e) ? $scope.incrementSeconds() : $scope.decrementSeconds()); } @@ -239,7 +244,7 @@ angular.module('ui.bootstrap.timepicker', []) // Respond on up/down arrowkeys this.setupArrowkeyEvents = function(hoursInputEl, minutesInputEl, secondsInputEl) { - hoursInputEl.bind('keydown', function(e) { + hoursInputEl.on('keydown', function(e) { if (!disabled) { if (e.which === 38) { // up e.preventDefault(); @@ -253,7 +258,7 @@ angular.module('ui.bootstrap.timepicker', []) } }); - minutesInputEl.bind('keydown', function(e) { + minutesInputEl.on('keydown', function(e) { if (!disabled) { if (e.which === 38) { // up e.preventDefault(); @@ -267,7 +272,7 @@ angular.module('ui.bootstrap.timepicker', []) } }); - secondsInputEl.bind('keydown', function(e) { + secondsInputEl.on('keydown', function(e) { if (!disabled) { if (e.which === 38) { // up e.preventDefault(); @@ -295,14 +300,23 @@ angular.module('ui.bootstrap.timepicker', []) ngModelCtrl.$setValidity('time', false); if (angular.isDefined(invalidHours)) { $scope.invalidHours = invalidHours; + if (hoursModelCtrl) { + hoursModelCtrl.$setValidity('hours', false); + } } if (angular.isDefined(invalidMinutes)) { $scope.invalidMinutes = invalidMinutes; + if (minutesModelCtrl) { + minutesModelCtrl.$setValidity('minutes', false); + } } if (angular.isDefined(invalidSeconds)) { $scope.invalidSeconds = invalidSeconds; + if (secondsModelCtrl) { + secondsModelCtrl.$setValidity('seconds', false); + } } }; @@ -325,7 +339,7 @@ angular.module('ui.bootstrap.timepicker', []) } }; - hoursInputEl.bind('blur', function(e) { + hoursInputEl.on('blur', function(e) { ngModelCtrl.$setTouched(); if (modelIsEmpty()) { makeValid(); @@ -357,7 +371,7 @@ angular.module('ui.bootstrap.timepicker', []) } }; - minutesInputEl.bind('blur', function(e) { + minutesInputEl.on('blur', function(e) { ngModelCtrl.$setTouched(); if (modelIsEmpty()) { makeValid(); @@ -383,7 +397,7 @@ angular.module('ui.bootstrap.timepicker', []) } }; - secondsInputEl.bind('blur', function(e) { + secondsInputEl.on('blur', function(e) { if (modelIsEmpty()) { makeValid(); } else if (!$scope.invalidSeconds && $scope.seconds < 10) { @@ -425,6 +439,18 @@ angular.module('ui.bootstrap.timepicker', []) } function makeValid() { + if (hoursModelCtrl) { + hoursModelCtrl.$setValidity('hours', true); + } + + if (minutesModelCtrl) { + minutesModelCtrl.$setValidity('minutes', true); + } + + if (secondsModelCtrl) { + secondsModelCtrl.$setValidity('seconds', true); + } + ngModelCtrl.$setValidity('time', true); $scope.invalidHours = false; $scope.invalidMinutes = false; @@ -547,9 +573,9 @@ angular.module('ui.bootstrap.timepicker', []) .directive('uibTimepicker', ['uibTimepickerConfig', function(uibTimepickerConfig) { return { require: ['uibTimepicker', '?^ngModel'], + restrict: 'A', controller: 'UibTimepickerController', controllerAs: 'timepicker', - replace: true, scope: {}, templateUrl: function(element, attrs) { return attrs.templateUrl || uibTimepickerConfig.templateUrl; diff --git a/src/tooltip/docs/demo.html b/src/tooltip/docs/demo.html index 4ccc41f512..3f97ca724e 100644 --- a/src/tooltip/docs/demo.html +++ b/src/tooltip/docs/demo.html @@ -29,7 +29,9 @@

                                                                                              - I can even contain HTML. Check me out! + I can even contain HTML as a + scope variable or + inline string

                                                                                              diff --git a/src/tooltip/docs/readme.md b/src/tooltip/docs/readme.md index d85fb47221..1bc4b04d44 100644 --- a/src/tooltip/docs/readme.md +++ b/src/tooltip/docs/readme.md @@ -118,3 +118,5 @@ For Safari 7+ support, if you want to use the **focus** `tooltip-trigger`, you n Click Me ``` + +For Safari (potentially all versions up to 9), there is an issue with the hover CSS selector when using multiple elements grouped close to each other that are using the tooltip - it is possible for multiple elements to gain the hover state when mousing between the elements quickly and exiting the container at the right time. See [issue #5445](https://github.com/angular-ui/bootstrap/issues/5445) for more details. diff --git a/src/tooltip/test/tooltip-template.spec.js b/src/tooltip/test/tooltip-template.spec.js index cec7d4b5cc..e474c6dbc3 100644 --- a/src/tooltip/test/tooltip-template.spec.js +++ b/src/tooltip/test/tooltip-template.spec.js @@ -75,6 +75,7 @@ describe('tooltip template', function() { it('should hide tooltip when template becomes empty', inject(function($timeout) { trigger(elm, 'mouseenter'); + $timeout.flush(0); expect(tooltipScope.isOpen).toBe(true); scope.templateUrl = ''; diff --git a/src/tooltip/test/tooltip.spec.js b/src/tooltip/test/tooltip.spec.js index ddb3a9a0c9..3058429dae 100644 --- a/src/tooltip/test/tooltip.spec.js +++ b/src/tooltip/test/tooltip.spec.js @@ -27,7 +27,7 @@ describe('tooltip', function() { })); afterEach(function() { - $document.off('keypress'); + $document.off('keyup'); }); function trigger(element, evt) { @@ -179,7 +179,6 @@ describe('tooltip', function() { 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); trigger(elm, 'mouseleave'); @@ -188,7 +187,6 @@ describe('tooltip', function() { trigger(elm, 'mouseenter'); ttScope = angular.element(elmBody.children()[1]).isolateScope(); - expect(ttScope.placement).toBe('top'); expect(ttScope.content).toBe(scope.tooltipMsg); })); @@ -297,10 +295,9 @@ describe('tooltip', function() { trigger(elm, 'mouseenter'); expect(tooltipScope.isOpen).toBe(false); - $timeout.flush(500); - expect(tooltipScope.isOpen).toBe(false); elmScope.disabled = true; elmScope.$digest(); + $timeout.flush(500); expect(tooltipScope.isOpen).toBe(false); }); @@ -345,22 +342,24 @@ describe('tooltip', function() { expect(tooltipScope.isOpen).toBe(true); expect(tooltipScope2.isOpen).toBe(true); - var evt = $.Event('keypress'); + var evt = $.Event('keyup'); evt.which = 27; $document.trigger(evt); tooltipScope.$digest(); tooltipScope2.$digest(); + $timeout.flush(); expect(tooltipScope.isOpen).toBe(true); expect(tooltipScope2.isOpen).toBe(false); - var evt2 = $.Event('keypress'); + var evt2 = $.Event('keyup'); evt2.which = 27; $document.trigger(evt2); tooltipScope.$digest(); tooltipScope2.$digest(); + $timeout.flush(500); expect(tooltipScope.isOpen).toBe(false); expect(tooltipScope2.isOpen).toBe(false); diff --git a/src/tooltip/test/tooltip2.spec.js b/src/tooltip/test/tooltip2.spec.js index e8df646479..81326e466b 100644 --- a/src/tooltip/test/tooltip2.spec.js +++ b/src/tooltip/test/tooltip2.spec.js @@ -1,5 +1,5 @@ describe('tooltip directive', function() { - var $rootScope, $compile, $document, $timeout; + var $rootScope, $compile, $document, $timeout, body, fragment; beforeEach(module('ui.bootstrap.tooltip')); beforeEach(module('uib/template/tooltip/tooltip-popup.html')); @@ -10,6 +10,8 @@ describe('tooltip directive', function() { $compile = _$compile_; $document = _$document_; $timeout = _$timeout_; + + body = $document.find('body'); })); beforeEach(function() { @@ -39,12 +41,13 @@ describe('tooltip directive', function() { afterEach(function() { $document.off('keypress'); + fragment.remove(); }); function compileTooltip(ttipMarkup) { - var fragment = $compile('

                                                                                              ' + ttipMarkup + '
                                                                                              ')($rootScope); + fragment = $compile('
                                                                                              ' + ttipMarkup + '
                                                                                              ')($rootScope); $rootScope.$digest(); - return fragment; + body.append(fragment); } function closeTooltip(hostEl, triggerEvt, shouldNotFlush) { @@ -62,7 +65,7 @@ describe('tooltip directive', function() { describe('basic scenarios with default options', function() { it('shows default tooltip on mouse enter and closes on mouse leave', function() { - var fragment = compileTooltip('Trigger here'); + compileTooltip('Trigger here'); trigger(fragment.find('span'), 'mouseenter'); expect(fragment).toHaveOpenTooltips(); @@ -72,16 +75,17 @@ describe('tooltip directive', function() { }); it('should not show a tooltip when its content is empty', function() { - var fragment = compileTooltip(''); + compileTooltip(''); trigger(fragment.find('span'), 'mouseenter'); expect(fragment).not.toHaveOpenTooltips(); }); it('should not show a tooltip when its content becomes empty', function() { $rootScope.content = 'some text'; - var fragment = compileTooltip(''); + compileTooltip(''); trigger(fragment.find('span'), 'mouseenter'); + $timeout.flush(0); expect(fragment).toHaveOpenTooltips(); $rootScope.content = ''; @@ -92,7 +96,7 @@ describe('tooltip directive', function() { it('should update tooltip when its content becomes empty', function() { $rootScope.content = 'some text'; - var fragment = compileTooltip(''); + compileTooltip(''); $rootScope.content = ''; $rootScope.$digest(); @@ -119,7 +123,7 @@ describe('tooltip directive', function() { describe(key, function() { describe('placement', function() { it('can specify an alternative, valid placement', function() { - var fragment = compileTooltip('Trigger here'); + compileTooltip('Trigger here'); trigger(fragment.find('span'), 'mouseenter'); var ttipElement = fragment.find('div.tooltip'); @@ -133,7 +137,7 @@ describe('tooltip directive', function() { describe('class', function() { it('can specify a custom class', function() { - var fragment = compileTooltip('Trigger here'); + compileTooltip('Trigger here'); trigger(fragment.find('span'), 'mouseenter'); var ttipElement = fragment.find('div.tooltip'); @@ -149,7 +153,7 @@ describe('tooltip directive', function() { }); it('should show even after close trigger is called multiple times - issue #1847', function() { - var fragment = compileTooltip('Trigger here'); + compileTooltip('Trigger here'); trigger(fragment.find('span'), 'mouseenter'); expect(fragment).toHaveOpenTooltips(); @@ -169,7 +173,7 @@ describe('tooltip directive', function() { }); it('should hide even after show trigger is called multiple times', function() { - var fragment = compileTooltip('Trigger here'); + compileTooltip('Trigger here'); trigger(fragment.find('span'), 'mouseenter'); trigger(fragment.find('span'), 'mouseenter'); @@ -179,7 +183,7 @@ describe('tooltip directive', function() { }); it('should not show tooltips element is disabled (button) - issue #3167', function() { - var fragment = compileTooltip(''); + compileTooltip(''); trigger(fragment.find('button'), 'mouseenter'); expect(fragment).toHaveOpenTooltips(); diff --git a/src/tooltip/tooltip.js b/src/tooltip/tooltip.js index cb3a508561..dd02a077a1 100644 --- a/src/tooltip/tooltip.js +++ b/src/tooltip/tooltip.js @@ -71,10 +71,10 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.s */ this.$get = ['$window', '$compile', '$timeout', '$document', '$uibPosition', '$interpolate', '$rootScope', '$parse', '$$stackedMap', function($window, $compile, $timeout, $document, $position, $interpolate, $rootScope, $parse, $$stackedMap) { var openedTooltips = $$stackedMap.createNew(); - $document.on('keypress', keypressListener); + $document.on('keyup', keypressListener); $rootScope.$on('$destroy', function() { - $document.off('keypress', keypressListener); + $document.off('keyup', keypressListener); }); function keypressListener(e) { @@ -82,7 +82,6 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.s var last = openedTooltips.top(); if (last) { last.value.close(); - openedTooltips.removeTop(); last = null; } } @@ -126,12 +125,11 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.s (options.useContentExp ? 'content-exp="contentExp()" ' : 'content="' + startSym + 'content' + endSym + '" ') + - 'placement="' + startSym + 'placement' + endSym + '" ' + - 'popup-class="' + startSym + 'popupClass' + endSym + '" ' + - 'animation="animation" ' + - 'is-open="isOpen" ' + 'origin-scope="origScope" ' + - 'class="uib-position-measure"' + + 'class="uib-position-measure ' + prefix + '" ' + + 'tooltip-animation-class="fade"' + + 'uib-tooltip-classes ' + + 'ng-class="{ in: isOpen }" ' + '>' + '
                                                                                              '; @@ -146,6 +144,7 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.s var showTimeout; var hideTimeout; var positionTimeout; + var adjustmentTimeout; var appendToBody = angular.isDefined(options.appendToBody) ? options.appendToBody : false; var triggers = getTriggers(undefined); var hasEnableExp = angular.isDefined(attrs[prefix + 'Enable']); @@ -163,11 +162,14 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.s if (!positionTimeout) { positionTimeout = $timeout(function() { var ttPosition = $position.positionElements(element, tooltip, ttScope.placement, appendToBody); + var initialHeight = angular.isDefined(tooltip.offsetHeight) ? tooltip.offsetHeight : tooltip.prop('offsetHeight'); + var elementPos = appendToBody ? $position.offset(element) : $position.position(element); tooltip.css({ top: ttPosition.top + 'px', left: ttPosition.left + 'px' }); + var placementClasses = ttPosition.placement.split('-'); - if (!tooltip.hasClass(ttPosition.placement.split('-')[0])) { + if (!tooltip.hasClass(placementClasses[0])) { tooltip.removeClass(lastPlacement.split('-')[0]); - tooltip.addClass(ttPosition.placement.split('-')[0]); + tooltip.addClass(placementClasses[0]); } if (!tooltip.hasClass(options.placementClassPrefix + ttPosition.placement)) { @@ -175,6 +177,15 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.s tooltip.addClass(options.placementClassPrefix + ttPosition.placement); } + adjustmentTimeout = $timeout(function() { + var currentHeight = angular.isDefined(tooltip.offsetHeight) ? tooltip.offsetHeight : tooltip.prop('offsetHeight'); + var adjustment = $position.adjustTop(placementClasses, elementPos, initialHeight, currentHeight); + if (adjustment) { + tooltip.css(adjustment); + } + adjustmentTimeout = null; + }, 0, false); + // first time through tt element will have the // uib-position-measure class or if the placement // has changed we need to position the arrow. @@ -197,9 +208,6 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.s // By default, the tooltip is not open. // TODO add ability to start tooltip opened ttScope.isOpen = false; - openedTooltips.add(ttScope, { - close: hide - }); function toggleTooltipBind() { if (!ttScope.isOpen) { @@ -326,6 +334,10 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.s } }); + openedTooltips.add(ttScope, { + close: hide + }); + prepObservers(); } @@ -336,8 +348,15 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.s if (tooltip) { tooltip.remove(); + tooltip = null; + if (adjustmentTimeout) { + $timeout.cancel(adjustmentTimeout); + } } + + openedTooltips.remove(ttScope); + if (tooltipLinkedScope) { tooltipLinkedScope.$destroy(); tooltipLinkedScope = null; @@ -477,6 +496,13 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.s } } + // KeyboardEvent handler to hide the tooltip on Escape key press + function hideOnEscapeKey(e) { + if (e.which === 27) { + hideTooltipBind(); + } + } + var unregisterTriggers = function() { triggers.show.forEach(function(trigger) { if (trigger === 'outsideClick') { @@ -485,6 +511,7 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.s element.off(trigger, showTooltipBind); element.off(trigger, toggleTooltipBind); } + element.off('keypress', hideOnEscapeKey); }); triggers.hide.forEach(function(trigger) { if (trigger === 'outsideClick') { @@ -524,12 +551,7 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.s element.on(trigger, showTooltipBind); element.on(triggers.hide[idx], hideTooltipBind); } - - element.on('keypress', function(e) { - if (e.which === 27) { - hideTooltipBind(); - } - }); + element.on('keypress', hideOnEscapeKey); }); } } @@ -553,7 +575,6 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.s scope.$on('$destroy', function onDestroyTooltip() { unregisterTriggers(); removeTooltip(); - openedTooltips.remove(ttScope); ttScope = null; }); }; @@ -656,7 +677,7 @@ function ($animate, $sce, $compile, $templateRequest) { element.addClass(scope.popupClass); } - if (scope.animation()) { + if (scope.animation) { element.addClass(attrs.tooltipAnimationClass); } } @@ -665,8 +686,8 @@ function ($animate, $sce, $compile, $templateRequest) { .directive('uibTooltipPopup', function() { return { - replace: true, - scope: { content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' }, + restrict: 'A', + scope: { content: '@' }, templateUrl: 'uib/template/tooltip/tooltip-popup.html' }; }) @@ -677,9 +698,8 @@ function ($animate, $sce, $compile, $templateRequest) { .directive('uibTooltipTemplatePopup', function() { return { - replace: true, - scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&', - originScope: '&' }, + restrict: 'A', + scope: { contentExp: '&', originScope: '&' }, templateUrl: 'uib/template/tooltip/tooltip-template-popup.html' }; }) @@ -692,8 +712,8 @@ function ($animate, $sce, $compile, $templateRequest) { .directive('uibTooltipHtmlPopup', function() { return { - replace: true, - scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&' }, + restrict: 'A', + scope: { contentExp: '&' }, templateUrl: 'uib/template/tooltip/tooltip-html-popup.html' }; }) diff --git a/src/typeahead/test/typeahead.spec.js b/src/typeahead/test/typeahead.spec.js index 4b5b44691a..4423b8a66e 100644 --- a/src/typeahead/test/typeahead.spec.js +++ b/src/typeahead/test/typeahead.spec.js @@ -313,6 +313,39 @@ describe('typeahead tests', function() { expect($scope.form.input.$error.parse).toBeFalsy(); }); + // fix for #6032 + it('should clear errors and refresh scope after blur for typeahead-editable="false"', function () { + var element = prepareInputEl( + '
                                                                                              ' + + '' + + '
                                                                                              '); + var inputEl = findInput(element); + + // first try + changeInputValueTo(element, 'not in matches'); + expect($scope.result).toEqual(undefined); + expect(inputEl.val()).toEqual('not in matches'); + expect(element.find('form')).toHaveClass('invalid'); + inputEl.blur(); + + expect(inputEl.val()).toEqual(''); // <-- input is reset + expect($scope.form.input.$error.editable).toBeFalsy(); + expect($scope.form.input.$error.parse).toBeFalsy(); + expect(element.find('form')).not.toHaveClass('invalid'); // <-- form has no error (it always works for some reason) + + // second try + changeInputValueTo(element, 'not in matches'); + expect($scope.result).toEqual(undefined); + expect(inputEl.val()).toEqual('not in matches'); + expect(element.find('form')).toHaveClass('invalid'); + inputEl.blur(); + + expect(inputEl.val()).toEqual(''); // <-- input is reset + expect($scope.form.input.$error.editable).toBeFalsy(); + expect($scope.form.input.$error.parse).toBeFalsy(); + expect(element.find('form')).not.toHaveClass('invalid'); // <-- form has no error (it didn't work prior to #6032 fix) + }); + it('should go through other validators after blur for typeahead-editable="false"', function () { var element = prepareInputEl( '
                                                                                              ' + diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 1e9c7245f3..71b807c817 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -5,7 +5,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap * Extracted to a separate service for ease of unit testing */ .factory('uibTypeaheadParser', ['$parse', function($parse) { - // 00000111000000000000022200000000000000003333333333333330000000000044000 + // 000001111111100000000000002222222200000000000000003333333333333330000000000044444444000 var TYPEAHEAD_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/; return { parse: function(input) { @@ -94,7 +94,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap var invokeModelSetter = $parse(attrs.ngModel + '($$$p)'); var $setModelValue = function(scope, newValue) { if (angular.isFunction(parsedModel(originalScope)) && - ngModelOptions && ngModelOptions.$options && ngModelOptions.$options.getterSetter) { + ngModelOptions.getOption('getterSetter')) { return invokeModelSetter(scope, {$$$p: newValue}); } @@ -430,7 +430,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap } }); - element.bind('focus', function (evt) { + element.on('focus', function (evt) { hasFocus = true; if (minLength === 0 && !modelCtrl.$viewValue) { $timeout(function() { @@ -439,7 +439,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap } }); - element.bind('blur', function(evt) { + element.on('blur', function(evt) { if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) { selected = true; scope.$apply(function() { @@ -454,9 +454,11 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap } if (!isEditable && modelCtrl.$error.editable) { modelCtrl.$setViewValue(); - // Reset validity as we are clearing - modelCtrl.$setValidity('editable', true); - modelCtrl.$setValidity('parse', true); + scope.$apply(function() { + // Reset validity as we are clearing + modelCtrl.$setValidity('editable', true); + modelCtrl.$setValidity('parse', true); + }); element.val(''); } hasFocus = false; @@ -505,11 +507,11 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap element.after($popup); } - this.init = function(_modelCtrl, _ngModelOptions) { + this.init = function(_modelCtrl) { modelCtrl = _modelCtrl; - ngModelOptions = _ngModelOptions; + ngModelOptions = extractOptions(modelCtrl); - scope.debounceUpdate = modelCtrl.$options && $parse(modelCtrl.$options.debounce)(originalScope); + scope.debounceUpdate = $parse(ngModelOptions.getOption('debounce'))(originalScope); //plug into $parsers pipeline to open a typeahead on view changes initiated from DOM //$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue @@ -569,14 +571,32 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap return candidateViewValue !== emptyViewValue ? candidateViewValue : modelValue; }); }; + + function extractOptions(ngModelCtrl) { + var ngModelOptions; + + if (angular.version.minor < 6) { // in angular < 1.6 $options could be missing + // guarantee a value + ngModelOptions = ngModelCtrl.$options || {}; + + // mimic 1.6+ api + ngModelOptions.getOption = function (key) { + return ngModelOptions[key]; + }; + } else { // in angular >=1.6 $options is always present + ngModelOptions = ngModelCtrl.$options; + } + + return ngModelOptions; + } }]) .directive('uibTypeahead', function() { return { controller: 'UibTypeaheadController', - require: ['ngModel', '^?ngModelOptions', 'uibTypeahead'], + require: ['ngModel', 'uibTypeahead'], link: function(originalScope, element, attrs, ctrls) { - ctrls[2].init(ctrls[0], ctrls[1]); + ctrls[1].init(ctrls[0]); } }; }) diff --git a/template/accordion/accordion-group.html b/template/accordion/accordion-group.html index 728c674c1c..8cae8c023b 100644 --- a/template/accordion/accordion-group.html +++ b/template/accordion/accordion-group.html @@ -1,10 +1,8 @@ -
                                                                                              - -
                                                                                              -
                                                                                              -
                                                                                              + +
                                                                                              +
                                                                                              diff --git a/template/alert/alert.html b/template/alert/alert.html index 0885587d98..b5bade4b0d 100644 --- a/template/alert/alert.html +++ b/template/alert/alert.html @@ -1,7 +1,5 @@ - + +
                                                                                              diff --git a/template/carousel/carousel.html b/template/carousel/carousel.html index dc98a5aaed..1d76a98958 100644 --- a/template/carousel/carousel.html +++ b/template/carousel/carousel.html @@ -1,16 +1,14 @@ - + + + + previous + + + + next + + diff --git a/template/carousel/slide.html b/template/carousel/slide.html index 522013922a..d2938998e9 100644 --- a/template/carousel/slide.html +++ b/template/carousel/slide.html @@ -1,3 +1 @@ -
                                                                                              +
                                                                                              diff --git a/template/datepicker/datepicker.html b/template/datepicker/datepicker.html index 6099c285fb..8e2d2eb806 100644 --- a/template/datepicker/datepicker.html +++ b/template/datepicker/datepicker.html @@ -1,5 +1,5 @@ -
                                                                                              - - - +
                                                                                              +
                                                                                              +
                                                                                              +
                                                                                              diff --git a/template/datepicker/day.html b/template/datepicker/day.html index 69e7007684..8a81ad3671 100644 --- a/template/datepicker/day.html +++ b/template/datepicker/day.html @@ -1,9 +1,9 @@ - +
                                                                                              - + - + @@ -11,7 +11,7 @@ - +
                                                                                              {{ weekNumbers[$index] }} + - - - + + + - +
                                                                                              diff --git a/template/datepicker/year.html b/template/datepicker/year.html index 05a0077773..f50bfddd7e 100644 --- a/template/datepicker/year.html +++ b/template/datepicker/year.html @@ -1,13 +1,13 @@ - +
                                                                                              - + - + - +
                                                                                              diff --git a/template/datepickerPopup/popup.html b/template/datepickerPopup/popup.html index 0a9b73d706..b9a501b54d 100644 --- a/template/datepickerPopup/popup.html +++ b/template/datepickerPopup/popup.html @@ -1,12 +1,10 @@ -
                                                                                              - -
                                                                                              + diff --git a/template/modal/backdrop.html b/template/modal/backdrop.html deleted file mode 100644 index 1eeaa1a996..0000000000 --- a/template/modal/backdrop.html +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/template/modal/window.html b/template/modal/window.html index e28a8e8373..9850c1b61e 100644 --- a/template/modal/window.html +++ b/template/modal/window.html @@ -1,6 +1 @@ - + diff --git a/template/pager/pager.html b/template/pager/pager.html index 46f227faac..45c11150b2 100644 --- a/template/pager/pager.html +++ b/template/pager/pager.html @@ -1,4 +1,2 @@ - +
                                                                                            • {{::getText('previous')}}
                                                                                            • +
                                                                                            • {{::getText('next')}}
                                                                                            • diff --git a/template/pagination/pagination.html b/template/pagination/pagination.html index f55a7b6b92..cf52a94e8d 100644 --- a/template/pagination/pagination.html +++ b/template/pagination/pagination.html @@ -1,7 +1,5 @@ - + + + + + diff --git a/template/popover/popover-html.html b/template/popover/popover-html.html index e127a0f2aa..722fd14a72 100644 --- a/template/popover/popover-html.html +++ b/template/popover/popover-html.html @@ -1,11 +1,6 @@ -
                                                                                              -
                                                                                              +
                                                                                              -
                                                                                              -

                                                                                              -
                                                                                              -
                                                                                              +
                                                                                              +

                                                                                              +
                                                                                              diff --git a/template/popover/popover-template.html b/template/popover/popover-template.html index 52d2c2fde6..6053b807c9 100644 --- a/template/popover/popover-template.html +++ b/template/popover/popover-template.html @@ -1,13 +1,8 @@ -
                                                                                              -
                                                                                              +
                                                                                              -
                                                                                              -

                                                                                              -
                                                                                              -
                                                                                              +
                                                                                              +

                                                                                              +
                                                                                              diff --git a/template/popover/popover.html b/template/popover/popover.html index a5cbdb5a06..60286c95c1 100644 --- a/template/popover/popover.html +++ b/template/popover/popover.html @@ -1,11 +1,6 @@ -
                                                                                              -
                                                                                              +
                                                                                              -
                                                                                              -

                                                                                              -
                                                                                              -
                                                                                              +
                                                                                              +

                                                                                              +
                                                                                              diff --git a/template/timepicker/timepicker.html b/template/timepicker/timepicker.html index 4acf647f22..076bb2a30e 100644 --- a/template/timepicker/timepicker.html +++ b/template/timepicker/timepicker.html @@ -1,11 +1,11 @@ - + - + - + @@ -23,11 +23,11 @@ - + - + - + diff --git a/template/tooltip/tooltip-html-popup.html b/template/tooltip/tooltip-html-popup.html index 6ecef364c3..aaa1e81db2 100644 --- a/template/tooltip/tooltip-html-popup.html +++ b/template/tooltip/tooltip-html-popup.html @@ -1,7 +1,2 @@ -
                                                                                              -
                                                                                              -
                                                                                              -
                                                                                              +
                                                                                              +
                                                                                              diff --git a/template/tooltip/tooltip-popup.html b/template/tooltip/tooltip-popup.html index dbbda48517..e2dfc0873d 100644 --- a/template/tooltip/tooltip-popup.html +++ b/template/tooltip/tooltip-popup.html @@ -1,7 +1,2 @@ -
                                                                                              -
                                                                                              -
                                                                                              -
                                                                                              +
                                                                                              +
                                                                                              diff --git a/template/tooltip/tooltip-template-popup.html b/template/tooltip/tooltip-template-popup.html index be8e97950f..5059765954 100644 --- a/template/tooltip/tooltip-template-popup.html +++ b/template/tooltip/tooltip-template-popup.html @@ -1,9 +1,4 @@ -
                                                                                              -
                                                                                              -
                                                                                              -
                                                                                              +
                                                                                              +