@@ -0,0 +1,6 @@
|
||||
*.html eol=lf
|
||||
*.css eol=lf
|
||||
*.js eol=lf
|
||||
*.md eol=lf
|
||||
*.json eol=lf
|
||||
*.yml eol=lf
|
||||
+10
-10
@@ -1,11 +1,11 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "0.8"
|
||||
|
||||
before_script:
|
||||
- export DISPLAY=:99.0
|
||||
- sh -e /etc/init.d/xvfb start
|
||||
- npm install --quiet -g grunt-cli karma
|
||||
- npm install
|
||||
|
||||
language: node_js
|
||||
node_js:
|
||||
- "0.8"
|
||||
|
||||
before_script:
|
||||
- export DISPLAY=:99.0
|
||||
- sh -e /etc/init.d/xvfb start
|
||||
- npm install --quiet -g grunt-cli karma
|
||||
- npm install
|
||||
|
||||
script: grunt
|
||||
+93
-93
@@ -1,93 +1,93 @@
|
||||
# 0.3.0 (2013-04-30)
|
||||
|
||||
## Features
|
||||
|
||||
- **progressbar:**
|
||||
- add progressbar directive ([261f2072](https://github.com/angular-ui/bootstrap/commit/261f2072))
|
||||
- **rating:**
|
||||
- add rating directive ([6b5e6369](https://github.com/angular-ui/bootstrap/commit/6b5e6369))
|
||||
- **typeahead:**
|
||||
- support the editable property ([a40c3fbe](https://github.com/angular-ui/bootstrap/commit/a40c3fbe))
|
||||
- support typeahead-loading bindable expression ([b58c9c88](https://github.com/angular-ui/bootstrap/commit/b58c9c88))
|
||||
- **tooltip:**
|
||||
- added popup-delay option ([a79a2ba8](https://github.com/angular-ui/bootstrap/commit/a79a2ba8))
|
||||
- added appendToBody to $tooltip ([1ee467f8](https://github.com/angular-ui/bootstrap/commit/1ee467f8))
|
||||
- added tooltip-html-unsafe directive ([45ed2805](https://github.com/angular-ui/bootstrap/commit/45ed2805))
|
||||
- support for custom triggers ([b1ba821b](https://github.com/angular-ui/bootstrap/commit/b1ba821b))
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **alert:**
|
||||
- don't show close button if no close callback specified ([c2645f4a](https://github.com/angular-ui/bootstrap/commit/c2645f4a))
|
||||
- **carousel:**
|
||||
- Hide navigation indicators if only one slide ([aedc0565](https://github.com/angular-ui/bootstrap/commit/aedc0565))
|
||||
- **collapse:**
|
||||
- remove reference to msTransition for IE10 ([55437b16](https://github.com/angular-ui/bootstrap/commit/55437b16))
|
||||
- **dialog:**
|
||||
- set _open to false on init ([dcc9ef31](https://github.com/angular-ui/bootstrap/commit/dcc9ef31))
|
||||
- close dialog on location change ([474ce52e](https://github.com/angular-ui/bootstrap/commit/474ce52e))
|
||||
- IE8 fix to not set data() against text nodes ([a6c540e5](https://github.com/angular-ui/bootstrap/commit/a6c540e5))
|
||||
- fix $apply in progres on $location change ([77e6acb9](https://github.com/angular-ui/bootstrap/commit/77e6acb9))
|
||||
- **tabs:**
|
||||
- remove superfluous href from tabs template ([38c1badd](https://github.com/angular-ui/bootstrap/commit/38c1badd))
|
||||
- **tooltip:**
|
||||
- fix positioning issues in tooltips and popovers ([6458f487](https://github.com/angular-ui/bootstrap/commit/6458f487))
|
||||
- **typeahead:**
|
||||
- close matches popup on click outside typeahead ([acca7dcd](https://github.com/angular-ui/bootstrap/commit/acca7dcd))
|
||||
- stop keydown event propagation when ESC pressed to discard matches ([22a00cd0](https://github.com/angular-ui/bootstrap/commit/22a00cd0))
|
||||
- correctly render initial model value ([929a46fa](https://github.com/angular-ui/bootstrap/commit/929a46fa))
|
||||
- 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)
|
||||
|
||||
## Features
|
||||
|
||||
- **dialog:**
|
||||
- Make $dialog 'resolve' property to work the same way of $routeProvider.when ([739f86f](https://github.com/angular-ui/bootstrap/commit/739f86f))
|
||||
- **modal:**
|
||||
- allow global override of modal options ([acaf72b](https://github.com/angular-ui/bootstrap/commit/acaf72b))
|
||||
- **buttons:**
|
||||
- add checkbox and radio buttons ([571ccf4](https://github.com/angular-ui/bootstrap/commit/571ccf4))
|
||||
- **carousel:**
|
||||
- add slide indicators ([3b677ee](https://github.com/angular-ui/bootstrap/commit/3b677ee))
|
||||
- **typeahead:**
|
||||
- add typeahead directive ([6a97da2](https://github.com/angular-ui/bootstrap/commit/6a97da2))
|
||||
- **accordion:**
|
||||
- enable HTML in accordion headings ([3afcaa4](https://github.com/angular-ui/bootstrap/commit/3afcaa4))
|
||||
- **pagination:**
|
||||
- add first/last link & constant congif options ([0ff0454](https://github.com/angular-ui/bootstrap/commit/0ff0454))
|
||||
|
||||
## Bug fixes
|
||||
|
||||
- **dialog:**
|
||||
- update resolve section to new syntax ([1f87486](https://github.com/angular-ui/bootstrap/commit/1f87486))
|
||||
- $compile entire modal ([7575b3c](https://github.com/angular-ui/bootstrap/commit/7575b3c))
|
||||
- **tooltip:**
|
||||
- don't show tooltips if there is no content to show ([030901e](https://github.com/angular-ui/bootstrap/commit/030901e))
|
||||
- fix placement issues ([a2bbf4d](https://github.com/angular-ui/bootstrap/commit/a2bbf4d))
|
||||
- **collapse:**
|
||||
- Avoids fixed height on collapse ([ff5d119](https://github.com/angular-ui/bootstrap/commit/ff5d119))
|
||||
- **accordion:**
|
||||
- fix minification issues ([f4da4d6](https://github.com/angular-ui/bootstrap/commit/f4da4d6))
|
||||
- **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_.
|
||||
|
||||
## Features
|
||||
|
||||
Version `0.1.0` was released with the following directives:
|
||||
|
||||
* accordion
|
||||
* alert
|
||||
* carousel
|
||||
* dialog
|
||||
* dropdownToggle
|
||||
* modal
|
||||
* pagination
|
||||
* popover
|
||||
* tabs
|
||||
* tooltip
|
||||
# 0.3.0 (2013-04-30)
|
||||
|
||||
## Features
|
||||
|
||||
- **progressbar:**
|
||||
- add progressbar directive ([261f2072](https://github.com/angular-ui/bootstrap/commit/261f2072))
|
||||
- **rating:**
|
||||
- add rating directive ([6b5e6369](https://github.com/angular-ui/bootstrap/commit/6b5e6369))
|
||||
- **typeahead:**
|
||||
- support the editable property ([a40c3fbe](https://github.com/angular-ui/bootstrap/commit/a40c3fbe))
|
||||
- support typeahead-loading bindable expression ([b58c9c88](https://github.com/angular-ui/bootstrap/commit/b58c9c88))
|
||||
- **tooltip:**
|
||||
- added popup-delay option ([a79a2ba8](https://github.com/angular-ui/bootstrap/commit/a79a2ba8))
|
||||
- added appendToBody to $tooltip ([1ee467f8](https://github.com/angular-ui/bootstrap/commit/1ee467f8))
|
||||
- added tooltip-html-unsafe directive ([45ed2805](https://github.com/angular-ui/bootstrap/commit/45ed2805))
|
||||
- support for custom triggers ([b1ba821b](https://github.com/angular-ui/bootstrap/commit/b1ba821b))
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **alert:**
|
||||
- don't show close button if no close callback specified ([c2645f4a](https://github.com/angular-ui/bootstrap/commit/c2645f4a))
|
||||
- **carousel:**
|
||||
- Hide navigation indicators if only one slide ([aedc0565](https://github.com/angular-ui/bootstrap/commit/aedc0565))
|
||||
- **collapse:**
|
||||
- remove reference to msTransition for IE10 ([55437b16](https://github.com/angular-ui/bootstrap/commit/55437b16))
|
||||
- **dialog:**
|
||||
- set _open to false on init ([dcc9ef31](https://github.com/angular-ui/bootstrap/commit/dcc9ef31))
|
||||
- close dialog on location change ([474ce52e](https://github.com/angular-ui/bootstrap/commit/474ce52e))
|
||||
- IE8 fix to not set data() against text nodes ([a6c540e5](https://github.com/angular-ui/bootstrap/commit/a6c540e5))
|
||||
- fix $apply in progres on $location change ([77e6acb9](https://github.com/angular-ui/bootstrap/commit/77e6acb9))
|
||||
- **tabs:**
|
||||
- remove superfluous href from tabs template ([38c1badd](https://github.com/angular-ui/bootstrap/commit/38c1badd))
|
||||
- **tooltip:**
|
||||
- fix positioning issues in tooltips and popovers ([6458f487](https://github.com/angular-ui/bootstrap/commit/6458f487))
|
||||
- **typeahead:**
|
||||
- close matches popup on click outside typeahead ([acca7dcd](https://github.com/angular-ui/bootstrap/commit/acca7dcd))
|
||||
- stop keydown event propagation when ESC pressed to discard matches ([22a00cd0](https://github.com/angular-ui/bootstrap/commit/22a00cd0))
|
||||
- correctly render initial model value ([929a46fa](https://github.com/angular-ui/bootstrap/commit/929a46fa))
|
||||
- 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)
|
||||
|
||||
## Features
|
||||
|
||||
- **dialog:**
|
||||
- Make $dialog 'resolve' property to work the same way of $routeProvider.when ([739f86f](https://github.com/angular-ui/bootstrap/commit/739f86f))
|
||||
- **modal:**
|
||||
- allow global override of modal options ([acaf72b](https://github.com/angular-ui/bootstrap/commit/acaf72b))
|
||||
- **buttons:**
|
||||
- add checkbox and radio buttons ([571ccf4](https://github.com/angular-ui/bootstrap/commit/571ccf4))
|
||||
- **carousel:**
|
||||
- add slide indicators ([3b677ee](https://github.com/angular-ui/bootstrap/commit/3b677ee))
|
||||
- **typeahead:**
|
||||
- add typeahead directive ([6a97da2](https://github.com/angular-ui/bootstrap/commit/6a97da2))
|
||||
- **accordion:**
|
||||
- enable HTML in accordion headings ([3afcaa4](https://github.com/angular-ui/bootstrap/commit/3afcaa4))
|
||||
- **pagination:**
|
||||
- add first/last link & constant congif options ([0ff0454](https://github.com/angular-ui/bootstrap/commit/0ff0454))
|
||||
|
||||
## Bug fixes
|
||||
|
||||
- **dialog:**
|
||||
- update resolve section to new syntax ([1f87486](https://github.com/angular-ui/bootstrap/commit/1f87486))
|
||||
- $compile entire modal ([7575b3c](https://github.com/angular-ui/bootstrap/commit/7575b3c))
|
||||
- **tooltip:**
|
||||
- don't show tooltips if there is no content to show ([030901e](https://github.com/angular-ui/bootstrap/commit/030901e))
|
||||
- fix placement issues ([a2bbf4d](https://github.com/angular-ui/bootstrap/commit/a2bbf4d))
|
||||
- **collapse:**
|
||||
- Avoids fixed height on collapse ([ff5d119](https://github.com/angular-ui/bootstrap/commit/ff5d119))
|
||||
- **accordion:**
|
||||
- fix minification issues ([f4da4d6](https://github.com/angular-ui/bootstrap/commit/f4da4d6))
|
||||
- **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_.
|
||||
|
||||
## Features
|
||||
|
||||
Version `0.1.0` was released with the following directives:
|
||||
|
||||
* accordion
|
||||
* alert
|
||||
* carousel
|
||||
* dialog
|
||||
* dropdownToggle
|
||||
* modal
|
||||
* pagination
|
||||
* popover
|
||||
* tabs
|
||||
* tooltip
|
||||
|
||||
+12
-12
@@ -1,12 +1,12 @@
|
||||
We are always looking for the quality contributions and will be happy to accept your Pull Requests as long as those adhere to some basic rules:
|
||||
|
||||
* Please make sure that your contribution fits well in the project's context:
|
||||
* we are aiming at rebuilding boostrap directives in pure AngularJS, without any dependencies on any external JavaScript library;
|
||||
* the only dependency should be boostrap CSS and its markup structure;
|
||||
* directives should be html-agnostic as much as possible which in practice means:
|
||||
* templates should be referred to using the `templateUrl` property
|
||||
* it should be easy to change a default template to a custom one
|
||||
* directives shouldn't manipulate DOM structure directly (when possible)
|
||||
* Please assure that you are submitting quality code, specifically make sure that:
|
||||
* your directive has accompanying tests and all the tests are passing; don't hesitate to contact us (angular-ui@googlegroups.com) if you ned any help with unit testing
|
||||
* your PR doesn't break the build; check the Travis-CI build status after opening a PR and push corrective commits if anything goes wrong
|
||||
We are always looking for the quality contributions and will be happy to accept your Pull Requests as long as those adhere to some basic rules:
|
||||
|
||||
* Please make sure that your contribution fits well in the project's context:
|
||||
* we are aiming at rebuilding boostrap directives in pure AngularJS, without any dependencies on any external JavaScript library;
|
||||
* the only dependency should be boostrap CSS and its markup structure;
|
||||
* directives should be html-agnostic as much as possible which in practice means:
|
||||
* templates should be referred to using the `templateUrl` property
|
||||
* it should be easy to change a default template to a custom one
|
||||
* directives shouldn't manipulate DOM structure directly (when possible)
|
||||
* Please assure that you are submitting quality code, specifically make sure that:
|
||||
* your directive has accompanying tests and all the tests are passing; don't hesitate to contact us (angular-ui@googlegroups.com) if you ned any help with unit testing
|
||||
* your PR doesn't break the build; check the Travis-CI build status after opening a PR and push corrective commits if anything goes wrong
|
||||
|
||||
@@ -11,6 +11,8 @@ module.exports = function(grunt) {
|
||||
grunt.loadNpmTasks('grunt-karma');
|
||||
|
||||
// Project configuration.
|
||||
grunt.util.linefeed = '\n';
|
||||
|
||||
grunt.initConfig({
|
||||
ngversion: '1.0.5',
|
||||
bsversion: '2.3.1',
|
||||
|
||||
+28
-28
@@ -1,28 +1,28 @@
|
||||
# <%= version%> (<%= today%>)
|
||||
|
||||
## Features
|
||||
|
||||
<% _(changelog.feat).forEach(function(changes, component) { %>
|
||||
- **<%= component%>:**
|
||||
<% changes.forEach(function(change) { %>
|
||||
- <%= change.msg%> ([<%= change.sha1%>](https://github.com/angular-ui/bootstrap/commit/<%= change.sha1%>))
|
||||
<% }) %>
|
||||
<% }) %>
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
<% _(changelog.fix).forEach(function(changes, component) { %>
|
||||
- **<%= component%>:**
|
||||
<% changes.forEach(function(change) { %>
|
||||
- <%= change.msg%> ([<%= change.sha1%>](https://github.com/angular-ui/bootstrap/commit/<%= change.sha1%>))
|
||||
<% }) %>
|
||||
<% }) %>
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
<% _(changelog.breaking).forEach(function(changes, component) { %>
|
||||
- **<%= component%>:**
|
||||
<% changes.forEach(function(change) { %>
|
||||
<%= change.msg%>
|
||||
<% }) %>
|
||||
<% }) %>
|
||||
# <%= version%> (<%= today%>)
|
||||
|
||||
## Features
|
||||
|
||||
<% _(changelog.feat).forEach(function(changes, component) { %>
|
||||
- **<%= component%>:**
|
||||
<% changes.forEach(function(change) { %>
|
||||
- <%= change.msg%> ([<%= change.sha1%>](https://github.com/angular-ui/bootstrap/commit/<%= change.sha1%>))
|
||||
<% }) %>
|
||||
<% }) %>
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
<% _(changelog.fix).forEach(function(changes, component) { %>
|
||||
- **<%= component%>:**
|
||||
<% changes.forEach(function(change) { %>
|
||||
- <%= change.msg%> ([<%= change.sha1%>](https://github.com/angular-ui/bootstrap/commit/<%= change.sha1%>))
|
||||
<% }) %>
|
||||
<% }) %>
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
<% _(changelog.breaking).forEach(function(changes, component) { %>
|
||||
- **<%= component%>:**
|
||||
<% changes.forEach(function(change) { %>
|
||||
<%= change.msg%>
|
||||
<% }) %>
|
||||
<% }) %>
|
||||
|
||||
+91
-91
@@ -1,92 +1,92 @@
|
||||
body {
|
||||
opacity: 1;
|
||||
-webkit-transition: opacity 1s ease;
|
||||
-moz-transition: opacity 1s ease;
|
||||
transition: opacity 1s;
|
||||
}
|
||||
|
||||
.ng-cloak {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
section {
|
||||
padding-top: 30px;
|
||||
}
|
||||
|
||||
.page-header h1 > small > a {
|
||||
color: #999;
|
||||
}
|
||||
.page-header h1 > small > a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 30px 0;
|
||||
margin-top: 70px;
|
||||
border-top: 1px solid #e5e5e5;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.hero-unit {
|
||||
position: relative;
|
||||
padding: 40px 0;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
text-shadow: 0 1px 3px rgba(0,0,0,.4), 0 0 30px rgba(0,0,0,.075);
|
||||
background: #020031;
|
||||
background: -moz-linear-gradient(45deg, #020031 0%, #6d3353 100%);
|
||||
background: -webkit-gradient(linear, left bottom, right top, color-stop(0%,#020031), color-stop(100%,#6d3353));
|
||||
background: -webkit-linear-gradient(45deg, #020031 0%,#6d3353 100%);
|
||||
background: -o-linear-gradient(45deg, #020031 0%,#6d3353 100%);
|
||||
background: -ms-linear-gradient(45deg, #020031 0%,#6d3353 100%);
|
||||
background: linear-gradient(45deg, #020031 0%,#6d3353 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#020031', endColorstr='#6d3353',GradientType=1 );
|
||||
-webkit-box-shadow: inset 0 3px 7px rgba(0,0,0,.2), inset 0 -3px 7px rgba(0,0,0,.2);
|
||||
-moz-box-shadow: inset 0 3px 7px rgba(0,0,0,.2), inset 0 -3px 7px rgba(0,0,0,.2);
|
||||
box-shadow: inset 0 3px 7px rgba(0,0,0,.2), inset 0 -3px 7px rgba(0,0,0,.2);
|
||||
border-radius: 0;
|
||||
-moz-border-radius: 0;
|
||||
-webkit-border-radius: 0;
|
||||
-o-border-radius: 0;
|
||||
}
|
||||
.hero-unit .btn, .pagination-centered .btn {
|
||||
float: none;
|
||||
font-weight: normal;
|
||||
}
|
||||
.hero-unit p {
|
||||
margin: 1em 0;
|
||||
}
|
||||
.bs-docs-social {
|
||||
margin-top: 1em;
|
||||
padding: 15px 0;
|
||||
text-align: center;
|
||||
background-color: rgba(245,245,245,0.3);
|
||||
border-top: 1px solid rgba(255,255,255,0.3);
|
||||
border-bottom: 1px solid rgba(221,221,221,0.3);
|
||||
}
|
||||
.bs-docs-social-buttons {
|
||||
margin-left: 0;
|
||||
margin-bottom: 0;
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.bs-docs-social-buttons li {
|
||||
display: inline-block;
|
||||
padding: 5px 8px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.icon-github {
|
||||
background: no-repeat url('github-16px.png');
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
/* Not enough room on mobile for markup tab, js tab, and plunk btn.
|
||||
And no one cares about plunk button on a phone anyway */
|
||||
@media only screen and (max-device-width: 480px) {
|
||||
#plunk-btn {
|
||||
display: none;
|
||||
}
|
||||
body {
|
||||
opacity: 1;
|
||||
-webkit-transition: opacity 1s ease;
|
||||
-moz-transition: opacity 1s ease;
|
||||
transition: opacity 1s;
|
||||
}
|
||||
|
||||
.ng-cloak {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
section {
|
||||
padding-top: 30px;
|
||||
}
|
||||
|
||||
.page-header h1 > small > a {
|
||||
color: #999;
|
||||
}
|
||||
.page-header h1 > small > a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 30px 0;
|
||||
margin-top: 70px;
|
||||
border-top: 1px solid #e5e5e5;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.hero-unit {
|
||||
position: relative;
|
||||
padding: 40px 0;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
text-shadow: 0 1px 3px rgba(0,0,0,.4), 0 0 30px rgba(0,0,0,.075);
|
||||
background: #020031;
|
||||
background: -moz-linear-gradient(45deg, #020031 0%, #6d3353 100%);
|
||||
background: -webkit-gradient(linear, left bottom, right top, color-stop(0%,#020031), color-stop(100%,#6d3353));
|
||||
background: -webkit-linear-gradient(45deg, #020031 0%,#6d3353 100%);
|
||||
background: -o-linear-gradient(45deg, #020031 0%,#6d3353 100%);
|
||||
background: -ms-linear-gradient(45deg, #020031 0%,#6d3353 100%);
|
||||
background: linear-gradient(45deg, #020031 0%,#6d3353 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#020031', endColorstr='#6d3353',GradientType=1 );
|
||||
-webkit-box-shadow: inset 0 3px 7px rgba(0,0,0,.2), inset 0 -3px 7px rgba(0,0,0,.2);
|
||||
-moz-box-shadow: inset 0 3px 7px rgba(0,0,0,.2), inset 0 -3px 7px rgba(0,0,0,.2);
|
||||
box-shadow: inset 0 3px 7px rgba(0,0,0,.2), inset 0 -3px 7px rgba(0,0,0,.2);
|
||||
border-radius: 0;
|
||||
-moz-border-radius: 0;
|
||||
-webkit-border-radius: 0;
|
||||
-o-border-radius: 0;
|
||||
}
|
||||
.hero-unit .btn, .pagination-centered .btn {
|
||||
float: none;
|
||||
font-weight: normal;
|
||||
}
|
||||
.hero-unit p {
|
||||
margin: 1em 0;
|
||||
}
|
||||
.bs-docs-social {
|
||||
margin-top: 1em;
|
||||
padding: 15px 0;
|
||||
text-align: center;
|
||||
background-color: rgba(245,245,245,0.3);
|
||||
border-top: 1px solid rgba(255,255,255,0.3);
|
||||
border-bottom: 1px solid rgba(221,221,221,0.3);
|
||||
}
|
||||
.bs-docs-social-buttons {
|
||||
margin-left: 0;
|
||||
margin-bottom: 0;
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.bs-docs-social-buttons li {
|
||||
display: inline-block;
|
||||
padding: 5px 8px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.icon-github {
|
||||
background: no-repeat url('github-16px.png');
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
/* Not enough room on mobile for markup tab, js tab, and plunk btn.
|
||||
And no one cares about plunk button on a phone anyway */
|
||||
@media only screen and (max-device-width: 480px) {
|
||||
#plunk-btn {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
+58
-58
@@ -1,58 +1,58 @@
|
||||
angular.module('plunker', [])
|
||||
|
||||
.factory('plunkGenerator', function ($document) {
|
||||
|
||||
return function (ngVersion, bsVersion, version, module, content) {
|
||||
|
||||
var form = angular.element('<form style="display: none;" method="post" action="http://plnkr.co/edit/?p=preview" target="_blank"></form>');
|
||||
var addField = function (name, value) {
|
||||
var input = angular.element('<input type="hidden" name="' + name + '">');
|
||||
input.attr('value', value);
|
||||
form.append(input);
|
||||
};
|
||||
|
||||
var indexContent = function (content, version) {
|
||||
return '<!doctype html>\n' +
|
||||
'<html ng-app="plunker">\n' +
|
||||
' <head>\n' +
|
||||
' <script src="http://ajax.googleapis.com/ajax/libs/angularjs/'+ngVersion+'/angular.js"></script>\n' +
|
||||
' <script src="http://angular-ui.github.io/bootstrap/ui-bootstrap-tpls-'+version+'.js"></script>\n' +
|
||||
' <script src="example.js"></script>\n' +
|
||||
' <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/'+bsVersion+'/css/bootstrap-combined.min.css" rel="stylesheet">\n' +
|
||||
' </head>\n' +
|
||||
' <body>\n\n' +
|
||||
content + '\n' +
|
||||
' </body>\n' +
|
||||
'</html>\n';
|
||||
};
|
||||
|
||||
var scriptContent = function(content) {
|
||||
return "angular.module('plunker', ['ui.bootstrap']);" + "\n" + content;
|
||||
};
|
||||
|
||||
addField('description', 'http://angular-ui.github.io/bootstrap/');
|
||||
addField('files[index.html]', indexContent(content.markup, version));
|
||||
addField('files[example.js]', scriptContent(content.javascript));
|
||||
|
||||
$document.find('body').append(form);
|
||||
form[0].submit();
|
||||
form.remove();
|
||||
};
|
||||
})
|
||||
|
||||
.controller('PlunkerCtrl', function ($scope, plunkGenerator) {
|
||||
|
||||
$scope.content = {};
|
||||
|
||||
$scope.edit = function (ngVersion, bsVersion, version, module) {
|
||||
plunkGenerator(ngVersion, bsVersion, version, module, $scope.content);
|
||||
};
|
||||
})
|
||||
|
||||
.directive('plunkerContent', function () {
|
||||
return {
|
||||
link:function (scope, element, attrs) {
|
||||
scope.$parent.content[attrs.plunkerContent] = element.text();
|
||||
}
|
||||
}
|
||||
});
|
||||
angular.module('plunker', [])
|
||||
|
||||
.factory('plunkGenerator', function ($document) {
|
||||
|
||||
return function (ngVersion, bsVersion, version, module, content) {
|
||||
|
||||
var form = angular.element('<form style="display: none;" method="post" action="http://plnkr.co/edit/?p=preview" target="_blank"></form>');
|
||||
var addField = function (name, value) {
|
||||
var input = angular.element('<input type="hidden" name="' + name + '">');
|
||||
input.attr('value', value);
|
||||
form.append(input);
|
||||
};
|
||||
|
||||
var indexContent = function (content, version) {
|
||||
return '<!doctype html>\n' +
|
||||
'<html ng-app="plunker">\n' +
|
||||
' <head>\n' +
|
||||
' <script src="http://ajax.googleapis.com/ajax/libs/angularjs/'+ngVersion+'/angular.js"></script>\n' +
|
||||
' <script src="http://angular-ui.github.io/bootstrap/ui-bootstrap-tpls-'+version+'.js"></script>\n' +
|
||||
' <script src="example.js"></script>\n' +
|
||||
' <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/'+bsVersion+'/css/bootstrap-combined.min.css" rel="stylesheet">\n' +
|
||||
' </head>\n' +
|
||||
' <body>\n\n' +
|
||||
content + '\n' +
|
||||
' </body>\n' +
|
||||
'</html>\n';
|
||||
};
|
||||
|
||||
var scriptContent = function(content) {
|
||||
return "angular.module('plunker', ['ui.bootstrap']);" + "\n" + content;
|
||||
};
|
||||
|
||||
addField('description', 'http://angular-ui.github.io/bootstrap/');
|
||||
addField('files[index.html]', indexContent(content.markup, version));
|
||||
addField('files[example.js]', scriptContent(content.javascript));
|
||||
|
||||
$document.find('body').append(form);
|
||||
form[0].submit();
|
||||
form.remove();
|
||||
};
|
||||
})
|
||||
|
||||
.controller('PlunkerCtrl', function ($scope, plunkGenerator) {
|
||||
|
||||
$scope.content = {};
|
||||
|
||||
$scope.edit = function (ngVersion, bsVersion, version, module) {
|
||||
plunkGenerator(ngVersion, bsVersion, version, module, $scope.content);
|
||||
};
|
||||
})
|
||||
|
||||
.directive('plunkerContent', function () {
|
||||
return {
|
||||
link:function (scope, element, attrs) {
|
||||
scope.$parent.content[attrs.plunkerContent] = element.text();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
+141
-141
@@ -1,141 +1,141 @@
|
||||
angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
|
||||
|
||||
.constant('accordionConfig', {
|
||||
closeOthers: true
|
||||
})
|
||||
|
||||
.controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', function ($scope, $attrs, accordionConfig) {
|
||||
|
||||
// This array keeps track of the accordion groups
|
||||
this.groups = [];
|
||||
|
||||
// Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to
|
||||
this.closeOthers = function(openGroup) {
|
||||
var closeOthers = angular.isDefined($attrs.closeOthers) ? $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers;
|
||||
if ( closeOthers ) {
|
||||
angular.forEach(this.groups, function (group) {
|
||||
if ( group !== openGroup ) {
|
||||
group.isOpen = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// This is called from the accordion-group directive to add itself to the accordion
|
||||
this.addGroup = function(groupScope) {
|
||||
var that = this;
|
||||
this.groups.push(groupScope);
|
||||
|
||||
groupScope.$on('$destroy', function (event) {
|
||||
that.removeGroup(groupScope);
|
||||
});
|
||||
};
|
||||
|
||||
// This is called from the accordion-group directive when to remove itself
|
||||
this.removeGroup = function(group) {
|
||||
var index = this.groups.indexOf(group);
|
||||
if ( index !== -1 ) {
|
||||
this.groups.splice(this.groups.indexOf(group), 1);
|
||||
}
|
||||
};
|
||||
|
||||
}])
|
||||
|
||||
// The accordion directive simply sets up the directive controller
|
||||
// and adds an accordion CSS class to itself element.
|
||||
.directive('accordion', function () {
|
||||
return {
|
||||
restrict:'EA',
|
||||
controller:'AccordionController',
|
||||
transclude: true,
|
||||
replace: false,
|
||||
templateUrl: 'template/accordion/accordion.html'
|
||||
};
|
||||
})
|
||||
|
||||
// The accordion-group directive indicates a block of html that will expand and collapse in an accordion
|
||||
.directive('accordionGroup', ['$parse', '$transition', '$timeout', function($parse, $transition, $timeout) {
|
||||
return {
|
||||
require:'^accordion', // We need this directive to be inside an accordion
|
||||
restrict:'EA',
|
||||
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
|
||||
templateUrl:'template/accordion/accordion-group.html',
|
||||
scope:{ heading:'@' }, // Create an isolated scope and interpolate the heading attribute onto this scope
|
||||
controller: ['$scope', function($scope) {
|
||||
this.setHeading = function(element) {
|
||||
this.heading = element;
|
||||
};
|
||||
}],
|
||||
link: function(scope, element, attrs, accordionCtrl) {
|
||||
var getIsOpen, setIsOpen;
|
||||
|
||||
accordionCtrl.addGroup(scope);
|
||||
|
||||
scope.isOpen = false;
|
||||
|
||||
if ( attrs.isOpen ) {
|
||||
getIsOpen = $parse(attrs.isOpen);
|
||||
setIsOpen = getIsOpen.assign;
|
||||
|
||||
scope.$watch(
|
||||
function watchIsOpen() { return getIsOpen(scope.$parent); },
|
||||
function updateOpen(value) { scope.isOpen = value; }
|
||||
);
|
||||
|
||||
scope.isOpen = getIsOpen ? getIsOpen(scope.$parent) : false;
|
||||
}
|
||||
|
||||
scope.$watch('isOpen', function(value) {
|
||||
if ( value ) {
|
||||
accordionCtrl.closeOthers(scope);
|
||||
}
|
||||
if ( setIsOpen ) {
|
||||
setIsOpen(scope.$parent, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}])
|
||||
|
||||
// Use accordion-heading below an accordion-group to provide a heading containing HTML
|
||||
// <accordion-group>
|
||||
// <accordion-heading>Heading containing HTML - <img src="..."></accordion-heading>
|
||||
// </accordion-group>
|
||||
.directive('accordionHeading', function() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
transclude: true, // Grab the contents to be used as the heading
|
||||
template: '', // In effect remove this element!
|
||||
replace: true,
|
||||
require: '^accordionGroup',
|
||||
compile: function(element, attr, transclude) {
|
||||
return function link(scope, element, attr, accordionGroupCtrl) {
|
||||
// Pass the heading to the accordion-group controller
|
||||
// so that it can be transcluded into the right place in the template
|
||||
// [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat]
|
||||
accordionGroupCtrl.setHeading(transclude(scope, function() {}));
|
||||
};
|
||||
}
|
||||
};
|
||||
})
|
||||
|
||||
// Use in the accordion-group template to indicate where you want the heading to be transcluded
|
||||
// You must provide the property on the accordion-group controller that will hold the transcluded element
|
||||
// <div class="accordion-group">
|
||||
// <div class="accordion-heading" ><a ... accordion-transclude="heading">...</a></div>
|
||||
// ...
|
||||
// </div>
|
||||
.directive('accordionTransclude', function() {
|
||||
return {
|
||||
require: '^accordionGroup',
|
||||
link: function(scope, element, attr, controller) {
|
||||
scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) {
|
||||
if ( heading ) {
|
||||
element.html('');
|
||||
element.append(heading);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
|
||||
|
||||
.constant('accordionConfig', {
|
||||
closeOthers: true
|
||||
})
|
||||
|
||||
.controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', function ($scope, $attrs, accordionConfig) {
|
||||
|
||||
// This array keeps track of the accordion groups
|
||||
this.groups = [];
|
||||
|
||||
// Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to
|
||||
this.closeOthers = function(openGroup) {
|
||||
var closeOthers = angular.isDefined($attrs.closeOthers) ? $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers;
|
||||
if ( closeOthers ) {
|
||||
angular.forEach(this.groups, function (group) {
|
||||
if ( group !== openGroup ) {
|
||||
group.isOpen = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// This is called from the accordion-group directive to add itself to the accordion
|
||||
this.addGroup = function(groupScope) {
|
||||
var that = this;
|
||||
this.groups.push(groupScope);
|
||||
|
||||
groupScope.$on('$destroy', function (event) {
|
||||
that.removeGroup(groupScope);
|
||||
});
|
||||
};
|
||||
|
||||
// This is called from the accordion-group directive when to remove itself
|
||||
this.removeGroup = function(group) {
|
||||
var index = this.groups.indexOf(group);
|
||||
if ( index !== -1 ) {
|
||||
this.groups.splice(this.groups.indexOf(group), 1);
|
||||
}
|
||||
};
|
||||
|
||||
}])
|
||||
|
||||
// The accordion directive simply sets up the directive controller
|
||||
// and adds an accordion CSS class to itself element.
|
||||
.directive('accordion', function () {
|
||||
return {
|
||||
restrict:'EA',
|
||||
controller:'AccordionController',
|
||||
transclude: true,
|
||||
replace: false,
|
||||
templateUrl: 'template/accordion/accordion.html'
|
||||
};
|
||||
})
|
||||
|
||||
// The accordion-group directive indicates a block of html that will expand and collapse in an accordion
|
||||
.directive('accordionGroup', ['$parse', '$transition', '$timeout', function($parse, $transition, $timeout) {
|
||||
return {
|
||||
require:'^accordion', // We need this directive to be inside an accordion
|
||||
restrict:'EA',
|
||||
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
|
||||
templateUrl:'template/accordion/accordion-group.html',
|
||||
scope:{ heading:'@' }, // Create an isolated scope and interpolate the heading attribute onto this scope
|
||||
controller: ['$scope', function($scope) {
|
||||
this.setHeading = function(element) {
|
||||
this.heading = element;
|
||||
};
|
||||
}],
|
||||
link: function(scope, element, attrs, accordionCtrl) {
|
||||
var getIsOpen, setIsOpen;
|
||||
|
||||
accordionCtrl.addGroup(scope);
|
||||
|
||||
scope.isOpen = false;
|
||||
|
||||
if ( attrs.isOpen ) {
|
||||
getIsOpen = $parse(attrs.isOpen);
|
||||
setIsOpen = getIsOpen.assign;
|
||||
|
||||
scope.$watch(
|
||||
function watchIsOpen() { return getIsOpen(scope.$parent); },
|
||||
function updateOpen(value) { scope.isOpen = value; }
|
||||
);
|
||||
|
||||
scope.isOpen = getIsOpen ? getIsOpen(scope.$parent) : false;
|
||||
}
|
||||
|
||||
scope.$watch('isOpen', function(value) {
|
||||
if ( value ) {
|
||||
accordionCtrl.closeOthers(scope);
|
||||
}
|
||||
if ( setIsOpen ) {
|
||||
setIsOpen(scope.$parent, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}])
|
||||
|
||||
// Use accordion-heading below an accordion-group to provide a heading containing HTML
|
||||
// <accordion-group>
|
||||
// <accordion-heading>Heading containing HTML - <img src="..."></accordion-heading>
|
||||
// </accordion-group>
|
||||
.directive('accordionHeading', function() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
transclude: true, // Grab the contents to be used as the heading
|
||||
template: '', // In effect remove this element!
|
||||
replace: true,
|
||||
require: '^accordionGroup',
|
||||
compile: function(element, attr, transclude) {
|
||||
return function link(scope, element, attr, accordionGroupCtrl) {
|
||||
// Pass the heading to the accordion-group controller
|
||||
// so that it can be transcluded into the right place in the template
|
||||
// [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat]
|
||||
accordionGroupCtrl.setHeading(transclude(scope, function() {}));
|
||||
};
|
||||
}
|
||||
};
|
||||
})
|
||||
|
||||
// Use in the accordion-group template to indicate where you want the heading to be transcluded
|
||||
// You must provide the property on the accordion-group controller that will hold the transcluded element
|
||||
// <div class="accordion-group">
|
||||
// <div class="accordion-heading" ><a ... accordion-transclude="heading">...</a></div>
|
||||
// ...
|
||||
// </div>
|
||||
.directive('accordionTransclude', function() {
|
||||
return {
|
||||
require: '^accordionGroup',
|
||||
link: function(scope, element, attr, controller) {
|
||||
scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) {
|
||||
if ( heading ) {
|
||||
element.html('');
|
||||
element.append(heading);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
+299
-299
@@ -1,299 +1,299 @@
|
||||
describe('accordion', function () {
|
||||
var $scope;
|
||||
|
||||
beforeEach(module('ui.bootstrap.accordion'));
|
||||
beforeEach(module('template/accordion/accordion.html'));
|
||||
beforeEach(module('template/accordion/accordion-group.html'));
|
||||
|
||||
beforeEach(inject(function ($rootScope) {
|
||||
$scope = $rootScope;
|
||||
}));
|
||||
|
||||
describe('controller', function () {
|
||||
|
||||
var ctrl, $element, $attrs;
|
||||
beforeEach(inject(function($controller) {
|
||||
$attrs = {}; $element = {};
|
||||
ctrl = $controller('AccordionController', { $scope: $scope, $element: $element, $attrs: $attrs });
|
||||
}));
|
||||
|
||||
describe('addGroup', function() {
|
||||
it('adds a the specified group to the collection', function() {
|
||||
var group1, group2;
|
||||
ctrl.addGroup(group1 = $scope.$new());
|
||||
ctrl.addGroup(group2 = $scope.$new());
|
||||
expect(ctrl.groups.length).toBe(2);
|
||||
expect(ctrl.groups[0]).toBe(group1);
|
||||
expect(ctrl.groups[1]).toBe(group2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('closeOthers', function() {
|
||||
var group1, group2, group3;
|
||||
beforeEach(function() {
|
||||
ctrl.addGroup(group1 = { isOpen: true, $on : angular.noop });
|
||||
ctrl.addGroup(group2 = { isOpen: true, $on : angular.noop });
|
||||
ctrl.addGroup(group3 = { isOpen: true, $on : angular.noop });
|
||||
});
|
||||
it('should close other groups if close-others attribute is not defined', function() {
|
||||
delete $attrs.closeOthers;
|
||||
ctrl.closeOthers(group2);
|
||||
expect(group1.isOpen).toBe(false);
|
||||
expect(group2.isOpen).toBe(true);
|
||||
expect(group3.isOpen).toBe(false);
|
||||
});
|
||||
|
||||
it('should close other groups if close-others attribute is true', function() {
|
||||
$attrs.closeOthers = 'true';
|
||||
ctrl.closeOthers(group3);
|
||||
expect(group1.isOpen).toBe(false);
|
||||
expect(group2.isOpen).toBe(false);
|
||||
expect(group3.isOpen).toBe(true);
|
||||
});
|
||||
|
||||
it('should not close other groups if close-others attribute is false', function() {
|
||||
$attrs.closeOthers = 'false';
|
||||
ctrl.closeOthers(group2);
|
||||
expect(group1.isOpen).toBe(true);
|
||||
expect(group2.isOpen).toBe(true);
|
||||
expect(group3.isOpen).toBe(true);
|
||||
});
|
||||
|
||||
describe('setting accordionConfig', function() {
|
||||
var originalCloseOthers;
|
||||
beforeEach(inject(function(accordionConfig) {
|
||||
originalCloseOthers = accordionConfig.closeOthers;
|
||||
accordionConfig.closeOthers = false;
|
||||
}));
|
||||
afterEach(inject(function(accordionConfig) {
|
||||
// return it to the original value
|
||||
accordionConfig.closeOthers = originalCloseOthers;
|
||||
}));
|
||||
|
||||
it('should not close other groups if accordionConfig.closeOthers is false', function() {
|
||||
ctrl.closeOthers(group2);
|
||||
expect(group1.isOpen).toBe(true);
|
||||
expect(group2.isOpen).toBe(true);
|
||||
expect(group3.isOpen).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeGroup', function() {
|
||||
it('should remove the specified group', function () {
|
||||
var group1, group2, group3;
|
||||
ctrl.addGroup(group1 = $scope.$new());
|
||||
ctrl.addGroup(group2 = $scope.$new());
|
||||
ctrl.addGroup(group3 = $scope.$new());
|
||||
ctrl.removeGroup(group2);
|
||||
expect(ctrl.groups.length).toBe(2);
|
||||
expect(ctrl.groups[0]).toBe(group1);
|
||||
expect(ctrl.groups[1]).toBe(group3);
|
||||
});
|
||||
it('should ignore remove of non-existing group', function () {
|
||||
var group1, group2;
|
||||
ctrl.addGroup(group1 = $scope.$new());
|
||||
ctrl.addGroup(group2 = $scope.$new());
|
||||
expect(ctrl.groups.length).toBe(2);
|
||||
ctrl.removeGroup({});
|
||||
expect(ctrl.groups.length).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('accordion-group', function () {
|
||||
|
||||
var scope, $compile;
|
||||
var element, groups;
|
||||
var findGroupLink = function (index) {
|
||||
return groups.eq(index).find('a').eq(0);
|
||||
};
|
||||
var findGroupBody = function (index) {
|
||||
return groups.eq(index).find('.accordion-body').eq(0);
|
||||
};
|
||||
|
||||
|
||||
beforeEach(inject(function(_$rootScope_, _$compile_) {
|
||||
scope = _$rootScope_;
|
||||
$compile = _$compile_;
|
||||
}));
|
||||
|
||||
afterEach(function () {
|
||||
element = groups = scope = $compile = undefined;
|
||||
});
|
||||
|
||||
describe('with static groups', function () {
|
||||
beforeEach(function () {
|
||||
var tpl =
|
||||
"<accordion>" +
|
||||
"<accordion-group heading=\"title 1\">Content 1</accordion-group>" +
|
||||
"<accordion-group heading=\"title 2\">Content 2</accordion-group>" +
|
||||
"</accordion>";
|
||||
element = angular.element(tpl);
|
||||
angular.element(document.body).append(element);
|
||||
$compile(element)(scope);
|
||||
scope.$digest();
|
||||
groups = element.find('.accordion-group');
|
||||
});
|
||||
afterEach(function() {
|
||||
element.remove();
|
||||
});
|
||||
|
||||
it('should create accordion groups with content', function () {
|
||||
expect(groups.length).toEqual(2);
|
||||
expect(findGroupLink(0).text()).toEqual('title 1');
|
||||
expect(findGroupBody(0).text().trim()).toEqual('Content 1');
|
||||
expect(findGroupLink(1).text()).toEqual('title 2');
|
||||
expect(findGroupBody(1).text().trim()).toEqual('Content 2');
|
||||
});
|
||||
|
||||
it('should change selected element on click', function () {
|
||||
findGroupLink(0).click();
|
||||
scope.$digest();
|
||||
expect(findGroupBody(0).scope().isOpen).toBe(true);
|
||||
|
||||
findGroupLink(1).click();
|
||||
scope.$digest();
|
||||
expect(findGroupBody(0).scope().isOpen).toBe(false);
|
||||
expect(findGroupBody(1).scope().isOpen).toBe(true);
|
||||
});
|
||||
|
||||
it('should toggle element on click', function() {
|
||||
findGroupLink(0).click();
|
||||
scope.$digest();
|
||||
expect(findGroupBody(0).scope().isOpen).toBe(true);
|
||||
findGroupLink(0).click();
|
||||
scope.$digest();
|
||||
expect(findGroupBody(0).scope().isOpen).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with dynamic groups', function () {
|
||||
var model;
|
||||
beforeEach(function () {
|
||||
var tpl =
|
||||
"<accordion>" +
|
||||
"<accordion-group ng-repeat='group in groups' heading='{{group.name}}'>{{group.content}}</accordion-group>" +
|
||||
"</accordion>";
|
||||
element = angular.element(tpl);
|
||||
model = [
|
||||
{name: 'title 1', content: 'Content 1'},
|
||||
{name: 'title 2', content: 'Content 2'}
|
||||
];
|
||||
|
||||
$compile(element)(scope);
|
||||
scope.$digest();
|
||||
});
|
||||
|
||||
it('should have no groups initially', function () {
|
||||
groups = element.find('.accordion-group');
|
||||
expect(groups.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should have a group for each model item', function() {
|
||||
scope.groups = model;
|
||||
scope.$digest();
|
||||
groups = element.find('.accordion-group');
|
||||
expect(groups.length).toEqual(2);
|
||||
expect(findGroupLink(0).text()).toEqual('title 1');
|
||||
expect(findGroupBody(0).text().trim()).toEqual('Content 1');
|
||||
expect(findGroupLink(1).text()).toEqual('title 2');
|
||||
expect(findGroupBody(1).text().trim()).toEqual('Content 2');
|
||||
});
|
||||
|
||||
it('should react properly on removing items from the model', function () {
|
||||
scope.groups = model;
|
||||
scope.$digest();
|
||||
groups = element.find('.accordion-group');
|
||||
expect(groups.length).toEqual(2);
|
||||
|
||||
scope.groups.splice(0,1);
|
||||
scope.$digest();
|
||||
groups = element.find('.accordion-group');
|
||||
expect(groups.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('is-open attribute', function() {
|
||||
beforeEach(function () {
|
||||
var tpl =
|
||||
"<accordion>" +
|
||||
"<accordion-group heading=\"title 1\" is-open=\"open1\">Content 1</accordion-group>" +
|
||||
"<accordion-group heading=\"title 2\" is-open=\"open2\">Content 2</accordion-group>" +
|
||||
"</accordion>";
|
||||
element = angular.element(tpl);
|
||||
scope.open1 = false;
|
||||
scope.open2 = true;
|
||||
$compile(element)(scope);
|
||||
scope.$digest();
|
||||
groups = element.find('.accordion-group');
|
||||
});
|
||||
|
||||
it('should open the group with isOpen set to true', function () {
|
||||
expect(findGroupBody(0).scope().isOpen).toBe(false);
|
||||
expect(findGroupBody(1).scope().isOpen).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('is-open attribute with dynamic content', function() {
|
||||
beforeEach(function () {
|
||||
var tpl =
|
||||
"<accordion>" +
|
||||
"<accordion-group heading=\"title 1\" is-open=\"open1\"><div ng-repeat='item in items'>{{item}}</div></accordion-group>" +
|
||||
"<accordion-group heading=\"title 2\" is-open=\"open2\">Static content</accordion-group>" +
|
||||
"</accordion>";
|
||||
element = angular.element(tpl);
|
||||
scope.items = ['Item 1', 'Item 2', 'Item 3'];
|
||||
scope.open1 = true;
|
||||
scope.open2 = false;
|
||||
angular.element(document.body).append(element);
|
||||
$compile(element)(scope);
|
||||
scope.$digest();
|
||||
groups = element.find('.accordion-group');
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
element.remove();
|
||||
});
|
||||
|
||||
it('should have visible group body when the group with isOpen set to true', function () {
|
||||
expect(findGroupBody(0)[0].clientHeight).not.toBe(0);
|
||||
expect(findGroupBody(1)[0].clientHeight).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('accordion-heading element', function() {
|
||||
beforeEach(function() {
|
||||
var tpl =
|
||||
'<accordion ng-init="a = [1,2,3]">' +
|
||||
'<accordion-group heading="I get overridden">' +
|
||||
'<accordion-heading>Heading Element <span ng-repeat="x in a">{{x}}</span> </accordion-heading>' +
|
||||
'Body' +
|
||||
'</accordion-group>' +
|
||||
'</accordion>';
|
||||
element = $compile(tpl)(scope);
|
||||
scope.$digest();
|
||||
groups = element.find('.accordion-group');
|
||||
});
|
||||
it('transcludes the <accordion-heading> content into the heading link', function() {
|
||||
expect(findGroupLink(0).text()).toBe('Heading Element 123 ');
|
||||
});
|
||||
it('attaches the same scope to the transcluded heading and body', function() {
|
||||
expect(findGroupLink(0).find('span').scope().$id).toBe(findGroupBody(0).find('span').scope().$id);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('accordion-heading, with repeating accordion-groups', function() {
|
||||
it('should clone the accordion-heading for each group', function() {
|
||||
element = $compile('<accordion><accordion-group ng-repeat="x in [1,2,3]"><accordion-heading>{{x}}</accordion-heading></accordion-group></accordion>')(scope);
|
||||
scope.$digest();
|
||||
groups = element.find('.accordion-group');
|
||||
expect(groups.length).toBe(3);
|
||||
expect(findGroupLink(0).text()).toBe('1');
|
||||
expect(findGroupLink(1).text()).toBe('2');
|
||||
expect(findGroupLink(2).text()).toBe('3');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('accordion', function () {
|
||||
var $scope;
|
||||
|
||||
beforeEach(module('ui.bootstrap.accordion'));
|
||||
beforeEach(module('template/accordion/accordion.html'));
|
||||
beforeEach(module('template/accordion/accordion-group.html'));
|
||||
|
||||
beforeEach(inject(function ($rootScope) {
|
||||
$scope = $rootScope;
|
||||
}));
|
||||
|
||||
describe('controller', function () {
|
||||
|
||||
var ctrl, $element, $attrs;
|
||||
beforeEach(inject(function($controller) {
|
||||
$attrs = {}; $element = {};
|
||||
ctrl = $controller('AccordionController', { $scope: $scope, $element: $element, $attrs: $attrs });
|
||||
}));
|
||||
|
||||
describe('addGroup', function() {
|
||||
it('adds a the specified group to the collection', function() {
|
||||
var group1, group2;
|
||||
ctrl.addGroup(group1 = $scope.$new());
|
||||
ctrl.addGroup(group2 = $scope.$new());
|
||||
expect(ctrl.groups.length).toBe(2);
|
||||
expect(ctrl.groups[0]).toBe(group1);
|
||||
expect(ctrl.groups[1]).toBe(group2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('closeOthers', function() {
|
||||
var group1, group2, group3;
|
||||
beforeEach(function() {
|
||||
ctrl.addGroup(group1 = { isOpen: true, $on : angular.noop });
|
||||
ctrl.addGroup(group2 = { isOpen: true, $on : angular.noop });
|
||||
ctrl.addGroup(group3 = { isOpen: true, $on : angular.noop });
|
||||
});
|
||||
it('should close other groups if close-others attribute is not defined', function() {
|
||||
delete $attrs.closeOthers;
|
||||
ctrl.closeOthers(group2);
|
||||
expect(group1.isOpen).toBe(false);
|
||||
expect(group2.isOpen).toBe(true);
|
||||
expect(group3.isOpen).toBe(false);
|
||||
});
|
||||
|
||||
it('should close other groups if close-others attribute is true', function() {
|
||||
$attrs.closeOthers = 'true';
|
||||
ctrl.closeOthers(group3);
|
||||
expect(group1.isOpen).toBe(false);
|
||||
expect(group2.isOpen).toBe(false);
|
||||
expect(group3.isOpen).toBe(true);
|
||||
});
|
||||
|
||||
it('should not close other groups if close-others attribute is false', function() {
|
||||
$attrs.closeOthers = 'false';
|
||||
ctrl.closeOthers(group2);
|
||||
expect(group1.isOpen).toBe(true);
|
||||
expect(group2.isOpen).toBe(true);
|
||||
expect(group3.isOpen).toBe(true);
|
||||
});
|
||||
|
||||
describe('setting accordionConfig', function() {
|
||||
var originalCloseOthers;
|
||||
beforeEach(inject(function(accordionConfig) {
|
||||
originalCloseOthers = accordionConfig.closeOthers;
|
||||
accordionConfig.closeOthers = false;
|
||||
}));
|
||||
afterEach(inject(function(accordionConfig) {
|
||||
// return it to the original value
|
||||
accordionConfig.closeOthers = originalCloseOthers;
|
||||
}));
|
||||
|
||||
it('should not close other groups if accordionConfig.closeOthers is false', function() {
|
||||
ctrl.closeOthers(group2);
|
||||
expect(group1.isOpen).toBe(true);
|
||||
expect(group2.isOpen).toBe(true);
|
||||
expect(group3.isOpen).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeGroup', function() {
|
||||
it('should remove the specified group', function () {
|
||||
var group1, group2, group3;
|
||||
ctrl.addGroup(group1 = $scope.$new());
|
||||
ctrl.addGroup(group2 = $scope.$new());
|
||||
ctrl.addGroup(group3 = $scope.$new());
|
||||
ctrl.removeGroup(group2);
|
||||
expect(ctrl.groups.length).toBe(2);
|
||||
expect(ctrl.groups[0]).toBe(group1);
|
||||
expect(ctrl.groups[1]).toBe(group3);
|
||||
});
|
||||
it('should ignore remove of non-existing group', function () {
|
||||
var group1, group2;
|
||||
ctrl.addGroup(group1 = $scope.$new());
|
||||
ctrl.addGroup(group2 = $scope.$new());
|
||||
expect(ctrl.groups.length).toBe(2);
|
||||
ctrl.removeGroup({});
|
||||
expect(ctrl.groups.length).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('accordion-group', function () {
|
||||
|
||||
var scope, $compile;
|
||||
var element, groups;
|
||||
var findGroupLink = function (index) {
|
||||
return groups.eq(index).find('a').eq(0);
|
||||
};
|
||||
var findGroupBody = function (index) {
|
||||
return groups.eq(index).find('.accordion-body').eq(0);
|
||||
};
|
||||
|
||||
|
||||
beforeEach(inject(function(_$rootScope_, _$compile_) {
|
||||
scope = _$rootScope_;
|
||||
$compile = _$compile_;
|
||||
}));
|
||||
|
||||
afterEach(function () {
|
||||
element = groups = scope = $compile = undefined;
|
||||
});
|
||||
|
||||
describe('with static groups', function () {
|
||||
beforeEach(function () {
|
||||
var tpl =
|
||||
"<accordion>" +
|
||||
"<accordion-group heading=\"title 1\">Content 1</accordion-group>" +
|
||||
"<accordion-group heading=\"title 2\">Content 2</accordion-group>" +
|
||||
"</accordion>";
|
||||
element = angular.element(tpl);
|
||||
angular.element(document.body).append(element);
|
||||
$compile(element)(scope);
|
||||
scope.$digest();
|
||||
groups = element.find('.accordion-group');
|
||||
});
|
||||
afterEach(function() {
|
||||
element.remove();
|
||||
});
|
||||
|
||||
it('should create accordion groups with content', function () {
|
||||
expect(groups.length).toEqual(2);
|
||||
expect(findGroupLink(0).text()).toEqual('title 1');
|
||||
expect(findGroupBody(0).text().trim()).toEqual('Content 1');
|
||||
expect(findGroupLink(1).text()).toEqual('title 2');
|
||||
expect(findGroupBody(1).text().trim()).toEqual('Content 2');
|
||||
});
|
||||
|
||||
it('should change selected element on click', function () {
|
||||
findGroupLink(0).click();
|
||||
scope.$digest();
|
||||
expect(findGroupBody(0).scope().isOpen).toBe(true);
|
||||
|
||||
findGroupLink(1).click();
|
||||
scope.$digest();
|
||||
expect(findGroupBody(0).scope().isOpen).toBe(false);
|
||||
expect(findGroupBody(1).scope().isOpen).toBe(true);
|
||||
});
|
||||
|
||||
it('should toggle element on click', function() {
|
||||
findGroupLink(0).click();
|
||||
scope.$digest();
|
||||
expect(findGroupBody(0).scope().isOpen).toBe(true);
|
||||
findGroupLink(0).click();
|
||||
scope.$digest();
|
||||
expect(findGroupBody(0).scope().isOpen).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with dynamic groups', function () {
|
||||
var model;
|
||||
beforeEach(function () {
|
||||
var tpl =
|
||||
"<accordion>" +
|
||||
"<accordion-group ng-repeat='group in groups' heading='{{group.name}}'>{{group.content}}</accordion-group>" +
|
||||
"</accordion>";
|
||||
element = angular.element(tpl);
|
||||
model = [
|
||||
{name: 'title 1', content: 'Content 1'},
|
||||
{name: 'title 2', content: 'Content 2'}
|
||||
];
|
||||
|
||||
$compile(element)(scope);
|
||||
scope.$digest();
|
||||
});
|
||||
|
||||
it('should have no groups initially', function () {
|
||||
groups = element.find('.accordion-group');
|
||||
expect(groups.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should have a group for each model item', function() {
|
||||
scope.groups = model;
|
||||
scope.$digest();
|
||||
groups = element.find('.accordion-group');
|
||||
expect(groups.length).toEqual(2);
|
||||
expect(findGroupLink(0).text()).toEqual('title 1');
|
||||
expect(findGroupBody(0).text().trim()).toEqual('Content 1');
|
||||
expect(findGroupLink(1).text()).toEqual('title 2');
|
||||
expect(findGroupBody(1).text().trim()).toEqual('Content 2');
|
||||
});
|
||||
|
||||
it('should react properly on removing items from the model', function () {
|
||||
scope.groups = model;
|
||||
scope.$digest();
|
||||
groups = element.find('.accordion-group');
|
||||
expect(groups.length).toEqual(2);
|
||||
|
||||
scope.groups.splice(0,1);
|
||||
scope.$digest();
|
||||
groups = element.find('.accordion-group');
|
||||
expect(groups.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('is-open attribute', function() {
|
||||
beforeEach(function () {
|
||||
var tpl =
|
||||
"<accordion>" +
|
||||
"<accordion-group heading=\"title 1\" is-open=\"open1\">Content 1</accordion-group>" +
|
||||
"<accordion-group heading=\"title 2\" is-open=\"open2\">Content 2</accordion-group>" +
|
||||
"</accordion>";
|
||||
element = angular.element(tpl);
|
||||
scope.open1 = false;
|
||||
scope.open2 = true;
|
||||
$compile(element)(scope);
|
||||
scope.$digest();
|
||||
groups = element.find('.accordion-group');
|
||||
});
|
||||
|
||||
it('should open the group with isOpen set to true', function () {
|
||||
expect(findGroupBody(0).scope().isOpen).toBe(false);
|
||||
expect(findGroupBody(1).scope().isOpen).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('is-open attribute with dynamic content', function() {
|
||||
beforeEach(function () {
|
||||
var tpl =
|
||||
"<accordion>" +
|
||||
"<accordion-group heading=\"title 1\" is-open=\"open1\"><div ng-repeat='item in items'>{{item}}</div></accordion-group>" +
|
||||
"<accordion-group heading=\"title 2\" is-open=\"open2\">Static content</accordion-group>" +
|
||||
"</accordion>";
|
||||
element = angular.element(tpl);
|
||||
scope.items = ['Item 1', 'Item 2', 'Item 3'];
|
||||
scope.open1 = true;
|
||||
scope.open2 = false;
|
||||
angular.element(document.body).append(element);
|
||||
$compile(element)(scope);
|
||||
scope.$digest();
|
||||
groups = element.find('.accordion-group');
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
element.remove();
|
||||
});
|
||||
|
||||
it('should have visible group body when the group with isOpen set to true', function () {
|
||||
expect(findGroupBody(0)[0].clientHeight).not.toBe(0);
|
||||
expect(findGroupBody(1)[0].clientHeight).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('accordion-heading element', function() {
|
||||
beforeEach(function() {
|
||||
var tpl =
|
||||
'<accordion ng-init="a = [1,2,3]">' +
|
||||
'<accordion-group heading="I get overridden">' +
|
||||
'<accordion-heading>Heading Element <span ng-repeat="x in a">{{x}}</span> </accordion-heading>' +
|
||||
'Body' +
|
||||
'</accordion-group>' +
|
||||
'</accordion>';
|
||||
element = $compile(tpl)(scope);
|
||||
scope.$digest();
|
||||
groups = element.find('.accordion-group');
|
||||
});
|
||||
it('transcludes the <accordion-heading> content into the heading link', function() {
|
||||
expect(findGroupLink(0).text()).toBe('Heading Element 123 ');
|
||||
});
|
||||
it('attaches the same scope to the transcluded heading and body', function() {
|
||||
expect(findGroupLink(0).find('span').scope().$id).toBe(findGroupBody(0).find('span').scope().$id);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('accordion-heading, with repeating accordion-groups', function() {
|
||||
it('should clone the accordion-heading for each group', function() {
|
||||
element = $compile('<accordion><accordion-group ng-repeat="x in [1,2,3]"><accordion-heading>{{x}}</accordion-heading></accordion-group></accordion>')(scope);
|
||||
scope.$digest();
|
||||
groups = element.find('.accordion-group');
|
||||
expect(groups.length).toBe(3);
|
||||
expect(findGroupLink(0).text()).toBe('1');
|
||||
expect(findGroupLink(1).text()).toBe('2');
|
||||
expect(findGroupLink(2).text()).toBe('3');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+75
-75
@@ -1,76 +1,76 @@
|
||||
angular.module('ui.bootstrap.buttons', [])
|
||||
|
||||
.constant('buttonConfig', {
|
||||
activeClass:'active',
|
||||
toggleEvent:'click'
|
||||
})
|
||||
|
||||
.directive('btnRadio', ['buttonConfig', function (buttonConfig) {
|
||||
var activeClass = buttonConfig.activeClass || 'active';
|
||||
var toggleEvent = buttonConfig.toggleEvent || 'click';
|
||||
|
||||
return {
|
||||
|
||||
require:'ngModel',
|
||||
link:function (scope, element, attrs, ngModelCtrl) {
|
||||
|
||||
var value = scope.$eval(attrs.btnRadio);
|
||||
|
||||
//model -> UI
|
||||
scope.$watch(function () {
|
||||
return ngModelCtrl.$modelValue;
|
||||
}, function (modelValue) {
|
||||
if (angular.equals(modelValue, value)){
|
||||
element.addClass(activeClass);
|
||||
} else {
|
||||
element.removeClass(activeClass);
|
||||
}
|
||||
});
|
||||
|
||||
//ui->model
|
||||
element.bind(toggleEvent, function () {
|
||||
if (!element.hasClass(activeClass)) {
|
||||
scope.$apply(function () {
|
||||
ngModelCtrl.$setViewValue(value);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}])
|
||||
|
||||
.directive('btnCheckbox', ['buttonConfig', function (buttonConfig) {
|
||||
|
||||
var activeClass = buttonConfig.activeClass || 'active';
|
||||
var toggleEvent = buttonConfig.toggleEvent || 'click';
|
||||
|
||||
return {
|
||||
require:'ngModel',
|
||||
link:function (scope, element, attrs, ngModelCtrl) {
|
||||
|
||||
var trueValue = scope.$eval(attrs.btnCheckboxTrue);
|
||||
var falseValue = scope.$eval(attrs.btnCheckboxFalse);
|
||||
|
||||
trueValue = angular.isDefined(trueValue) ? trueValue : true;
|
||||
falseValue = angular.isDefined(falseValue) ? falseValue : false;
|
||||
|
||||
//model -> UI
|
||||
scope.$watch(function () {
|
||||
return ngModelCtrl.$modelValue;
|
||||
}, function (modelValue) {
|
||||
if (angular.equals(modelValue, trueValue)) {
|
||||
element.addClass(activeClass);
|
||||
} else {
|
||||
element.removeClass(activeClass);
|
||||
}
|
||||
});
|
||||
|
||||
//ui->model
|
||||
element.bind(toggleEvent, function () {
|
||||
scope.$apply(function () {
|
||||
ngModelCtrl.$setViewValue(element.hasClass(activeClass) ? falseValue : trueValue);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
angular.module('ui.bootstrap.buttons', [])
|
||||
|
||||
.constant('buttonConfig', {
|
||||
activeClass:'active',
|
||||
toggleEvent:'click'
|
||||
})
|
||||
|
||||
.directive('btnRadio', ['buttonConfig', function (buttonConfig) {
|
||||
var activeClass = buttonConfig.activeClass || 'active';
|
||||
var toggleEvent = buttonConfig.toggleEvent || 'click';
|
||||
|
||||
return {
|
||||
|
||||
require:'ngModel',
|
||||
link:function (scope, element, attrs, ngModelCtrl) {
|
||||
|
||||
var value = scope.$eval(attrs.btnRadio);
|
||||
|
||||
//model -> UI
|
||||
scope.$watch(function () {
|
||||
return ngModelCtrl.$modelValue;
|
||||
}, function (modelValue) {
|
||||
if (angular.equals(modelValue, value)){
|
||||
element.addClass(activeClass);
|
||||
} else {
|
||||
element.removeClass(activeClass);
|
||||
}
|
||||
});
|
||||
|
||||
//ui->model
|
||||
element.bind(toggleEvent, function () {
|
||||
if (!element.hasClass(activeClass)) {
|
||||
scope.$apply(function () {
|
||||
ngModelCtrl.$setViewValue(value);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}])
|
||||
|
||||
.directive('btnCheckbox', ['buttonConfig', function (buttonConfig) {
|
||||
|
||||
var activeClass = buttonConfig.activeClass || 'active';
|
||||
var toggleEvent = buttonConfig.toggleEvent || 'click';
|
||||
|
||||
return {
|
||||
require:'ngModel',
|
||||
link:function (scope, element, attrs, ngModelCtrl) {
|
||||
|
||||
var trueValue = scope.$eval(attrs.btnCheckboxTrue);
|
||||
var falseValue = scope.$eval(attrs.btnCheckboxFalse);
|
||||
|
||||
trueValue = angular.isDefined(trueValue) ? trueValue : true;
|
||||
falseValue = angular.isDefined(falseValue) ? falseValue : false;
|
||||
|
||||
//model -> UI
|
||||
scope.$watch(function () {
|
||||
return ngModelCtrl.$modelValue;
|
||||
}, function (modelValue) {
|
||||
if (angular.equals(modelValue, trueValue)) {
|
||||
element.addClass(activeClass);
|
||||
} else {
|
||||
element.removeClass(activeClass);
|
||||
}
|
||||
});
|
||||
|
||||
//ui->model
|
||||
element.bind(toggleEvent, function () {
|
||||
scope.$apply(function () {
|
||||
ngModelCtrl.$setViewValue(element.hasClass(activeClass) ? falseValue : trueValue);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
+20
-20
@@ -1,21 +1,21 @@
|
||||
<div ng-controller="ButtonsCtrl">
|
||||
<h4>Single toggle</h4>
|
||||
<pre>{{singleModel}}</pre>
|
||||
<button type="button" class="btn btn-primary" ng-model="singleModel" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
|
||||
Single Toggle
|
||||
</button>
|
||||
<h4>Checkbox</h4>
|
||||
<pre>{{checkModel}}</pre>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary" ng-model="checkModel.left" btn-checkbox>Left</button>
|
||||
<button type="button" class="btn btn-primary" ng-model="checkModel.middle" btn-checkbox>Middle</button>
|
||||
<button type="button" class="btn btn-primary" ng-model="checkModel.right" btn-checkbox>Right</button>
|
||||
</div>
|
||||
<h4>Radio</h4>
|
||||
<pre>{{radioModel}}</pre>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary" ng-model="radioModel" btn-radio="'Left'">Left</button>
|
||||
<button type="button" class="btn btn-primary" ng-model="radioModel" btn-radio="'Middle'">Middle</button>
|
||||
<button type="button" class="btn btn-primary" ng-model="radioModel" btn-radio="'Right'">Right</button>
|
||||
</div>
|
||||
<div ng-controller="ButtonsCtrl">
|
||||
<h4>Single toggle</h4>
|
||||
<pre>{{singleModel}}</pre>
|
||||
<button type="button" class="btn btn-primary" ng-model="singleModel" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
|
||||
Single Toggle
|
||||
</button>
|
||||
<h4>Checkbox</h4>
|
||||
<pre>{{checkModel}}</pre>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary" ng-model="checkModel.left" btn-checkbox>Left</button>
|
||||
<button type="button" class="btn btn-primary" ng-model="checkModel.middle" btn-checkbox>Middle</button>
|
||||
<button type="button" class="btn btn-primary" ng-model="checkModel.right" btn-checkbox>Right</button>
|
||||
</div>
|
||||
<h4>Radio</h4>
|
||||
<pre>{{radioModel}}</pre>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary" ng-model="radioModel" btn-radio="'Left'">Left</button>
|
||||
<button type="button" class="btn btn-primary" ng-model="radioModel" btn-radio="'Middle'">Middle</button>
|
||||
<button type="button" class="btn btn-primary" ng-model="radioModel" btn-radio="'Right'">Right</button>
|
||||
</div>
|
||||
</div>
|
||||
+11
-11
@@ -1,12 +1,12 @@
|
||||
var ButtonsCtrl = function ($scope) {
|
||||
|
||||
$scope.singleModel = 1;
|
||||
|
||||
$scope.radioModel = 'Middle';
|
||||
|
||||
$scope.checkModel = {
|
||||
left: false,
|
||||
middle: true,
|
||||
right: false
|
||||
};
|
||||
var ButtonsCtrl = function ($scope) {
|
||||
|
||||
$scope.singleModel = 1;
|
||||
|
||||
$scope.radioModel = 'Middle';
|
||||
|
||||
$scope.checkModel = {
|
||||
left: false,
|
||||
middle: true,
|
||||
right: false
|
||||
};
|
||||
};
|
||||
@@ -1,2 +1,2 @@
|
||||
There are 2 directives that can make a group of buttons to behave like a set of checkboxes or radio buttons.
|
||||
|
||||
There are 2 directives that can make a group of buttons to behave like a set of checkboxes or radio buttons.
|
||||
|
||||
|
||||
@@ -1,94 +1,94 @@
|
||||
describe('buttons', function () {
|
||||
|
||||
var $scope, $compile;
|
||||
|
||||
beforeEach(module('ui.bootstrap.buttons'));
|
||||
beforeEach(inject(function (_$rootScope_, _$compile_) {
|
||||
$scope = _$rootScope_;
|
||||
$compile = _$compile_;
|
||||
}));
|
||||
|
||||
describe('checkbox', function () {
|
||||
|
||||
var compileButton = function (markup, scope) {
|
||||
var el = $compile(markup)(scope);
|
||||
scope.$digest();
|
||||
return el;
|
||||
};
|
||||
|
||||
//model -> UI
|
||||
it('should work correctly with default model values', function () {
|
||||
$scope.model = false;
|
||||
var btn = compileButton('<button ng-model="model" btn-checkbox>click</button>', $scope);
|
||||
expect(btn).not.toHaveClass('active');
|
||||
|
||||
$scope.model = true;
|
||||
$scope.$digest();
|
||||
expect(btn).toHaveClass('active');
|
||||
});
|
||||
|
||||
it('should bind custom model values', function () {
|
||||
$scope.model = 1;
|
||||
var btn = compileButton('<button ng-model="model" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">click</button>', $scope);
|
||||
expect(btn).toHaveClass('active');
|
||||
|
||||
$scope.model = 0;
|
||||
$scope.$digest();
|
||||
expect(btn).not.toHaveClass('active');
|
||||
});
|
||||
|
||||
//UI-> model
|
||||
it('should toggle default model values on click', function () {
|
||||
$scope.model = false;
|
||||
var btn = compileButton('<button ng-model="model" btn-checkbox>click</button>', $scope);
|
||||
|
||||
btn.click();
|
||||
expect($scope.model).toEqual(true);
|
||||
btn.click();
|
||||
expect($scope.model).toEqual(false);
|
||||
});
|
||||
|
||||
it('should toggle custom model values on click', function () {
|
||||
$scope.model = 0;
|
||||
var btn = compileButton('<button ng-model="model" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">click</button>', $scope);
|
||||
|
||||
btn.click();
|
||||
expect($scope.model).toEqual(1);
|
||||
btn.click();
|
||||
expect($scope.model).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('radio', function () {
|
||||
|
||||
var compileButtons = function (markup, scope) {
|
||||
var el = $compile('<div>'+markup+'</div>')(scope);
|
||||
scope.$digest();
|
||||
return el.find('button');
|
||||
};
|
||||
|
||||
//model -> UI
|
||||
it('should work correctly set active class based on model', function () {
|
||||
var btns = compileButtons('<button ng-model="model" btn-radio="1">click1</button><button ng-model="model" btn-radio="2">click2</button>', $scope);
|
||||
expect(btns.eq(0)).not.toHaveClass('active');
|
||||
expect(btns.eq(1)).not.toHaveClass('active');
|
||||
|
||||
$scope.model = 2;
|
||||
$scope.$digest();
|
||||
expect(btns.eq(0)).not.toHaveClass('active');
|
||||
expect(btns.eq(1)).toHaveClass('active');
|
||||
});
|
||||
|
||||
//UI->model
|
||||
it('should work correctly set active class based on model', function () {
|
||||
var btns = compileButtons('<button ng-model="model" btn-radio="1">click1</button><button ng-model="model" btn-radio="2">click2</button>', $scope);
|
||||
expect($scope.model).toBeUndefined();
|
||||
|
||||
btns.eq(0).click();
|
||||
expect($scope.model).toEqual(1);
|
||||
|
||||
btns.eq(1).click();
|
||||
expect($scope.model).toEqual(2);
|
||||
});
|
||||
});
|
||||
describe('buttons', function () {
|
||||
|
||||
var $scope, $compile;
|
||||
|
||||
beforeEach(module('ui.bootstrap.buttons'));
|
||||
beforeEach(inject(function (_$rootScope_, _$compile_) {
|
||||
$scope = _$rootScope_;
|
||||
$compile = _$compile_;
|
||||
}));
|
||||
|
||||
describe('checkbox', function () {
|
||||
|
||||
var compileButton = function (markup, scope) {
|
||||
var el = $compile(markup)(scope);
|
||||
scope.$digest();
|
||||
return el;
|
||||
};
|
||||
|
||||
//model -> UI
|
||||
it('should work correctly with default model values', function () {
|
||||
$scope.model = false;
|
||||
var btn = compileButton('<button ng-model="model" btn-checkbox>click</button>', $scope);
|
||||
expect(btn).not.toHaveClass('active');
|
||||
|
||||
$scope.model = true;
|
||||
$scope.$digest();
|
||||
expect(btn).toHaveClass('active');
|
||||
});
|
||||
|
||||
it('should bind custom model values', function () {
|
||||
$scope.model = 1;
|
||||
var btn = compileButton('<button ng-model="model" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">click</button>', $scope);
|
||||
expect(btn).toHaveClass('active');
|
||||
|
||||
$scope.model = 0;
|
||||
$scope.$digest();
|
||||
expect(btn).not.toHaveClass('active');
|
||||
});
|
||||
|
||||
//UI-> model
|
||||
it('should toggle default model values on click', function () {
|
||||
$scope.model = false;
|
||||
var btn = compileButton('<button ng-model="model" btn-checkbox>click</button>', $scope);
|
||||
|
||||
btn.click();
|
||||
expect($scope.model).toEqual(true);
|
||||
btn.click();
|
||||
expect($scope.model).toEqual(false);
|
||||
});
|
||||
|
||||
it('should toggle custom model values on click', function () {
|
||||
$scope.model = 0;
|
||||
var btn = compileButton('<button ng-model="model" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">click</button>', $scope);
|
||||
|
||||
btn.click();
|
||||
expect($scope.model).toEqual(1);
|
||||
btn.click();
|
||||
expect($scope.model).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('radio', function () {
|
||||
|
||||
var compileButtons = function (markup, scope) {
|
||||
var el = $compile('<div>'+markup+'</div>')(scope);
|
||||
scope.$digest();
|
||||
return el.find('button');
|
||||
};
|
||||
|
||||
//model -> UI
|
||||
it('should work correctly set active class based on model', function () {
|
||||
var btns = compileButtons('<button ng-model="model" btn-radio="1">click1</button><button ng-model="model" btn-radio="2">click2</button>', $scope);
|
||||
expect(btns.eq(0)).not.toHaveClass('active');
|
||||
expect(btns.eq(1)).not.toHaveClass('active');
|
||||
|
||||
$scope.model = 2;
|
||||
$scope.$digest();
|
||||
expect(btns.eq(0)).not.toHaveClass('active');
|
||||
expect(btns.eq(1)).toHaveClass('active');
|
||||
});
|
||||
|
||||
//UI->model
|
||||
it('should work correctly set active class based on model', function () {
|
||||
var btns = compileButtons('<button ng-model="model" btn-radio="1">click1</button><button ng-model="model" btn-radio="2">click2</button>', $scope);
|
||||
expect($scope.model).toBeUndefined();
|
||||
|
||||
btns.eq(0).click();
|
||||
expect($scope.model).toEqual(1);
|
||||
|
||||
btns.eq(1).click();
|
||||
expect($scope.model).toEqual(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
+15
-15
@@ -1,16 +1,16 @@
|
||||
<div ng-controller="ModalDemoCtrl">
|
||||
<button class="btn" ng-click="open()">Open me!</button>
|
||||
<div modal="shouldBeOpen" close="close()" options="opts">
|
||||
<div class="modal-header">
|
||||
<h3>I'm a modal!</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<ul>
|
||||
<li ng-repeat="item in items">{{item}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-warning cancel" ng-click="close()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-controller="ModalDemoCtrl">
|
||||
<button class="btn" ng-click="open()">Open me!</button>
|
||||
<div modal="shouldBeOpen" close="close()" options="opts">
|
||||
<div class="modal-header">
|
||||
<h3>I'm a modal!</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<ul>
|
||||
<li ng-repeat="item in items">{{item}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-warning cancel" ng-click="close()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,9 +1,9 @@
|
||||
<div ng-controller="PaginationDemoCtrl">
|
||||
<pagination num-pages="noOfPages" current-page="currentPage"></pagination>
|
||||
<pagination num-pages="noOfPages" current-page="currentPage" class="pagination-small" previous-text="«" next-text="»"></pagination>
|
||||
<pagination boundary-links="true" num-pages="noOfPages" current-page="currentPage" max-size="maxSize"></pagination>
|
||||
<pagination num-pages="noOfPages" current-page="currentPage" max-size="maxSize"></pagination>
|
||||
<pagination direction-links="false" num-pages="noOfPages" current-page="currentPage"></pagination>
|
||||
<button class="btn" ng-click="setPage(3)">Set current page to: 3</button>
|
||||
The selected page no: {{currentPage}}
|
||||
<div ng-controller="PaginationDemoCtrl">
|
||||
<pagination num-pages="noOfPages" current-page="currentPage"></pagination>
|
||||
<pagination num-pages="noOfPages" current-page="currentPage" class="pagination-small" previous-text="«" next-text="»"></pagination>
|
||||
<pagination boundary-links="true" num-pages="noOfPages" current-page="currentPage" max-size="maxSize"></pagination>
|
||||
<pagination num-pages="noOfPages" current-page="currentPage" max-size="maxSize"></pagination>
|
||||
<pagination direction-links="false" num-pages="noOfPages" current-page="currentPage"></pagination>
|
||||
<button class="btn" ng-click="setPage(3)">Set current page to: 3</button>
|
||||
The selected page no: {{currentPage}}
|
||||
</div>
|
||||
@@ -1,9 +1,9 @@
|
||||
var PaginationDemoCtrl = function ($scope) {
|
||||
$scope.noOfPages = 7;
|
||||
$scope.currentPage = 4;
|
||||
$scope.maxSize = 5;
|
||||
|
||||
$scope.setPage = function (pageNo) {
|
||||
$scope.currentPage = pageNo;
|
||||
};
|
||||
};
|
||||
var PaginationDemoCtrl = function ($scope) {
|
||||
$scope.noOfPages = 7;
|
||||
$scope.currentPage = 4;
|
||||
$scope.maxSize = 5;
|
||||
|
||||
$scope.setPage = function (pageNo) {
|
||||
$scope.currentPage = pageNo;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
A lightweight pagination directive that is focused on ... providing pagination!
|
||||
|
||||
It will take care of visualising a pagination bar. Additionally it will make sure that the state (enabled / disabled) of the Previous / Next and First / Last buttons (if exist) is maintained correctly.
|
||||
|
||||
A lightweight pagination directive that is focused on ... providing pagination!
|
||||
|
||||
It will take care of visualising a pagination bar. Additionally it will make sure that the state (enabled / disabled) of the Previous / Next and First / Last buttons (if exist) is maintained correctly.
|
||||
|
||||
It also provides optional attribute max-size to limit the size of pagination bar.
|
||||
+79
-79
@@ -1,79 +1,79 @@
|
||||
angular.module('ui.bootstrap.position', [])
|
||||
|
||||
/**
|
||||
* A set of utility methods that can be use to retrieve position of DOM elements.
|
||||
* It is meant to be used where we need to absolute-position DOM elements in
|
||||
* relation to other, existing elements (this is the case for tooltips, popovers,
|
||||
* typeahead suggestions etc.).
|
||||
*/
|
||||
.factory('$position', ['$document', '$window', function ($document, $window) {
|
||||
|
||||
function getStyle(el, cssprop) {
|
||||
if (el.currentStyle) { //IE
|
||||
return el.currentStyle[cssprop];
|
||||
} else if ($window.getComputedStyle) {
|
||||
return $window.getComputedStyle(el)[cssprop];
|
||||
}
|
||||
// finally try and get inline style
|
||||
return el.style[cssprop];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given element is statically positioned
|
||||
* @param element - raw DOM element
|
||||
*/
|
||||
function isStaticPositioned(element) {
|
||||
return (getStyle(element, "position") || 'static' ) === 'static';
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the closest, non-statically positioned parentOffset of a given element
|
||||
* @param element
|
||||
*/
|
||||
var parentOffsetEl = function (element) {
|
||||
var docDomEl = $document[0];
|
||||
var offsetParent = element.offsetParent || docDomEl;
|
||||
while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) {
|
||||
offsetParent = offsetParent.offsetParent;
|
||||
}
|
||||
return offsetParent || docDomEl;
|
||||
};
|
||||
|
||||
return {
|
||||
/**
|
||||
* Provides read-only equivalent of jQuery's position function:
|
||||
* http://api.jquery.com/position/
|
||||
*/
|
||||
position: function (element) {
|
||||
var elBCR = this.offset(element);
|
||||
var offsetParentBCR = { top: 0, left: 0 };
|
||||
var offsetParentEl = parentOffsetEl(element[0]);
|
||||
if (offsetParentEl != $document[0]) {
|
||||
offsetParentBCR = this.offset(angular.element(offsetParentEl));
|
||||
offsetParentBCR.top += offsetParentEl.clientTop;
|
||||
offsetParentBCR.left += offsetParentEl.clientLeft;
|
||||
}
|
||||
|
||||
return {
|
||||
width: element.prop('offsetWidth'),
|
||||
height: element.prop('offsetHeight'),
|
||||
top: elBCR.top - offsetParentBCR.top,
|
||||
left: elBCR.left - offsetParentBCR.left
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Provides read-only equivalent of jQuery's offset function:
|
||||
* http://api.jquery.com/offset/
|
||||
*/
|
||||
offset: function (element) {
|
||||
var boundingClientRect = element[0].getBoundingClientRect();
|
||||
return {
|
||||
width: element.prop('offsetWidth'),
|
||||
height: element.prop('offsetHeight'),
|
||||
top: boundingClientRect.top + ($window.pageYOffset || $document[0].body.scrollTop),
|
||||
left: boundingClientRect.left + ($window.pageXOffset || $document[0].body.scrollLeft)
|
||||
};
|
||||
}
|
||||
};
|
||||
}]);
|
||||
angular.module('ui.bootstrap.position', [])
|
||||
|
||||
/**
|
||||
* A set of utility methods that can be use to retrieve position of DOM elements.
|
||||
* It is meant to be used where we need to absolute-position DOM elements in
|
||||
* relation to other, existing elements (this is the case for tooltips, popovers,
|
||||
* typeahead suggestions etc.).
|
||||
*/
|
||||
.factory('$position', ['$document', '$window', function ($document, $window) {
|
||||
|
||||
function getStyle(el, cssprop) {
|
||||
if (el.currentStyle) { //IE
|
||||
return el.currentStyle[cssprop];
|
||||
} else if ($window.getComputedStyle) {
|
||||
return $window.getComputedStyle(el)[cssprop];
|
||||
}
|
||||
// finally try and get inline style
|
||||
return el.style[cssprop];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given element is statically positioned
|
||||
* @param element - raw DOM element
|
||||
*/
|
||||
function isStaticPositioned(element) {
|
||||
return (getStyle(element, "position") || 'static' ) === 'static';
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the closest, non-statically positioned parentOffset of a given element
|
||||
* @param element
|
||||
*/
|
||||
var parentOffsetEl = function (element) {
|
||||
var docDomEl = $document[0];
|
||||
var offsetParent = element.offsetParent || docDomEl;
|
||||
while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) {
|
||||
offsetParent = offsetParent.offsetParent;
|
||||
}
|
||||
return offsetParent || docDomEl;
|
||||
};
|
||||
|
||||
return {
|
||||
/**
|
||||
* Provides read-only equivalent of jQuery's position function:
|
||||
* http://api.jquery.com/position/
|
||||
*/
|
||||
position: function (element) {
|
||||
var elBCR = this.offset(element);
|
||||
var offsetParentBCR = { top: 0, left: 0 };
|
||||
var offsetParentEl = parentOffsetEl(element[0]);
|
||||
if (offsetParentEl != $document[0]) {
|
||||
offsetParentBCR = this.offset(angular.element(offsetParentEl));
|
||||
offsetParentBCR.top += offsetParentEl.clientTop;
|
||||
offsetParentBCR.left += offsetParentEl.clientLeft;
|
||||
}
|
||||
|
||||
return {
|
||||
width: element.prop('offsetWidth'),
|
||||
height: element.prop('offsetHeight'),
|
||||
top: elBCR.top - offsetParentBCR.top,
|
||||
left: elBCR.left - offsetParentBCR.left
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Provides read-only equivalent of jQuery's offset function:
|
||||
* http://api.jquery.com/offset/
|
||||
*/
|
||||
offset: function (element) {
|
||||
var boundingClientRect = element[0].getBoundingClientRect();
|
||||
return {
|
||||
width: element.prop('offsetWidth'),
|
||||
height: element.prop('offsetHeight'),
|
||||
top: boundingClientRect.top + ($window.pageYOffset || $document[0].body.scrollTop),
|
||||
left: boundingClientRect.left + ($window.pageXOffset || $document[0].body.scrollLeft)
|
||||
};
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
+118
-118
@@ -1,118 +1,118 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" ng-app="position">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script src="../../../misc/test-lib/angular.js"></script>
|
||||
<script src="../position.js"></script>
|
||||
<style type="text/css">
|
||||
.container {
|
||||
border: 1px solid red;
|
||||
}
|
||||
|
||||
.container-relative {
|
||||
border: 1px solid red;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content {
|
||||
border: 5px solid #808080;
|
||||
background-color: dodgerblue;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.positioned {
|
||||
border: 5px solid #808080;
|
||||
background-color: green;
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
angular.module('position', ['ui.bootstrap.position']).directive('position', function ($compile, $position) {
|
||||
return {
|
||||
link: function (scope, element, attrs) {
|
||||
|
||||
var positionedEl = angular.element('<div class="positioned">Positioned</div>');
|
||||
var elPosition = $position.position(element);
|
||||
elPosition.left += elPosition.width;
|
||||
|
||||
positionedEl.css({left: elPosition.left + 'px', top: elPosition.top + 'px'});
|
||||
element.after($compile(positionedEl)(scope));
|
||||
}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body class="container">
|
||||
<h3>Within body</h3>
|
||||
|
||||
<div class="content" position>Content</div>
|
||||
|
||||
<h3>Within statically positioned DIV</h3>
|
||||
|
||||
<div class="container">
|
||||
<div class="content" position>Content</div>
|
||||
</div>
|
||||
|
||||
<h3>Within relative-positioned DIV - position specified in CSS</h3>
|
||||
|
||||
<div class="container-relative">
|
||||
<div class="content" position>Content</div>
|
||||
</div>
|
||||
|
||||
<h3>Within relative-positioned DIV</h3>
|
||||
|
||||
<div style="position: relative; left: 200px" class="container">
|
||||
<div class="content" position>Content</div>
|
||||
</div>
|
||||
|
||||
<h3>Within absolute-positioned DIV</h3>
|
||||
|
||||
<div style="position: absolute; left: 400px" class="container">
|
||||
<div class="content" position>Content</div>
|
||||
</div>
|
||||
|
||||
<h3>Next to a float element</h3>
|
||||
|
||||
<div class="container">
|
||||
<div class="content" style="float: right" position>Content</div>
|
||||
</div>
|
||||
|
||||
<h3>Within a table</h3>
|
||||
<table class="container">
|
||||
<tr>
|
||||
<td>Some other content</td>
|
||||
<td>
|
||||
<div class="content" position>Content</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h3>Within a table that is inside a relative-positioned DIV</h3>
|
||||
|
||||
<div style="position: relative; left: 200px" class="container">
|
||||
<table class="container">
|
||||
<tr>
|
||||
<td>Some other content</td>
|
||||
<td>
|
||||
<div class="content" position>Content</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h3>Inside looong text</h3>
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur non velit nulla. Suspendisse sit amet tempus diam. Sed at ultricies neque. Suspendisse id felis a sem placerat ornare. Donec auctor, purus at molestie tempor, arcu enim molestie lacus, ac imperdiet massa urna eu massa. Praesent velit tellus, scelerisque a fermentum ut, ornare in diam. Phasellus egestas molestie feugiat. Vivamus sit amet viverra metus.</p>
|
||||
<p>Etiam ultricies odio commodo erat ullamcorper sodales. Nullam ac dui ac libero dictum mollis. Quisque convallis adipiscing facilisis. In nec nisi velit, id auctor lectus. Cras interdum urna non felis lacinia vulputate. Integer dignissim, mi aliquam gravida auctor, massa odio cursus lorem, eu ultrices eros nisl tempus diam. Maecenas tristique pellentesque nisi sed adipiscing. Aenean hendrerit sapien quis arcu lobortis vitae pulvinar ante volutpat. Morbi consectetur erat eu lacus facilisis eu ullamcorper orci euismod. Quisque diam dui, interdum in suscipit et, fringilla non justo. Pellentesque non nibh odio. Proin sit amet massa sem.</p>
|
||||
<p>Nam in urna erat, at congue nisi. Donec eu tellus lorem, sed facilisis tellus. Aliquam suscipit faucibus ipsum, at hendrerit metus interdum at. Integer et eros ac lacus vulputate sagittis quis quis erat. Suspendisse consectetur vehicula purus vitae imperdiet. Suspendisse in augue magna, quis imperdiet enim. Nullam non diam ac erat auctor bibendum. Praesent ante mauris, egestas sit amet molestie sed, tristique at lorem. Nam at mi ac nisl venenatis semper nec eget mi. Pellentesque a lectus ac leo feugiat suscipit. Quisque tristique dui nec urna placerat a viverra mi iaculis. Ut et tellus et turpis sagittis iaculis nec eu magna. Sed quis nunc non arcu tincidunt ultricies viverra id mauris.</p>
|
||||
<p>Curabitur luctus rutrum ultricies. Aenean ut rutrum orci. Sed molestie lorem in leo cursus id feugiat nisi scelerisque. Maecenas pulvinar neque nec lacus feugiat dictum. Donec viverra felis nec nisi mollis feugiat. Phasellus vehicula, ligula at mattis porttitor, sapien urna hendrerit quam, at fringilla nisl quam vel elit. In eu lacus ligula. Praesent eget gravida nisl. Suspendisse velit diam, pellentesque a tempus quis, vestibulum vel leo.</p>
|
||||
<p>Maecenas feugiat ultrices laoreet. Sed congue posuere diam ac faucibus. Pellentesque eget leo ligula. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Sed nec quam eu tellus sagittis cursus a sit amet eros. Mauris sit amet orci at orci vulputate commodo ut ut nunc. Etiam sagittis erat ut nisi ultricies feugiat. Morbi sed eros nisi. Cras vitae augue in risus aliquet commodo non id est.</p>
|
||||
<div class="content" position>HERE</div>
|
||||
<p>Maecenas laoreet nisi pretium elit bibendum eget tempor nunc aliquet. Vivamus interdum nisi sit amet tortor fermentum congue. Suspendisse at posuere erat. Aliquam hendrerit ultricies nunc non adipiscing. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Duis molestie viverra nulla a aliquet. Nullam non eros vel sem vehicula suscipit. Ut sit amet arcu ac tortor dignissim viverra in a ligula.</p>
|
||||
|
||||
<div style="position: fixed; bottom: 0px" class="container">
|
||||
<h3>Within fixed div</h3>
|
||||
<div class="content" position>Content</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" ng-app="position">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script src="../../../misc/test-lib/angular.js"></script>
|
||||
<script src="../position.js"></script>
|
||||
<style type="text/css">
|
||||
.container {
|
||||
border: 1px solid red;
|
||||
}
|
||||
|
||||
.container-relative {
|
||||
border: 1px solid red;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content {
|
||||
border: 5px solid #808080;
|
||||
background-color: dodgerblue;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.positioned {
|
||||
border: 5px solid #808080;
|
||||
background-color: green;
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
angular.module('position', ['ui.bootstrap.position']).directive('position', function ($compile, $position) {
|
||||
return {
|
||||
link: function (scope, element, attrs) {
|
||||
|
||||
var positionedEl = angular.element('<div class="positioned">Positioned</div>');
|
||||
var elPosition = $position.position(element);
|
||||
elPosition.left += elPosition.width;
|
||||
|
||||
positionedEl.css({left: elPosition.left + 'px', top: elPosition.top + 'px'});
|
||||
element.after($compile(positionedEl)(scope));
|
||||
}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body class="container">
|
||||
<h3>Within body</h3>
|
||||
|
||||
<div class="content" position>Content</div>
|
||||
|
||||
<h3>Within statically positioned DIV</h3>
|
||||
|
||||
<div class="container">
|
||||
<div class="content" position>Content</div>
|
||||
</div>
|
||||
|
||||
<h3>Within relative-positioned DIV - position specified in CSS</h3>
|
||||
|
||||
<div class="container-relative">
|
||||
<div class="content" position>Content</div>
|
||||
</div>
|
||||
|
||||
<h3>Within relative-positioned DIV</h3>
|
||||
|
||||
<div style="position: relative; left: 200px" class="container">
|
||||
<div class="content" position>Content</div>
|
||||
</div>
|
||||
|
||||
<h3>Within absolute-positioned DIV</h3>
|
||||
|
||||
<div style="position: absolute; left: 400px" class="container">
|
||||
<div class="content" position>Content</div>
|
||||
</div>
|
||||
|
||||
<h3>Next to a float element</h3>
|
||||
|
||||
<div class="container">
|
||||
<div class="content" style="float: right" position>Content</div>
|
||||
</div>
|
||||
|
||||
<h3>Within a table</h3>
|
||||
<table class="container">
|
||||
<tr>
|
||||
<td>Some other content</td>
|
||||
<td>
|
||||
<div class="content" position>Content</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h3>Within a table that is inside a relative-positioned DIV</h3>
|
||||
|
||||
<div style="position: relative; left: 200px" class="container">
|
||||
<table class="container">
|
||||
<tr>
|
||||
<td>Some other content</td>
|
||||
<td>
|
||||
<div class="content" position>Content</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h3>Inside looong text</h3>
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur non velit nulla. Suspendisse sit amet tempus diam. Sed at ultricies neque. Suspendisse id felis a sem placerat ornare. Donec auctor, purus at molestie tempor, arcu enim molestie lacus, ac imperdiet massa urna eu massa. Praesent velit tellus, scelerisque a fermentum ut, ornare in diam. Phasellus egestas molestie feugiat. Vivamus sit amet viverra metus.</p>
|
||||
<p>Etiam ultricies odio commodo erat ullamcorper sodales. Nullam ac dui ac libero dictum mollis. Quisque convallis adipiscing facilisis. In nec nisi velit, id auctor lectus. Cras interdum urna non felis lacinia vulputate. Integer dignissim, mi aliquam gravida auctor, massa odio cursus lorem, eu ultrices eros nisl tempus diam. Maecenas tristique pellentesque nisi sed adipiscing. Aenean hendrerit sapien quis arcu lobortis vitae pulvinar ante volutpat. Morbi consectetur erat eu lacus facilisis eu ullamcorper orci euismod. Quisque diam dui, interdum in suscipit et, fringilla non justo. Pellentesque non nibh odio. Proin sit amet massa sem.</p>
|
||||
<p>Nam in urna erat, at congue nisi. Donec eu tellus lorem, sed facilisis tellus. Aliquam suscipit faucibus ipsum, at hendrerit metus interdum at. Integer et eros ac lacus vulputate sagittis quis quis erat. Suspendisse consectetur vehicula purus vitae imperdiet. Suspendisse in augue magna, quis imperdiet enim. Nullam non diam ac erat auctor bibendum. Praesent ante mauris, egestas sit amet molestie sed, tristique at lorem. Nam at mi ac nisl venenatis semper nec eget mi. Pellentesque a lectus ac leo feugiat suscipit. Quisque tristique dui nec urna placerat a viverra mi iaculis. Ut et tellus et turpis sagittis iaculis nec eu magna. Sed quis nunc non arcu tincidunt ultricies viverra id mauris.</p>
|
||||
<p>Curabitur luctus rutrum ultricies. Aenean ut rutrum orci. Sed molestie lorem in leo cursus id feugiat nisi scelerisque. Maecenas pulvinar neque nec lacus feugiat dictum. Donec viverra felis nec nisi mollis feugiat. Phasellus vehicula, ligula at mattis porttitor, sapien urna hendrerit quam, at fringilla nisl quam vel elit. In eu lacus ligula. Praesent eget gravida nisl. Suspendisse velit diam, pellentesque a tempus quis, vestibulum vel leo.</p>
|
||||
<p>Maecenas feugiat ultrices laoreet. Sed congue posuere diam ac faucibus. Pellentesque eget leo ligula. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Sed nec quam eu tellus sagittis cursus a sit amet eros. Mauris sit amet orci at orci vulputate commodo ut ut nunc. Etiam sagittis erat ut nisi ultricies feugiat. Morbi sed eros nisi. Cras vitae augue in risus aliquet commodo non id est.</p>
|
||||
<div class="content" position>HERE</div>
|
||||
<p>Maecenas laoreet nisi pretium elit bibendum eget tempor nunc aliquet. Vivamus interdum nisi sit amet tortor fermentum congue. Suspendisse at posuere erat. Aliquam hendrerit ultricies nunc non adipiscing. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Duis molestie viverra nulla a aliquet. Nullam non eros vel sem vehicula suscipit. Ut sit amet arcu ac tortor dignissim viverra in a ligula.</p>
|
||||
|
||||
<div style="position: fixed; bottom: 0px" class="container">
|
||||
<h3>Within fixed div</h3>
|
||||
<div class="content" position>Content</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+23
-23
@@ -1,23 +1,23 @@
|
||||
<div ng-controller="TabsDemoCtrl">
|
||||
Select a tab by setting active binding to true:
|
||||
<br />
|
||||
<button class="btn" ng-click="tabs[0].active = true">
|
||||
Select second tab
|
||||
</button>
|
||||
<button class="btn" ng-click="tabs[1].active = true">
|
||||
Select third tab
|
||||
</button>
|
||||
<br /><br />
|
||||
<tabset>
|
||||
<tab heading="Static title">Static content</tab>
|
||||
<tab ng-repeat="tab in tabs" heading="{{tab.title}}" active="tab.active">
|
||||
{{tab.content}}
|
||||
</tab>
|
||||
<tab select="alertMe()">
|
||||
<tab-heading>
|
||||
<i class="icon-bell"></i> Select me for alert!
|
||||
</tab-heading>
|
||||
I've got an HTML heading, and a select callback. Pretty cool!
|
||||
</tab>
|
||||
</tabset>
|
||||
</div>
|
||||
<div ng-controller="TabsDemoCtrl">
|
||||
Select a tab by setting active binding to true:
|
||||
<br />
|
||||
<button class="btn" ng-click="tabs[0].active = true">
|
||||
Select second tab
|
||||
</button>
|
||||
<button class="btn" ng-click="tabs[1].active = true">
|
||||
Select third tab
|
||||
</button>
|
||||
<br /><br />
|
||||
<tabset>
|
||||
<tab heading="Static title">Static content</tab>
|
||||
<tab ng-repeat="tab in tabs" heading="{{tab.title}}" active="tab.active">
|
||||
{{tab.content}}
|
||||
</tab>
|
||||
<tab select="alertMe()">
|
||||
<tab-heading>
|
||||
<i class="icon-bell"></i> Select me for alert!
|
||||
</tab-heading>
|
||||
I've got an HTML heading, and a select callback. Pretty cool!
|
||||
</tab>
|
||||
</tabset>
|
||||
</div>
|
||||
|
||||
+12
-12
@@ -1,12 +1,12 @@
|
||||
var TabsDemoCtrl = function ($scope) {
|
||||
$scope.tabs = [
|
||||
{ title:"Dynamic Title 1", content:"Dynamic content 1" },
|
||||
{ title:"Dynamic Title 2", content:"Dynamic content 2" }
|
||||
];
|
||||
|
||||
$scope.alertMe = function() {
|
||||
setTimeout(function() {
|
||||
alert("You've selected the alert tab!");
|
||||
});
|
||||
};
|
||||
};
|
||||
var TabsDemoCtrl = function ($scope) {
|
||||
$scope.tabs = [
|
||||
{ title:"Dynamic Title 1", content:"Dynamic content 1" },
|
||||
{ title:"Dynamic Title 2", content:"Dynamic content 2" }
|
||||
];
|
||||
|
||||
$scope.alertMe = function() {
|
||||
setTimeout(function() {
|
||||
alert("You've selected the alert tab!");
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class='container-fluid' ng-controller="TypeaheadCtrl">
|
||||
<pre>Model: {{selected| json}}</pre>
|
||||
<input type="text" ng-model="selected" typeahead="state for state in states | filter:$viewValue">
|
||||
<div class='container-fluid' ng-controller="TypeaheadCtrl">
|
||||
<pre>Model: {{selected| json}}</pre>
|
||||
<input type="text" ng-model="selected" typeahead="state for state in states | filter:$viewValue">
|
||||
</div>
|
||||
@@ -1,5 +1,5 @@
|
||||
function TypeaheadCtrl($scope) {
|
||||
|
||||
$scope.selected = undefined;
|
||||
$scope.states = ['Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorado', 'Connecticut', 'Delaware', 'Florida', 'Georgia', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas', 'Kentucky', 'Louisiana', 'Maine', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', 'Mississippi', 'Missouri', 'Montana', 'Nebraska', 'Nevada', 'New Hampshire', 'New Jersey', 'New Mexico', 'New York', 'North Dakota', 'North Carolina', 'Ohio', 'Oklahoma', 'Oregon', 'Pennsylvania', 'Rhode Island', 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont', 'Virginia', 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming'];
|
||||
function TypeaheadCtrl($scope) {
|
||||
|
||||
$scope.selected = undefined;
|
||||
$scope.states = ['Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorado', 'Connecticut', 'Delaware', 'Florida', 'Georgia', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas', 'Kentucky', 'Louisiana', 'Maine', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', 'Mississippi', 'Missouri', 'Montana', 'Nebraska', 'Nevada', 'New Hampshire', 'New Jersey', 'New Mexico', 'New York', 'North Dakota', 'North Carolina', 'Ohio', 'Oklahoma', 'Oregon', 'Pennsylvania', 'Rhode Island', 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont', 'Virginia', 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming'];
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
Typeahead is a AngularJS version of [Twitter Bootstrap typeahead plugin](http://twitter.github.com/bootstrap/javascript.html#typeahead)
|
||||
|
||||
This directive can be used to quickly create elegant typeheads with any form text input.
|
||||
|
||||
It is very well integrated into the AngularJS as:
|
||||
|
||||
* it uses the same, flexible syntax as the [select directive](http://docs.angularjs.org/api/ng.directive:select)
|
||||
Typeahead is a AngularJS version of [Twitter Bootstrap typeahead plugin](http://twitter.github.com/bootstrap/javascript.html#typeahead)
|
||||
|
||||
This directive can be used to quickly create elegant typeheads with any form text input.
|
||||
|
||||
It is very well integrated into the AngularJS as:
|
||||
|
||||
* it uses the same, flexible syntax as the [select directive](http://docs.angularjs.org/api/ng.directive:select)
|
||||
* works with promises and it means that you can retrieve matches using the `$http` service with minimal effort
|
||||
@@ -1,403 +1,403 @@
|
||||
describe('typeahead tests', function () {
|
||||
|
||||
beforeEach(module('ui.bootstrap.typeahead'));
|
||||
beforeEach(module('template/typeahead/typeahead.html'));
|
||||
|
||||
describe('syntax parser', function () {
|
||||
|
||||
var typeaheadParser, scope, filterFilter;
|
||||
beforeEach(inject(function (_$rootScope_, _filterFilter_, _typeaheadParser_) {
|
||||
typeaheadParser = _typeaheadParser_;
|
||||
scope = _$rootScope_;
|
||||
filterFilter = _filterFilter_;
|
||||
}));
|
||||
|
||||
it('should parse the simplest array-based syntax', function () {
|
||||
scope.states = ['Alabama', 'California', 'Delaware'];
|
||||
var result = typeaheadParser.parse('state for state in states | filter:$viewValue');
|
||||
|
||||
var itemName = result.itemName;
|
||||
var locals = {$viewValue:'al'};
|
||||
expect(result.source(scope, locals)).toEqual(['Alabama', 'California']);
|
||||
|
||||
locals[itemName] = 'Alabama';
|
||||
expect(result.viewMapper(scope, locals)).toEqual('Alabama');
|
||||
expect(result.modelMapper(scope, locals)).toEqual('Alabama');
|
||||
});
|
||||
|
||||
it('should parse the simplest function-based syntax', function () {
|
||||
scope.getStates = function ($viewValue) {
|
||||
return filterFilter(['Alabama', 'California', 'Delaware'], $viewValue);
|
||||
};
|
||||
var result = typeaheadParser.parse('state for state in getStates($viewValue)');
|
||||
|
||||
var itemName = result.itemName;
|
||||
var locals = {$viewValue:'al'};
|
||||
expect(result.source(scope, locals)).toEqual(['Alabama', 'California']);
|
||||
|
||||
locals[itemName] = 'Alabama';
|
||||
expect(result.viewMapper(scope, locals)).toEqual('Alabama');
|
||||
expect(result.modelMapper(scope, locals)).toEqual('Alabama');
|
||||
});
|
||||
|
||||
it('should allow to specify custom model mapping that is used as a label as well', function () {
|
||||
|
||||
scope.states = [
|
||||
{code:'AL', name:'Alabama'},
|
||||
{code:'CA', name:'California'},
|
||||
{code:'DE', name:'Delaware'}
|
||||
];
|
||||
var result = typeaheadParser.parse("state.name for state in states | filter:$viewValue | orderBy:'name':true");
|
||||
|
||||
var itemName = result.itemName;
|
||||
expect(itemName).toEqual('state');
|
||||
expect(result.source(scope, {$viewValue:'al'})).toEqual([
|
||||
{code:'CA', name:'California'},
|
||||
{code:'AL', name:'Alabama'}
|
||||
]);
|
||||
|
||||
var locals = {$viewValue:'al'};
|
||||
locals[itemName] = {code:'AL', name:'Alabama'};
|
||||
expect(result.viewMapper(scope, locals)).toEqual('Alabama');
|
||||
expect(result.modelMapper(scope, locals)).toEqual('Alabama');
|
||||
});
|
||||
|
||||
it('should allow to specify custom view and model mappers', function () {
|
||||
|
||||
scope.states = [
|
||||
{code:'AL', name:'Alabama'},
|
||||
{code:'CA', name:'California'},
|
||||
{code:'DE', name:'Delaware'}
|
||||
];
|
||||
var result = typeaheadParser.parse("state.code as state.name + ' ('+state.code+')' for state in states | filter:$viewValue | orderBy:'name':true");
|
||||
|
||||
var itemName = result.itemName;
|
||||
expect(result.source(scope, {$viewValue:'al'})).toEqual([
|
||||
{code:'CA', name:'California'},
|
||||
{code:'AL', name:'Alabama'}
|
||||
]);
|
||||
|
||||
var locals = {$viewValue:'al'};
|
||||
locals[itemName] = {code:'AL', name:'Alabama'};
|
||||
expect(result.viewMapper(scope, locals)).toEqual('Alabama (AL)');
|
||||
expect(result.modelMapper(scope, locals)).toEqual('AL');
|
||||
});
|
||||
});
|
||||
|
||||
describe('typeaheadPopup - result rendering', function () {
|
||||
|
||||
var scope, $rootScope, $compile;
|
||||
beforeEach(inject(function (_$rootScope_, _$compile_) {
|
||||
$rootScope = _$rootScope_;
|
||||
scope = $rootScope.$new();
|
||||
$compile = _$compile_;
|
||||
}));
|
||||
|
||||
it('should render initial results', function () {
|
||||
|
||||
scope.matches = ['foo', 'bar', 'baz'];
|
||||
scope.active = 1;
|
||||
|
||||
var el = $compile("<div><typeahead-popup matches='matches' active='active' select='select(activeIdx)'></typeahead-popup></div>")(scope);
|
||||
$rootScope.$digest();
|
||||
|
||||
var liElems = el.find('li');
|
||||
expect(liElems.length).toEqual(3);
|
||||
expect(liElems.eq(0)).not.toHaveClass('active');
|
||||
expect(liElems.eq(1)).toHaveClass('active');
|
||||
expect(liElems.eq(2)).not.toHaveClass('active');
|
||||
});
|
||||
|
||||
it('should change active item on mouseenter', function () {
|
||||
|
||||
scope.matches = ['foo', 'bar', 'baz'];
|
||||
scope.active = 1;
|
||||
|
||||
var el = $compile("<div><typeahead-popup matches='matches' active='active' select='select(activeIdx)'></typeahead-popup></div>")(scope);
|
||||
$rootScope.$digest();
|
||||
|
||||
var liElems = el.find('li');
|
||||
expect(liElems.eq(1)).toHaveClass('active');
|
||||
expect(liElems.eq(2)).not.toHaveClass('active');
|
||||
|
||||
liElems.eq(2).trigger('mouseenter');
|
||||
|
||||
expect(liElems.eq(1)).not.toHaveClass('active');
|
||||
expect(liElems.eq(2)).toHaveClass('active');
|
||||
});
|
||||
|
||||
it('should select an item on mouse click', function () {
|
||||
|
||||
scope.matches = ['foo', 'bar', 'baz'];
|
||||
scope.active = 1;
|
||||
$rootScope.select = angular.noop;
|
||||
spyOn($rootScope, 'select');
|
||||
|
||||
var el = $compile("<div><typeahead-popup matches='matches' active='active' select='select(activeIdx)'></typeahead-popup></div>")(scope);
|
||||
$rootScope.$digest();
|
||||
|
||||
var liElems = el.find('li');
|
||||
liElems.eq(2).find('a').trigger('click');
|
||||
expect($rootScope.select).toHaveBeenCalledWith(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('typeaheadHighlight', function () {
|
||||
|
||||
var highlightFilter;
|
||||
beforeEach(inject(function (typeaheadHighlightFilter) {
|
||||
highlightFilter = typeaheadHighlightFilter;
|
||||
}));
|
||||
|
||||
it('should higlight a match', function () {
|
||||
expect(highlightFilter('before match after', 'match')).toEqual('before <strong>match</strong> after');
|
||||
});
|
||||
|
||||
it('should higlight a match with mixed case', function () {
|
||||
expect(highlightFilter('before MaTch after', 'match')).toEqual('before <strong>MaTch</strong> after');
|
||||
});
|
||||
|
||||
it('should higlight all matches', function () {
|
||||
expect(highlightFilter('before MaTch after match', 'match')).toEqual('before <strong>MaTch</strong> after <strong>match</strong>');
|
||||
});
|
||||
|
||||
it('should do nothing if no match', function () {
|
||||
expect(highlightFilter('before match after', 'nomatch')).toEqual('before match after');
|
||||
});
|
||||
|
||||
it('issue 316 - should work correctly for regexp reserved words', function () {
|
||||
expect(highlightFilter('before (match after', '(match')).toEqual('before <strong>(match</strong> after');
|
||||
});
|
||||
});
|
||||
|
||||
describe('typeahead', function () {
|
||||
|
||||
var $scope, $compile, $document;
|
||||
var changeInputValueTo;
|
||||
|
||||
beforeEach(inject(function (_$rootScope_, _$compile_, _$document_, $sniffer) {
|
||||
$scope = _$rootScope_;
|
||||
$scope.source = ['foo', 'bar', 'baz'];
|
||||
$compile = _$compile_;
|
||||
$document = _$document_;
|
||||
changeInputValueTo = function (element, value) {
|
||||
var inputEl = findInput(element);
|
||||
inputEl.val(value);
|
||||
inputEl.trigger($sniffer.hasEvent('input') ? 'input' : 'change');
|
||||
$scope.$digest();
|
||||
};
|
||||
}));
|
||||
|
||||
//utility functions
|
||||
var prepareInputEl = function(inputTpl) {
|
||||
var el = $compile(angular.element(inputTpl))($scope);
|
||||
$scope.$digest();
|
||||
return el;
|
||||
};
|
||||
|
||||
var findInput = function(element) {
|
||||
return element.find('input');
|
||||
};
|
||||
|
||||
var findDropDown = function(element) {
|
||||
return element.find('ul.typeahead');
|
||||
};
|
||||
|
||||
var findMatches = function(element) {
|
||||
return findDropDown(element).find('li');
|
||||
};
|
||||
|
||||
var triggerKeyDown = function(element, keyCode) {
|
||||
var inputEl = findInput(element);
|
||||
var e = $.Event("keydown");
|
||||
e.which = keyCode;
|
||||
inputEl.trigger(e);
|
||||
};
|
||||
|
||||
//custom matchers
|
||||
beforeEach(function () {
|
||||
this.addMatchers({
|
||||
toBeClosed: function() {
|
||||
var typeaheadEl = findDropDown(this.actual);
|
||||
this.message = function() {
|
||||
return "Expected '" + angular.mock.dump(this.actual) + "' to be closed.";
|
||||
};
|
||||
return typeaheadEl.css('display')==='none' && findMatches(this.actual).length === 0;
|
||||
|
||||
}, toBeOpenWithActive: function(noOfMatches, activeIdx) {
|
||||
|
||||
var typeaheadEl = findDropDown(this.actual);
|
||||
var liEls = findMatches(this.actual);
|
||||
|
||||
this.message = function() {
|
||||
return "Expected '" + angular.mock.dump(this.actual) + "' to be opened.";
|
||||
};
|
||||
return typeaheadEl.css('display')==='block' && liEls.length === noOfMatches && $(liEls[activeIdx]).hasClass('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
//coarse grained, "integration" tests
|
||||
describe('initial state and model changes', function () {
|
||||
|
||||
it('should be closed by default', function () {
|
||||
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source'></div>");
|
||||
expect(element).toBeClosed();
|
||||
});
|
||||
|
||||
it('should correctly render initial state if the "as" keyword is used', function () {
|
||||
|
||||
$scope.states = [{code: 'AL', name: 'Alaska'}, {code: 'CL', name: 'California'}];
|
||||
$scope.result = $scope.states[0];
|
||||
|
||||
var element = prepareInputEl("<div><input ng-model='result' typeahead='state as state.name for state in states'></div>");
|
||||
var inputEl = findInput(element);
|
||||
|
||||
expect(inputEl.val()).toEqual('Alaska');
|
||||
});
|
||||
|
||||
it('should not get open on model change', function () {
|
||||
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source'></div>");
|
||||
$scope.$apply(function(){
|
||||
$scope.result = 'foo';
|
||||
});
|
||||
expect(element).toBeClosed();
|
||||
});
|
||||
});
|
||||
|
||||
describe('basic functionality', function () {
|
||||
|
||||
it('should open and close typeahead based on matches', function () {
|
||||
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source | filter:$viewValue'></div>");
|
||||
changeInputValueTo(element, 'ba');
|
||||
expect(element).toBeOpenWithActive(2, 0);
|
||||
});
|
||||
|
||||
it('should not open typeahead if input value smaller than a defined threshold', function () {
|
||||
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source | filter:$viewValue' typeahead-min-length='2'></div>");
|
||||
changeInputValueTo(element, 'b');
|
||||
expect(element).toBeClosed();
|
||||
});
|
||||
|
||||
it('should support custom model selecting function', function () {
|
||||
$scope.updaterFn = function(selectedItem) {
|
||||
return 'prefix' + selectedItem;
|
||||
};
|
||||
var element = prepareInputEl("<div><input ng-model='result' typeahead='updaterFn(item) as item for item in source | filter:$viewValue'></div>");
|
||||
changeInputValueTo(element, 'f');
|
||||
triggerKeyDown(element, 13);
|
||||
expect($scope.result).toEqual('prefixfoo');
|
||||
});
|
||||
|
||||
it('should support custom label rendering function', function () {
|
||||
$scope.formatterFn = function(sourceItem) {
|
||||
return 'prefix' + sourceItem;
|
||||
};
|
||||
|
||||
var element = prepareInputEl("<div><input ng-model='result' typeahead='item as formatterFn(item) for item in source | filter:$viewValue'></div>");
|
||||
changeInputValueTo(element, 'fo');
|
||||
var matchHighlight = findMatches(element).find('a').html();
|
||||
expect(matchHighlight).toEqual('prefix<strong>fo</strong>o');
|
||||
});
|
||||
|
||||
it('should by default bind view value to model even if not part of matches', function () {
|
||||
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source | filter:$viewValue'></div>");
|
||||
changeInputValueTo(element, 'not in matches');
|
||||
expect($scope.result).toEqual('not in matches');
|
||||
});
|
||||
|
||||
it('should support the editable property to limit model bindings to matches only', function () {
|
||||
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source | filter:$viewValue' typeahead-editable='false'></div>");
|
||||
changeInputValueTo(element, 'not in matches');
|
||||
expect($scope.result).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('should bind loading indicator expression', inject(function ($timeout) {
|
||||
|
||||
$scope.isLoading = false;
|
||||
$scope.loadMatches = function(viewValue) {
|
||||
return $timeout(function() { return [];}, 1000);
|
||||
};
|
||||
|
||||
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in loadMatches()' typeahead-loading='isLoading'></div>");
|
||||
changeInputValueTo(element, 'foo');
|
||||
|
||||
expect($scope.isLoading).toBeTruthy();
|
||||
$timeout.flush();
|
||||
expect($scope.isLoading).toBeFalsy();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('selecting a match', function () {
|
||||
|
||||
it('should select a match on enter', function () {
|
||||
|
||||
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source | filter:$viewValue'></div>");
|
||||
var inputEl = findInput(element);
|
||||
|
||||
changeInputValueTo(element, 'b');
|
||||
triggerKeyDown(element, 13);
|
||||
|
||||
expect($scope.result).toEqual('bar');
|
||||
expect(inputEl.val()).toEqual('bar');
|
||||
});
|
||||
|
||||
it('should select a match on tab', function () {
|
||||
|
||||
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source | filter:$viewValue'></div>");
|
||||
var inputEl = findInput(element);
|
||||
|
||||
changeInputValueTo(element, 'b');
|
||||
triggerKeyDown(element, 9);
|
||||
|
||||
expect($scope.result).toEqual('bar');
|
||||
expect(inputEl.val()).toEqual('bar');
|
||||
});
|
||||
|
||||
it('should select match on click', function () {
|
||||
|
||||
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source | filter:$viewValue'></div>");
|
||||
var inputEl = findInput(element);
|
||||
|
||||
changeInputValueTo(element, 'b');
|
||||
var match = $(findMatches(element)[1]).find('a')[0];
|
||||
|
||||
$(match).click();
|
||||
$scope.$digest();
|
||||
|
||||
expect($scope.result).toEqual('baz');
|
||||
expect(inputEl.val()).toEqual('baz');
|
||||
});
|
||||
|
||||
it('should correctly update inputs value on mapping where label is not derived from the model', function () {
|
||||
|
||||
$scope.states = [{code: 'AL', name: 'Alaska'}, {code: 'CL', name: 'California'}];
|
||||
|
||||
var element = prepareInputEl("<div><input ng-model='result' typeahead='state.code as state.name for state in states | filter:$viewValue'></div>");
|
||||
var inputEl = findInput(element);
|
||||
|
||||
changeInputValueTo(element, 'Alas');
|
||||
triggerKeyDown(element, 13);
|
||||
|
||||
expect($scope.result).toEqual('AL');
|
||||
expect(inputEl.val()).toEqual('Alaska');
|
||||
});
|
||||
});
|
||||
|
||||
describe('regressions tests', function () {
|
||||
|
||||
it('issue 231 - closes matches popup on click outside typeahead', function () {
|
||||
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source | filter:$viewValue'></div>");
|
||||
var inputEl = findInput(element);
|
||||
|
||||
changeInputValueTo(element, 'b');
|
||||
|
||||
$document.find('body').click();
|
||||
$scope.$digest();
|
||||
|
||||
expect(element).toBeClosed();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
describe('typeahead tests', function () {
|
||||
|
||||
beforeEach(module('ui.bootstrap.typeahead'));
|
||||
beforeEach(module('template/typeahead/typeahead.html'));
|
||||
|
||||
describe('syntax parser', function () {
|
||||
|
||||
var typeaheadParser, scope, filterFilter;
|
||||
beforeEach(inject(function (_$rootScope_, _filterFilter_, _typeaheadParser_) {
|
||||
typeaheadParser = _typeaheadParser_;
|
||||
scope = _$rootScope_;
|
||||
filterFilter = _filterFilter_;
|
||||
}));
|
||||
|
||||
it('should parse the simplest array-based syntax', function () {
|
||||
scope.states = ['Alabama', 'California', 'Delaware'];
|
||||
var result = typeaheadParser.parse('state for state in states | filter:$viewValue');
|
||||
|
||||
var itemName = result.itemName;
|
||||
var locals = {$viewValue:'al'};
|
||||
expect(result.source(scope, locals)).toEqual(['Alabama', 'California']);
|
||||
|
||||
locals[itemName] = 'Alabama';
|
||||
expect(result.viewMapper(scope, locals)).toEqual('Alabama');
|
||||
expect(result.modelMapper(scope, locals)).toEqual('Alabama');
|
||||
});
|
||||
|
||||
it('should parse the simplest function-based syntax', function () {
|
||||
scope.getStates = function ($viewValue) {
|
||||
return filterFilter(['Alabama', 'California', 'Delaware'], $viewValue);
|
||||
};
|
||||
var result = typeaheadParser.parse('state for state in getStates($viewValue)');
|
||||
|
||||
var itemName = result.itemName;
|
||||
var locals = {$viewValue:'al'};
|
||||
expect(result.source(scope, locals)).toEqual(['Alabama', 'California']);
|
||||
|
||||
locals[itemName] = 'Alabama';
|
||||
expect(result.viewMapper(scope, locals)).toEqual('Alabama');
|
||||
expect(result.modelMapper(scope, locals)).toEqual('Alabama');
|
||||
});
|
||||
|
||||
it('should allow to specify custom model mapping that is used as a label as well', function () {
|
||||
|
||||
scope.states = [
|
||||
{code:'AL', name:'Alabama'},
|
||||
{code:'CA', name:'California'},
|
||||
{code:'DE', name:'Delaware'}
|
||||
];
|
||||
var result = typeaheadParser.parse("state.name for state in states | filter:$viewValue | orderBy:'name':true");
|
||||
|
||||
var itemName = result.itemName;
|
||||
expect(itemName).toEqual('state');
|
||||
expect(result.source(scope, {$viewValue:'al'})).toEqual([
|
||||
{code:'CA', name:'California'},
|
||||
{code:'AL', name:'Alabama'}
|
||||
]);
|
||||
|
||||
var locals = {$viewValue:'al'};
|
||||
locals[itemName] = {code:'AL', name:'Alabama'};
|
||||
expect(result.viewMapper(scope, locals)).toEqual('Alabama');
|
||||
expect(result.modelMapper(scope, locals)).toEqual('Alabama');
|
||||
});
|
||||
|
||||
it('should allow to specify custom view and model mappers', function () {
|
||||
|
||||
scope.states = [
|
||||
{code:'AL', name:'Alabama'},
|
||||
{code:'CA', name:'California'},
|
||||
{code:'DE', name:'Delaware'}
|
||||
];
|
||||
var result = typeaheadParser.parse("state.code as state.name + ' ('+state.code+')' for state in states | filter:$viewValue | orderBy:'name':true");
|
||||
|
||||
var itemName = result.itemName;
|
||||
expect(result.source(scope, {$viewValue:'al'})).toEqual([
|
||||
{code:'CA', name:'California'},
|
||||
{code:'AL', name:'Alabama'}
|
||||
]);
|
||||
|
||||
var locals = {$viewValue:'al'};
|
||||
locals[itemName] = {code:'AL', name:'Alabama'};
|
||||
expect(result.viewMapper(scope, locals)).toEqual('Alabama (AL)');
|
||||
expect(result.modelMapper(scope, locals)).toEqual('AL');
|
||||
});
|
||||
});
|
||||
|
||||
describe('typeaheadPopup - result rendering', function () {
|
||||
|
||||
var scope, $rootScope, $compile;
|
||||
beforeEach(inject(function (_$rootScope_, _$compile_) {
|
||||
$rootScope = _$rootScope_;
|
||||
scope = $rootScope.$new();
|
||||
$compile = _$compile_;
|
||||
}));
|
||||
|
||||
it('should render initial results', function () {
|
||||
|
||||
scope.matches = ['foo', 'bar', 'baz'];
|
||||
scope.active = 1;
|
||||
|
||||
var el = $compile("<div><typeahead-popup matches='matches' active='active' select='select(activeIdx)'></typeahead-popup></div>")(scope);
|
||||
$rootScope.$digest();
|
||||
|
||||
var liElems = el.find('li');
|
||||
expect(liElems.length).toEqual(3);
|
||||
expect(liElems.eq(0)).not.toHaveClass('active');
|
||||
expect(liElems.eq(1)).toHaveClass('active');
|
||||
expect(liElems.eq(2)).not.toHaveClass('active');
|
||||
});
|
||||
|
||||
it('should change active item on mouseenter', function () {
|
||||
|
||||
scope.matches = ['foo', 'bar', 'baz'];
|
||||
scope.active = 1;
|
||||
|
||||
var el = $compile("<div><typeahead-popup matches='matches' active='active' select='select(activeIdx)'></typeahead-popup></div>")(scope);
|
||||
$rootScope.$digest();
|
||||
|
||||
var liElems = el.find('li');
|
||||
expect(liElems.eq(1)).toHaveClass('active');
|
||||
expect(liElems.eq(2)).not.toHaveClass('active');
|
||||
|
||||
liElems.eq(2).trigger('mouseenter');
|
||||
|
||||
expect(liElems.eq(1)).not.toHaveClass('active');
|
||||
expect(liElems.eq(2)).toHaveClass('active');
|
||||
});
|
||||
|
||||
it('should select an item on mouse click', function () {
|
||||
|
||||
scope.matches = ['foo', 'bar', 'baz'];
|
||||
scope.active = 1;
|
||||
$rootScope.select = angular.noop;
|
||||
spyOn($rootScope, 'select');
|
||||
|
||||
var el = $compile("<div><typeahead-popup matches='matches' active='active' select='select(activeIdx)'></typeahead-popup></div>")(scope);
|
||||
$rootScope.$digest();
|
||||
|
||||
var liElems = el.find('li');
|
||||
liElems.eq(2).find('a').trigger('click');
|
||||
expect($rootScope.select).toHaveBeenCalledWith(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('typeaheadHighlight', function () {
|
||||
|
||||
var highlightFilter;
|
||||
beforeEach(inject(function (typeaheadHighlightFilter) {
|
||||
highlightFilter = typeaheadHighlightFilter;
|
||||
}));
|
||||
|
||||
it('should higlight a match', function () {
|
||||
expect(highlightFilter('before match after', 'match')).toEqual('before <strong>match</strong> after');
|
||||
});
|
||||
|
||||
it('should higlight a match with mixed case', function () {
|
||||
expect(highlightFilter('before MaTch after', 'match')).toEqual('before <strong>MaTch</strong> after');
|
||||
});
|
||||
|
||||
it('should higlight all matches', function () {
|
||||
expect(highlightFilter('before MaTch after match', 'match')).toEqual('before <strong>MaTch</strong> after <strong>match</strong>');
|
||||
});
|
||||
|
||||
it('should do nothing if no match', function () {
|
||||
expect(highlightFilter('before match after', 'nomatch')).toEqual('before match after');
|
||||
});
|
||||
|
||||
it('issue 316 - should work correctly for regexp reserved words', function () {
|
||||
expect(highlightFilter('before (match after', '(match')).toEqual('before <strong>(match</strong> after');
|
||||
});
|
||||
});
|
||||
|
||||
describe('typeahead', function () {
|
||||
|
||||
var $scope, $compile, $document;
|
||||
var changeInputValueTo;
|
||||
|
||||
beforeEach(inject(function (_$rootScope_, _$compile_, _$document_, $sniffer) {
|
||||
$scope = _$rootScope_;
|
||||
$scope.source = ['foo', 'bar', 'baz'];
|
||||
$compile = _$compile_;
|
||||
$document = _$document_;
|
||||
changeInputValueTo = function (element, value) {
|
||||
var inputEl = findInput(element);
|
||||
inputEl.val(value);
|
||||
inputEl.trigger($sniffer.hasEvent('input') ? 'input' : 'change');
|
||||
$scope.$digest();
|
||||
};
|
||||
}));
|
||||
|
||||
//utility functions
|
||||
var prepareInputEl = function(inputTpl) {
|
||||
var el = $compile(angular.element(inputTpl))($scope);
|
||||
$scope.$digest();
|
||||
return el;
|
||||
};
|
||||
|
||||
var findInput = function(element) {
|
||||
return element.find('input');
|
||||
};
|
||||
|
||||
var findDropDown = function(element) {
|
||||
return element.find('ul.typeahead');
|
||||
};
|
||||
|
||||
var findMatches = function(element) {
|
||||
return findDropDown(element).find('li');
|
||||
};
|
||||
|
||||
var triggerKeyDown = function(element, keyCode) {
|
||||
var inputEl = findInput(element);
|
||||
var e = $.Event("keydown");
|
||||
e.which = keyCode;
|
||||
inputEl.trigger(e);
|
||||
};
|
||||
|
||||
//custom matchers
|
||||
beforeEach(function () {
|
||||
this.addMatchers({
|
||||
toBeClosed: function() {
|
||||
var typeaheadEl = findDropDown(this.actual);
|
||||
this.message = function() {
|
||||
return "Expected '" + angular.mock.dump(this.actual) + "' to be closed.";
|
||||
};
|
||||
return typeaheadEl.css('display')==='none' && findMatches(this.actual).length === 0;
|
||||
|
||||
}, toBeOpenWithActive: function(noOfMatches, activeIdx) {
|
||||
|
||||
var typeaheadEl = findDropDown(this.actual);
|
||||
var liEls = findMatches(this.actual);
|
||||
|
||||
this.message = function() {
|
||||
return "Expected '" + angular.mock.dump(this.actual) + "' to be opened.";
|
||||
};
|
||||
return typeaheadEl.css('display')==='block' && liEls.length === noOfMatches && $(liEls[activeIdx]).hasClass('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
//coarse grained, "integration" tests
|
||||
describe('initial state and model changes', function () {
|
||||
|
||||
it('should be closed by default', function () {
|
||||
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source'></div>");
|
||||
expect(element).toBeClosed();
|
||||
});
|
||||
|
||||
it('should correctly render initial state if the "as" keyword is used', function () {
|
||||
|
||||
$scope.states = [{code: 'AL', name: 'Alaska'}, {code: 'CL', name: 'California'}];
|
||||
$scope.result = $scope.states[0];
|
||||
|
||||
var element = prepareInputEl("<div><input ng-model='result' typeahead='state as state.name for state in states'></div>");
|
||||
var inputEl = findInput(element);
|
||||
|
||||
expect(inputEl.val()).toEqual('Alaska');
|
||||
});
|
||||
|
||||
it('should not get open on model change', function () {
|
||||
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source'></div>");
|
||||
$scope.$apply(function(){
|
||||
$scope.result = 'foo';
|
||||
});
|
||||
expect(element).toBeClosed();
|
||||
});
|
||||
});
|
||||
|
||||
describe('basic functionality', function () {
|
||||
|
||||
it('should open and close typeahead based on matches', function () {
|
||||
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source | filter:$viewValue'></div>");
|
||||
changeInputValueTo(element, 'ba');
|
||||
expect(element).toBeOpenWithActive(2, 0);
|
||||
});
|
||||
|
||||
it('should not open typeahead if input value smaller than a defined threshold', function () {
|
||||
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source | filter:$viewValue' typeahead-min-length='2'></div>");
|
||||
changeInputValueTo(element, 'b');
|
||||
expect(element).toBeClosed();
|
||||
});
|
||||
|
||||
it('should support custom model selecting function', function () {
|
||||
$scope.updaterFn = function(selectedItem) {
|
||||
return 'prefix' + selectedItem;
|
||||
};
|
||||
var element = prepareInputEl("<div><input ng-model='result' typeahead='updaterFn(item) as item for item in source | filter:$viewValue'></div>");
|
||||
changeInputValueTo(element, 'f');
|
||||
triggerKeyDown(element, 13);
|
||||
expect($scope.result).toEqual('prefixfoo');
|
||||
});
|
||||
|
||||
it('should support custom label rendering function', function () {
|
||||
$scope.formatterFn = function(sourceItem) {
|
||||
return 'prefix' + sourceItem;
|
||||
};
|
||||
|
||||
var element = prepareInputEl("<div><input ng-model='result' typeahead='item as formatterFn(item) for item in source | filter:$viewValue'></div>");
|
||||
changeInputValueTo(element, 'fo');
|
||||
var matchHighlight = findMatches(element).find('a').html();
|
||||
expect(matchHighlight).toEqual('prefix<strong>fo</strong>o');
|
||||
});
|
||||
|
||||
it('should by default bind view value to model even if not part of matches', function () {
|
||||
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source | filter:$viewValue'></div>");
|
||||
changeInputValueTo(element, 'not in matches');
|
||||
expect($scope.result).toEqual('not in matches');
|
||||
});
|
||||
|
||||
it('should support the editable property to limit model bindings to matches only', function () {
|
||||
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source | filter:$viewValue' typeahead-editable='false'></div>");
|
||||
changeInputValueTo(element, 'not in matches');
|
||||
expect($scope.result).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('should bind loading indicator expression', inject(function ($timeout) {
|
||||
|
||||
$scope.isLoading = false;
|
||||
$scope.loadMatches = function(viewValue) {
|
||||
return $timeout(function() { return [];}, 1000);
|
||||
};
|
||||
|
||||
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in loadMatches()' typeahead-loading='isLoading'></div>");
|
||||
changeInputValueTo(element, 'foo');
|
||||
|
||||
expect($scope.isLoading).toBeTruthy();
|
||||
$timeout.flush();
|
||||
expect($scope.isLoading).toBeFalsy();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('selecting a match', function () {
|
||||
|
||||
it('should select a match on enter', function () {
|
||||
|
||||
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source | filter:$viewValue'></div>");
|
||||
var inputEl = findInput(element);
|
||||
|
||||
changeInputValueTo(element, 'b');
|
||||
triggerKeyDown(element, 13);
|
||||
|
||||
expect($scope.result).toEqual('bar');
|
||||
expect(inputEl.val()).toEqual('bar');
|
||||
});
|
||||
|
||||
it('should select a match on tab', function () {
|
||||
|
||||
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source | filter:$viewValue'></div>");
|
||||
var inputEl = findInput(element);
|
||||
|
||||
changeInputValueTo(element, 'b');
|
||||
triggerKeyDown(element, 9);
|
||||
|
||||
expect($scope.result).toEqual('bar');
|
||||
expect(inputEl.val()).toEqual('bar');
|
||||
});
|
||||
|
||||
it('should select match on click', function () {
|
||||
|
||||
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source | filter:$viewValue'></div>");
|
||||
var inputEl = findInput(element);
|
||||
|
||||
changeInputValueTo(element, 'b');
|
||||
var match = $(findMatches(element)[1]).find('a')[0];
|
||||
|
||||
$(match).click();
|
||||
$scope.$digest();
|
||||
|
||||
expect($scope.result).toEqual('baz');
|
||||
expect(inputEl.val()).toEqual('baz');
|
||||
});
|
||||
|
||||
it('should correctly update inputs value on mapping where label is not derived from the model', function () {
|
||||
|
||||
$scope.states = [{code: 'AL', name: 'Alaska'}, {code: 'CL', name: 'California'}];
|
||||
|
||||
var element = prepareInputEl("<div><input ng-model='result' typeahead='state.code as state.name for state in states | filter:$viewValue'></div>");
|
||||
var inputEl = findInput(element);
|
||||
|
||||
changeInputValueTo(element, 'Alas');
|
||||
triggerKeyDown(element, 13);
|
||||
|
||||
expect($scope.result).toEqual('AL');
|
||||
expect(inputEl.val()).toEqual('Alaska');
|
||||
});
|
||||
});
|
||||
|
||||
describe('regressions tests', function () {
|
||||
|
||||
it('issue 231 - closes matches popup on click outside typeahead', function () {
|
||||
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source | filter:$viewValue'></div>");
|
||||
var inputEl = findInput(element);
|
||||
|
||||
changeInputValueTo(element, 'b');
|
||||
|
||||
$document.find('body').click();
|
||||
$scope.$digest();
|
||||
|
||||
expect(element).toBeClosed();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
+236
-236
@@ -1,237 +1,237 @@
|
||||
angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position'])
|
||||
|
||||
/**
|
||||
* A helper service that can parse typeahead's syntax (string provided by users)
|
||||
* Extracted to a separate service for ease of unit testing
|
||||
*/
|
||||
.factory('typeaheadParser', ['$parse', function ($parse) {
|
||||
|
||||
// 00000111000000000000022200000000000000003333333333333330000000000044000
|
||||
var TYPEAHEAD_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/;
|
||||
|
||||
return {
|
||||
parse:function (input) {
|
||||
|
||||
var match = input.match(TYPEAHEAD_REGEXP), modelMapper, viewMapper, source;
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
"Expected typeahead specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_'" +
|
||||
" but got '" + input + "'.");
|
||||
}
|
||||
|
||||
return {
|
||||
itemName:match[3],
|
||||
source:$parse(match[4]),
|
||||
viewMapper:$parse(match[2] || match[1]),
|
||||
modelMapper:$parse(match[1])
|
||||
};
|
||||
}
|
||||
};
|
||||
}])
|
||||
|
||||
.directive('typeahead', ['$compile', '$parse', '$q', '$document', '$position', 'typeaheadParser', function ($compile, $parse, $q, $document, $position, typeaheadParser) {
|
||||
|
||||
var HOT_KEYS = [9, 13, 27, 38, 40];
|
||||
|
||||
return {
|
||||
require:'ngModel',
|
||||
link:function (originalScope, element, attrs, modelCtrl) {
|
||||
|
||||
var selected;
|
||||
|
||||
//minimal no of characters that needs to be entered before typeahead kicks-in
|
||||
var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1;
|
||||
|
||||
//expressions used by typeahead
|
||||
var parserResult = typeaheadParser.parse(attrs.typeahead);
|
||||
|
||||
//should it restrict model values to the ones selected from the popup only?
|
||||
var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false;
|
||||
|
||||
var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop;
|
||||
|
||||
//pop-up element used to display matches
|
||||
var popUpEl = angular.element(
|
||||
"<typeahead-popup " +
|
||||
"matches='matches' " +
|
||||
"active='activeIdx' " +
|
||||
"select='select(activeIdx)' "+
|
||||
"query='query' "+
|
||||
"position='position'>"+
|
||||
"</typeahead-popup>");
|
||||
|
||||
//create a child scope for the typeahead directive so we are not polluting original scope
|
||||
//with typeahead-specific data (matches, query etc.)
|
||||
var scope = originalScope.$new();
|
||||
originalScope.$on('$destroy', function(){
|
||||
scope.$destroy();
|
||||
});
|
||||
|
||||
var resetMatches = function() {
|
||||
scope.matches = [];
|
||||
scope.activeIdx = -1;
|
||||
};
|
||||
|
||||
var getMatchesAsync = function(inputValue) {
|
||||
|
||||
var locals = {$viewValue: inputValue};
|
||||
isLoadingSetter(originalScope, true);
|
||||
$q.when(parserResult.source(scope, locals)).then(function(matches) {
|
||||
|
||||
//it might happen that several async queries were in progress if a user were typing fast
|
||||
//but we are interested only in responses that correspond to the current view value
|
||||
if (inputValue === modelCtrl.$viewValue) {
|
||||
if (matches.length > 0) {
|
||||
|
||||
scope.activeIdx = 0;
|
||||
scope.matches.length = 0;
|
||||
|
||||
//transform labels
|
||||
for(var i=0; i<matches.length; i++) {
|
||||
locals[parserResult.itemName] = matches[i];
|
||||
scope.matches.push({
|
||||
label: parserResult.viewMapper(scope, locals),
|
||||
model: matches[i]
|
||||
});
|
||||
}
|
||||
|
||||
scope.query = inputValue;
|
||||
//position pop-up with matches - we need to re-calculate its position each time we are opening a window
|
||||
//with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page
|
||||
//due to other elements being rendered
|
||||
scope.position = $position.position(element);
|
||||
scope.position.top = scope.position.top + element.prop('offsetHeight');
|
||||
|
||||
} else {
|
||||
resetMatches();
|
||||
}
|
||||
isLoadingSetter(originalScope, false);
|
||||
}
|
||||
}, function(){
|
||||
resetMatches();
|
||||
isLoadingSetter(originalScope, false);
|
||||
});
|
||||
};
|
||||
|
||||
resetMatches();
|
||||
|
||||
//we need to propagate user's query so we can higlight matches
|
||||
scope.query = undefined;
|
||||
|
||||
//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
|
||||
modelCtrl.$parsers.push(function (inputValue) {
|
||||
|
||||
resetMatches();
|
||||
if (selected) {
|
||||
return inputValue;
|
||||
} else {
|
||||
if (inputValue && inputValue.length >= minSearch) {
|
||||
getMatchesAsync(inputValue);
|
||||
}
|
||||
}
|
||||
|
||||
return isEditable ? inputValue : undefined;
|
||||
});
|
||||
|
||||
modelCtrl.$render = function () {
|
||||
var locals = {};
|
||||
locals[parserResult.itemName] = selected || modelCtrl.$viewValue;
|
||||
element.val(parserResult.viewMapper(scope, locals) || modelCtrl.$viewValue);
|
||||
selected = undefined;
|
||||
};
|
||||
|
||||
scope.select = function (activeIdx) {
|
||||
//called from within the $digest() cycle
|
||||
var locals = {};
|
||||
locals[parserResult.itemName] = selected = scope.matches[activeIdx].model;
|
||||
|
||||
modelCtrl.$setViewValue(parserResult.modelMapper(scope, locals));
|
||||
modelCtrl.$render();
|
||||
};
|
||||
|
||||
//bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27)
|
||||
element.bind('keydown', function (evt) {
|
||||
|
||||
//typeahead is open and an "interesting" key was pressed
|
||||
if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
evt.preventDefault();
|
||||
|
||||
if (evt.which === 40) {
|
||||
scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length;
|
||||
scope.$digest();
|
||||
|
||||
} else if (evt.which === 38) {
|
||||
scope.activeIdx = (scope.activeIdx ? scope.activeIdx : scope.matches.length) - 1;
|
||||
scope.$digest();
|
||||
|
||||
} else if (evt.which === 13 || evt.which === 9) {
|
||||
scope.$apply(function () {
|
||||
scope.select(scope.activeIdx);
|
||||
});
|
||||
|
||||
} else if (evt.which === 27) {
|
||||
evt.stopPropagation();
|
||||
|
||||
resetMatches();
|
||||
scope.$digest();
|
||||
}
|
||||
});
|
||||
|
||||
$document.bind('click', function(){
|
||||
resetMatches();
|
||||
scope.$digest();
|
||||
});
|
||||
|
||||
element.after($compile(popUpEl)(scope));
|
||||
}
|
||||
};
|
||||
|
||||
}])
|
||||
|
||||
.directive('typeaheadPopup', function () {
|
||||
return {
|
||||
restrict:'E',
|
||||
scope:{
|
||||
matches:'=',
|
||||
query:'=',
|
||||
active:'=',
|
||||
position:'=',
|
||||
select:'&'
|
||||
},
|
||||
replace:true,
|
||||
templateUrl:'template/typeahead/typeahead.html',
|
||||
link:function (scope, element, attrs) {
|
||||
|
||||
scope.isOpen = function () {
|
||||
return scope.matches.length > 0;
|
||||
};
|
||||
|
||||
scope.isActive = function (matchIdx) {
|
||||
return scope.active == matchIdx;
|
||||
};
|
||||
|
||||
scope.selectActive = function (matchIdx) {
|
||||
scope.active = matchIdx;
|
||||
};
|
||||
|
||||
scope.selectMatch = function (activeIdx) {
|
||||
scope.select({activeIdx:activeIdx});
|
||||
};
|
||||
}
|
||||
};
|
||||
})
|
||||
|
||||
.filter('typeaheadHighlight', function() {
|
||||
|
||||
function escapeRegexp(queryToEscape) {
|
||||
return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1");
|
||||
}
|
||||
|
||||
return function(matchItem, query) {
|
||||
return query ? matchItem.replace(new RegExp(escapeRegexp(query), 'gi'), '<strong>$&</strong>') : query;
|
||||
};
|
||||
angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position'])
|
||||
|
||||
/**
|
||||
* A helper service that can parse typeahead's syntax (string provided by users)
|
||||
* Extracted to a separate service for ease of unit testing
|
||||
*/
|
||||
.factory('typeaheadParser', ['$parse', function ($parse) {
|
||||
|
||||
// 00000111000000000000022200000000000000003333333333333330000000000044000
|
||||
var TYPEAHEAD_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/;
|
||||
|
||||
return {
|
||||
parse:function (input) {
|
||||
|
||||
var match = input.match(TYPEAHEAD_REGEXP), modelMapper, viewMapper, source;
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
"Expected typeahead specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_'" +
|
||||
" but got '" + input + "'.");
|
||||
}
|
||||
|
||||
return {
|
||||
itemName:match[3],
|
||||
source:$parse(match[4]),
|
||||
viewMapper:$parse(match[2] || match[1]),
|
||||
modelMapper:$parse(match[1])
|
||||
};
|
||||
}
|
||||
};
|
||||
}])
|
||||
|
||||
.directive('typeahead', ['$compile', '$parse', '$q', '$document', '$position', 'typeaheadParser', function ($compile, $parse, $q, $document, $position, typeaheadParser) {
|
||||
|
||||
var HOT_KEYS = [9, 13, 27, 38, 40];
|
||||
|
||||
return {
|
||||
require:'ngModel',
|
||||
link:function (originalScope, element, attrs, modelCtrl) {
|
||||
|
||||
var selected;
|
||||
|
||||
//minimal no of characters that needs to be entered before typeahead kicks-in
|
||||
var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1;
|
||||
|
||||
//expressions used by typeahead
|
||||
var parserResult = typeaheadParser.parse(attrs.typeahead);
|
||||
|
||||
//should it restrict model values to the ones selected from the popup only?
|
||||
var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false;
|
||||
|
||||
var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop;
|
||||
|
||||
//pop-up element used to display matches
|
||||
var popUpEl = angular.element(
|
||||
"<typeahead-popup " +
|
||||
"matches='matches' " +
|
||||
"active='activeIdx' " +
|
||||
"select='select(activeIdx)' "+
|
||||
"query='query' "+
|
||||
"position='position'>"+
|
||||
"</typeahead-popup>");
|
||||
|
||||
//create a child scope for the typeahead directive so we are not polluting original scope
|
||||
//with typeahead-specific data (matches, query etc.)
|
||||
var scope = originalScope.$new();
|
||||
originalScope.$on('$destroy', function(){
|
||||
scope.$destroy();
|
||||
});
|
||||
|
||||
var resetMatches = function() {
|
||||
scope.matches = [];
|
||||
scope.activeIdx = -1;
|
||||
};
|
||||
|
||||
var getMatchesAsync = function(inputValue) {
|
||||
|
||||
var locals = {$viewValue: inputValue};
|
||||
isLoadingSetter(originalScope, true);
|
||||
$q.when(parserResult.source(scope, locals)).then(function(matches) {
|
||||
|
||||
//it might happen that several async queries were in progress if a user were typing fast
|
||||
//but we are interested only in responses that correspond to the current view value
|
||||
if (inputValue === modelCtrl.$viewValue) {
|
||||
if (matches.length > 0) {
|
||||
|
||||
scope.activeIdx = 0;
|
||||
scope.matches.length = 0;
|
||||
|
||||
//transform labels
|
||||
for(var i=0; i<matches.length; i++) {
|
||||
locals[parserResult.itemName] = matches[i];
|
||||
scope.matches.push({
|
||||
label: parserResult.viewMapper(scope, locals),
|
||||
model: matches[i]
|
||||
});
|
||||
}
|
||||
|
||||
scope.query = inputValue;
|
||||
//position pop-up with matches - we need to re-calculate its position each time we are opening a window
|
||||
//with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page
|
||||
//due to other elements being rendered
|
||||
scope.position = $position.position(element);
|
||||
scope.position.top = scope.position.top + element.prop('offsetHeight');
|
||||
|
||||
} else {
|
||||
resetMatches();
|
||||
}
|
||||
isLoadingSetter(originalScope, false);
|
||||
}
|
||||
}, function(){
|
||||
resetMatches();
|
||||
isLoadingSetter(originalScope, false);
|
||||
});
|
||||
};
|
||||
|
||||
resetMatches();
|
||||
|
||||
//we need to propagate user's query so we can higlight matches
|
||||
scope.query = undefined;
|
||||
|
||||
//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
|
||||
modelCtrl.$parsers.push(function (inputValue) {
|
||||
|
||||
resetMatches();
|
||||
if (selected) {
|
||||
return inputValue;
|
||||
} else {
|
||||
if (inputValue && inputValue.length >= minSearch) {
|
||||
getMatchesAsync(inputValue);
|
||||
}
|
||||
}
|
||||
|
||||
return isEditable ? inputValue : undefined;
|
||||
});
|
||||
|
||||
modelCtrl.$render = function () {
|
||||
var locals = {};
|
||||
locals[parserResult.itemName] = selected || modelCtrl.$viewValue;
|
||||
element.val(parserResult.viewMapper(scope, locals) || modelCtrl.$viewValue);
|
||||
selected = undefined;
|
||||
};
|
||||
|
||||
scope.select = function (activeIdx) {
|
||||
//called from within the $digest() cycle
|
||||
var locals = {};
|
||||
locals[parserResult.itemName] = selected = scope.matches[activeIdx].model;
|
||||
|
||||
modelCtrl.$setViewValue(parserResult.modelMapper(scope, locals));
|
||||
modelCtrl.$render();
|
||||
};
|
||||
|
||||
//bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27)
|
||||
element.bind('keydown', function (evt) {
|
||||
|
||||
//typeahead is open and an "interesting" key was pressed
|
||||
if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
evt.preventDefault();
|
||||
|
||||
if (evt.which === 40) {
|
||||
scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length;
|
||||
scope.$digest();
|
||||
|
||||
} else if (evt.which === 38) {
|
||||
scope.activeIdx = (scope.activeIdx ? scope.activeIdx : scope.matches.length) - 1;
|
||||
scope.$digest();
|
||||
|
||||
} else if (evt.which === 13 || evt.which === 9) {
|
||||
scope.$apply(function () {
|
||||
scope.select(scope.activeIdx);
|
||||
});
|
||||
|
||||
} else if (evt.which === 27) {
|
||||
evt.stopPropagation();
|
||||
|
||||
resetMatches();
|
||||
scope.$digest();
|
||||
}
|
||||
});
|
||||
|
||||
$document.bind('click', function(){
|
||||
resetMatches();
|
||||
scope.$digest();
|
||||
});
|
||||
|
||||
element.after($compile(popUpEl)(scope));
|
||||
}
|
||||
};
|
||||
|
||||
}])
|
||||
|
||||
.directive('typeaheadPopup', function () {
|
||||
return {
|
||||
restrict:'E',
|
||||
scope:{
|
||||
matches:'=',
|
||||
query:'=',
|
||||
active:'=',
|
||||
position:'=',
|
||||
select:'&'
|
||||
},
|
||||
replace:true,
|
||||
templateUrl:'template/typeahead/typeahead.html',
|
||||
link:function (scope, element, attrs) {
|
||||
|
||||
scope.isOpen = function () {
|
||||
return scope.matches.length > 0;
|
||||
};
|
||||
|
||||
scope.isActive = function (matchIdx) {
|
||||
return scope.active == matchIdx;
|
||||
};
|
||||
|
||||
scope.selectActive = function (matchIdx) {
|
||||
scope.active = matchIdx;
|
||||
};
|
||||
|
||||
scope.selectMatch = function (activeIdx) {
|
||||
scope.select({activeIdx:activeIdx});
|
||||
};
|
||||
}
|
||||
};
|
||||
})
|
||||
|
||||
.filter('typeaheadHighlight', function() {
|
||||
|
||||
function escapeRegexp(queryToEscape) {
|
||||
return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1");
|
||||
}
|
||||
|
||||
return function(matchItem, query) {
|
||||
return query ? matchItem.replace(new RegExp(escapeRegexp(query), 'gi'), '<strong>$&</strong>') : query;
|
||||
};
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="accordion-group">
|
||||
<div class="accordion-heading" ><a class="accordion-toggle" ng-click="isOpen = !isOpen" accordion-transclude="heading">{{heading}}</a></div>
|
||||
<div class="accordion-body" collapse="!isOpen">
|
||||
<div class="accordion-inner" ng-transclude></div> </div>
|
||||
<div class="accordion-group">
|
||||
<div class="accordion-heading" ><a class="accordion-toggle" ng-click="isOpen = !isOpen" accordion-transclude="heading">{{heading}}</a></div>
|
||||
<div class="accordion-body" collapse="!isOpen">
|
||||
<div class="accordion-inner" ng-transclude></div> </div>
|
||||
</div>
|
||||
@@ -1,5 +1,5 @@
|
||||
<ul class="typeahead dropdown-menu" ng-style="{display: isOpen()&&'block' || 'none', top: position.top+'px', left: position.left+'px'}">
|
||||
<li ng-repeat="match in matches" ng-class="{active: isActive($index) }" ng-mouseenter="selectActive($index)">
|
||||
<a tabindex="-1" ng-click="selectMatch($index)" ng-bind-html-unsafe="match.label | typeaheadHighlight:query"></a>
|
||||
</li>
|
||||
<ul class="typeahead dropdown-menu" ng-style="{display: isOpen()&&'block' || 'none', top: position.top+'px', left: position.left+'px'}">
|
||||
<li ng-repeat="match in matches" ng-class="{active: isActive($index) }" ng-mouseenter="selectActive($index)">
|
||||
<a tabindex="-1" ng-click="selectMatch($index)" ng-bind-html-unsafe="match.label | typeaheadHighlight:query"></a>
|
||||
</li>
|
||||
</ul>
|
||||
Reference in New Issue
Block a user