chore(*): normalize line endings

Closes #423
This commit is contained in:
Pawel Kozlowski
2013-05-20 21:35:39 +02:00
parent 48dac234c3
commit b0745c8e05
30 changed files with 1869 additions and 1861 deletions
+6
View File
@@ -0,0 +1,6 @@
*.html eol=lf
*.css eol=lf
*.js eol=lf
*.md eol=lf
*.json eol=lf
*.yml eol=lf
+10 -10
View File
@@ -1,11 +1,11 @@
language: node_js language: node_js
node_js: node_js:
- "0.8" - "0.8"
before_script: before_script:
- export DISPLAY=:99.0 - export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start - sh -e /etc/init.d/xvfb start
- npm install --quiet -g grunt-cli karma - npm install --quiet -g grunt-cli karma
- npm install - npm install
script: grunt script: grunt
+93 -93
View File
@@ -1,93 +1,93 @@
# 0.3.0 (2013-04-30) # 0.3.0 (2013-04-30)
## Features ## Features
- **progressbar:** - **progressbar:**
- add progressbar directive ([261f2072](https://github.com/angular-ui/bootstrap/commit/261f2072)) - add progressbar directive ([261f2072](https://github.com/angular-ui/bootstrap/commit/261f2072))
- **rating:** - **rating:**
- add rating directive ([6b5e6369](https://github.com/angular-ui/bootstrap/commit/6b5e6369)) - add rating directive ([6b5e6369](https://github.com/angular-ui/bootstrap/commit/6b5e6369))
- **typeahead:** - **typeahead:**
- support the editable property ([a40c3fbe](https://github.com/angular-ui/bootstrap/commit/a40c3fbe)) - 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)) - support typeahead-loading bindable expression ([b58c9c88](https://github.com/angular-ui/bootstrap/commit/b58c9c88))
- **tooltip:** - **tooltip:**
- added popup-delay option ([a79a2ba8](https://github.com/angular-ui/bootstrap/commit/a79a2ba8)) - 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 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)) - 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)) - support for custom triggers ([b1ba821b](https://github.com/angular-ui/bootstrap/commit/b1ba821b))
## Bug Fixes ## Bug Fixes
- **alert:** - **alert:**
- don't show close button if no close callback specified ([c2645f4a](https://github.com/angular-ui/bootstrap/commit/c2645f4a)) - don't show close button if no close callback specified ([c2645f4a](https://github.com/angular-ui/bootstrap/commit/c2645f4a))
- **carousel:** - **carousel:**
- Hide navigation indicators if only one slide ([aedc0565](https://github.com/angular-ui/bootstrap/commit/aedc0565)) - Hide navigation indicators if only one slide ([aedc0565](https://github.com/angular-ui/bootstrap/commit/aedc0565))
- **collapse:** - **collapse:**
- remove reference to msTransition for IE10 ([55437b16](https://github.com/angular-ui/bootstrap/commit/55437b16)) - remove reference to msTransition for IE10 ([55437b16](https://github.com/angular-ui/bootstrap/commit/55437b16))
- **dialog:** - **dialog:**
- set _open to false on init ([dcc9ef31](https://github.com/angular-ui/bootstrap/commit/dcc9ef31)) - 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)) - 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)) - 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)) - fix $apply in progres on $location change ([77e6acb9](https://github.com/angular-ui/bootstrap/commit/77e6acb9))
- **tabs:** - **tabs:**
- remove superfluous href from tabs template ([38c1badd](https://github.com/angular-ui/bootstrap/commit/38c1badd)) - remove superfluous href from tabs template ([38c1badd](https://github.com/angular-ui/bootstrap/commit/38c1badd))
- **tooltip:** - **tooltip:**
- fix positioning issues in tooltips and popovers ([6458f487](https://github.com/angular-ui/bootstrap/commit/6458f487)) - fix positioning issues in tooltips and popovers ([6458f487](https://github.com/angular-ui/bootstrap/commit/6458f487))
- **typeahead:** - **typeahead:**
- close matches popup on click outside typeahead ([acca7dcd](https://github.com/angular-ui/bootstrap/commit/acca7dcd)) - 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)) - 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 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)) - 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)) - fix matches pop-up positioning issues ([74beecdb](https://github.com/angular-ui/bootstrap/commit/74beecdb))
# 0.2.0 (2013-03-03) # 0.2.0 (2013-03-03)
## Features ## Features
- **dialog:** - **dialog:**
- Make $dialog 'resolve' property to work the same way of $routeProvider.when ([739f86f](https://github.com/angular-ui/bootstrap/commit/739f86f)) - Make $dialog 'resolve' property to work the same way of $routeProvider.when ([739f86f](https://github.com/angular-ui/bootstrap/commit/739f86f))
- **modal:** - **modal:**
- allow global override of modal options ([acaf72b](https://github.com/angular-ui/bootstrap/commit/acaf72b)) - allow global override of modal options ([acaf72b](https://github.com/angular-ui/bootstrap/commit/acaf72b))
- **buttons:** - **buttons:**
- add checkbox and radio buttons ([571ccf4](https://github.com/angular-ui/bootstrap/commit/571ccf4)) - add checkbox and radio buttons ([571ccf4](https://github.com/angular-ui/bootstrap/commit/571ccf4))
- **carousel:** - **carousel:**
- add slide indicators ([3b677ee](https://github.com/angular-ui/bootstrap/commit/3b677ee)) - add slide indicators ([3b677ee](https://github.com/angular-ui/bootstrap/commit/3b677ee))
- **typeahead:** - **typeahead:**
- add typeahead directive ([6a97da2](https://github.com/angular-ui/bootstrap/commit/6a97da2)) - add typeahead directive ([6a97da2](https://github.com/angular-ui/bootstrap/commit/6a97da2))
- **accordion:** - **accordion:**
- enable HTML in accordion headings ([3afcaa4](https://github.com/angular-ui/bootstrap/commit/3afcaa4)) - enable HTML in accordion headings ([3afcaa4](https://github.com/angular-ui/bootstrap/commit/3afcaa4))
- **pagination:** - **pagination:**
- add first/last link & constant congif options ([0ff0454](https://github.com/angular-ui/bootstrap/commit/0ff0454)) - add first/last link & constant congif options ([0ff0454](https://github.com/angular-ui/bootstrap/commit/0ff0454))
## Bug fixes ## Bug fixes
- **dialog:** - **dialog:**
- update resolve section to new syntax ([1f87486](https://github.com/angular-ui/bootstrap/commit/1f87486)) - 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)) - $compile entire modal ([7575b3c](https://github.com/angular-ui/bootstrap/commit/7575b3c))
- **tooltip:** - **tooltip:**
- don't show tooltips if there is no content to show ([030901e](https://github.com/angular-ui/bootstrap/commit/030901e)) - 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)) - fix placement issues ([a2bbf4d](https://github.com/angular-ui/bootstrap/commit/a2bbf4d))
- **collapse:** - **collapse:**
- Avoids fixed height on collapse ([ff5d119](https://github.com/angular-ui/bootstrap/commit/ff5d119)) - Avoids fixed height on collapse ([ff5d119](https://github.com/angular-ui/bootstrap/commit/ff5d119))
- **accordion:** - **accordion:**
- fix minification issues ([f4da4d6](https://github.com/angular-ui/bootstrap/commit/f4da4d6)) - fix minification issues ([f4da4d6](https://github.com/angular-ui/bootstrap/commit/f4da4d6))
- **typeahead:** - **typeahead:**
- update inputs value on mapping where label is not derived from the model ([a5f64de](https://github.com/angular-ui/bootstrap/commit/a5f64de)) - 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) # 0.1.0 (2013-02-02)
_Very first, initial release_. _Very first, initial release_.
## Features ## Features
Version `0.1.0` was released with the following directives: Version `0.1.0` was released with the following directives:
* accordion * accordion
* alert * alert
* carousel * carousel
* dialog * dialog
* dropdownToggle * dropdownToggle
* modal * modal
* pagination * pagination
* popover * popover
* tabs * tabs
* tooltip * tooltip
+12 -12
View File
@@ -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: 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: * 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; * 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; * the only dependency should be boostrap CSS and its markup structure;
* directives should be html-agnostic as much as possible which in practice means: * directives should be html-agnostic as much as possible which in practice means:
* templates should be referred to using the `templateUrl` property * templates should be referred to using the `templateUrl` property
* it should be easy to change a default template to a custom one * it should be easy to change a default template to a custom one
* directives shouldn't manipulate DOM structure directly (when possible) * directives shouldn't manipulate DOM structure directly (when possible)
* Please assure that you are submitting quality code, specifically make sure that: * 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 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 * 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
+2
View File
@@ -11,6 +11,8 @@ module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-karma'); grunt.loadNpmTasks('grunt-karma');
// Project configuration. // Project configuration.
grunt.util.linefeed = '\n';
grunt.initConfig({ grunt.initConfig({
ngversion: '1.0.5', ngversion: '1.0.5',
bsversion: '2.3.1', bsversion: '2.3.1',
+28 -28
View File
@@ -1,28 +1,28 @@
# <%= version%> (<%= today%>) # <%= version%> (<%= today%>)
## Features ## Features
<% _(changelog.feat).forEach(function(changes, component) { %> <% _(changelog.feat).forEach(function(changes, component) { %>
- **<%= component%>:** - **<%= component%>:**
<% changes.forEach(function(change) { %> <% changes.forEach(function(change) { %>
- <%= change.msg%> ([<%= change.sha1%>](https://github.com/angular-ui/bootstrap/commit/<%= change.sha1%>)) - <%= change.msg%> ([<%= change.sha1%>](https://github.com/angular-ui/bootstrap/commit/<%= change.sha1%>))
<% }) %> <% }) %>
<% }) %> <% }) %>
## Bug Fixes ## Bug Fixes
<% _(changelog.fix).forEach(function(changes, component) { %> <% _(changelog.fix).forEach(function(changes, component) { %>
- **<%= component%>:** - **<%= component%>:**
<% changes.forEach(function(change) { %> <% changes.forEach(function(change) { %>
- <%= change.msg%> ([<%= change.sha1%>](https://github.com/angular-ui/bootstrap/commit/<%= change.sha1%>)) - <%= change.msg%> ([<%= change.sha1%>](https://github.com/angular-ui/bootstrap/commit/<%= change.sha1%>))
<% }) %> <% }) %>
<% }) %> <% }) %>
## Breaking Changes ## Breaking Changes
<% _(changelog.breaking).forEach(function(changes, component) { %> <% _(changelog.breaking).forEach(function(changes, component) { %>
- **<%= component%>:** - **<%= component%>:**
<% changes.forEach(function(change) { %> <% changes.forEach(function(change) { %>
<%= change.msg%> <%= change.msg%>
<% }) %> <% }) %>
<% }) %> <% }) %>
+91 -91
View File
@@ -1,92 +1,92 @@
body { body {
opacity: 1; opacity: 1;
-webkit-transition: opacity 1s ease; -webkit-transition: opacity 1s ease;
-moz-transition: opacity 1s ease; -moz-transition: opacity 1s ease;
transition: opacity 1s; transition: opacity 1s;
} }
.ng-cloak { .ng-cloak {
opacity: 0; opacity: 0;
} }
section { section {
padding-top: 30px; padding-top: 30px;
} }
.page-header h1 > small > a { .page-header h1 > small > a {
color: #999; color: #999;
} }
.page-header h1 > small > a:hover { .page-header h1 > small > a:hover {
text-decoration: none; text-decoration: none;
} }
.footer { .footer {
text-align: center; text-align: center;
padding: 30px 0; padding: 30px 0;
margin-top: 70px; margin-top: 70px;
border-top: 1px solid #e5e5e5; border-top: 1px solid #e5e5e5;
background-color: #f5f5f5; background-color: #f5f5f5;
} }
.hero-unit { .hero-unit {
position: relative; position: relative;
padding: 40px 0; padding: 40px 0;
color: #fff; color: #fff;
text-align: center; text-align: center;
text-shadow: 0 1px 3px rgba(0,0,0,.4), 0 0 30px rgba(0,0,0,.075); text-shadow: 0 1px 3px rgba(0,0,0,.4), 0 0 30px rgba(0,0,0,.075);
background: #020031; background: #020031;
background: -moz-linear-gradient(45deg, #020031 0%, #6d3353 100%); 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-gradient(linear, left bottom, right top, color-stop(0%,#020031), color-stop(100%,#6d3353));
background: -webkit-linear-gradient(45deg, #020031 0%,#6d3353 100%); background: -webkit-linear-gradient(45deg, #020031 0%,#6d3353 100%);
background: -o-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: -ms-linear-gradient(45deg, #020031 0%,#6d3353 100%);
background: 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 ); 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); -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); -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); box-shadow: inset 0 3px 7px rgba(0,0,0,.2), inset 0 -3px 7px rgba(0,0,0,.2);
border-radius: 0; border-radius: 0;
-moz-border-radius: 0; -moz-border-radius: 0;
-webkit-border-radius: 0; -webkit-border-radius: 0;
-o-border-radius: 0; -o-border-radius: 0;
} }
.hero-unit .btn, .pagination-centered .btn { .hero-unit .btn, .pagination-centered .btn {
float: none; float: none;
font-weight: normal; font-weight: normal;
} }
.hero-unit p { .hero-unit p {
margin: 1em 0; margin: 1em 0;
} }
.bs-docs-social { .bs-docs-social {
margin-top: 1em; margin-top: 1em;
padding: 15px 0; padding: 15px 0;
text-align: center; text-align: center;
background-color: rgba(245,245,245,0.3); background-color: rgba(245,245,245,0.3);
border-top: 1px solid rgba(255,255,255,0.3); border-top: 1px solid rgba(255,255,255,0.3);
border-bottom: 1px solid rgba(221,221,221,0.3); border-bottom: 1px solid rgba(221,221,221,0.3);
} }
.bs-docs-social-buttons { .bs-docs-social-buttons {
margin-left: 0; margin-left: 0;
margin-bottom: 0; margin-bottom: 0;
padding-left: 0; padding-left: 0;
list-style: none; list-style: none;
} }
.bs-docs-social-buttons li { .bs-docs-social-buttons li {
display: inline-block; display: inline-block;
padding: 5px 8px; padding: 5px 8px;
line-height: 1; line-height: 1;
} }
.icon-github { .icon-github {
background: no-repeat url('github-16px.png'); background: no-repeat url('github-16px.png');
width: 16px; width: 16px;
height: 16px; height: 16px;
} }
/* Not enough room on mobile for markup tab, js tab, and plunk btn. /* Not enough room on mobile for markup tab, js tab, and plunk btn.
And no one cares about plunk button on a phone anyway */ And no one cares about plunk button on a phone anyway */
@media only screen and (max-device-width: 480px) { @media only screen and (max-device-width: 480px) {
#plunk-btn { #plunk-btn {
display: none; display: none;
} }
} }
+58 -58
View File
@@ -1,58 +1,58 @@
angular.module('plunker', []) angular.module('plunker', [])
.factory('plunkGenerator', function ($document) { .factory('plunkGenerator', function ($document) {
return function (ngVersion, bsVersion, version, module, content) { 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 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 addField = function (name, value) {
var input = angular.element('<input type="hidden" name="' + name + '">'); var input = angular.element('<input type="hidden" name="' + name + '">');
input.attr('value', value); input.attr('value', value);
form.append(input); form.append(input);
}; };
var indexContent = function (content, version) { var indexContent = function (content, version) {
return '<!doctype html>\n' + return '<!doctype html>\n' +
'<html ng-app="plunker">\n' + '<html ng-app="plunker">\n' +
' <head>\n' + ' <head>\n' +
' <script src="http://ajax.googleapis.com/ajax/libs/angularjs/'+ngVersion+'/angular.js"></script>\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="http://angular-ui.github.io/bootstrap/ui-bootstrap-tpls-'+version+'.js"></script>\n' +
' <script src="example.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' + ' <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/'+bsVersion+'/css/bootstrap-combined.min.css" rel="stylesheet">\n' +
' </head>\n' + ' </head>\n' +
' <body>\n\n' + ' <body>\n\n' +
content + '\n' + content + '\n' +
' </body>\n' + ' </body>\n' +
'</html>\n'; '</html>\n';
}; };
var scriptContent = function(content) { var scriptContent = function(content) {
return "angular.module('plunker', ['ui.bootstrap']);" + "\n" + content; return "angular.module('plunker', ['ui.bootstrap']);" + "\n" + content;
}; };
addField('description', 'http://angular-ui.github.io/bootstrap/'); addField('description', 'http://angular-ui.github.io/bootstrap/');
addField('files[index.html]', indexContent(content.markup, version)); addField('files[index.html]', indexContent(content.markup, version));
addField('files[example.js]', scriptContent(content.javascript)); addField('files[example.js]', scriptContent(content.javascript));
$document.find('body').append(form); $document.find('body').append(form);
form[0].submit(); form[0].submit();
form.remove(); form.remove();
}; };
}) })
.controller('PlunkerCtrl', function ($scope, plunkGenerator) { .controller('PlunkerCtrl', function ($scope, plunkGenerator) {
$scope.content = {}; $scope.content = {};
$scope.edit = function (ngVersion, bsVersion, version, module) { $scope.edit = function (ngVersion, bsVersion, version, module) {
plunkGenerator(ngVersion, bsVersion, version, module, $scope.content); plunkGenerator(ngVersion, bsVersion, version, module, $scope.content);
}; };
}) })
.directive('plunkerContent', function () { .directive('plunkerContent', function () {
return { return {
link:function (scope, element, attrs) { link:function (scope, element, attrs) {
scope.$parent.content[attrs.plunkerContent] = element.text(); scope.$parent.content[attrs.plunkerContent] = element.text();
} }
} }
}); });
+141 -141
View File
@@ -1,141 +1,141 @@
angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
.constant('accordionConfig', { .constant('accordionConfig', {
closeOthers: true closeOthers: true
}) })
.controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', function ($scope, $attrs, accordionConfig) { .controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', function ($scope, $attrs, accordionConfig) {
// This array keeps track of the accordion groups // This array keeps track of the accordion groups
this.groups = []; this.groups = [];
// Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to // Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to
this.closeOthers = function(openGroup) { this.closeOthers = function(openGroup) {
var closeOthers = angular.isDefined($attrs.closeOthers) ? $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers; var closeOthers = angular.isDefined($attrs.closeOthers) ? $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers;
if ( closeOthers ) { if ( closeOthers ) {
angular.forEach(this.groups, function (group) { angular.forEach(this.groups, function (group) {
if ( group !== openGroup ) { if ( group !== openGroup ) {
group.isOpen = false; group.isOpen = false;
} }
}); });
} }
}; };
// This is called from the accordion-group directive to add itself to the accordion // This is called from the accordion-group directive to add itself to the accordion
this.addGroup = function(groupScope) { this.addGroup = function(groupScope) {
var that = this; var that = this;
this.groups.push(groupScope); this.groups.push(groupScope);
groupScope.$on('$destroy', function (event) { groupScope.$on('$destroy', function (event) {
that.removeGroup(groupScope); that.removeGroup(groupScope);
}); });
}; };
// This is called from the accordion-group directive when to remove itself // This is called from the accordion-group directive when to remove itself
this.removeGroup = function(group) { this.removeGroup = function(group) {
var index = this.groups.indexOf(group); var index = this.groups.indexOf(group);
if ( index !== -1 ) { if ( index !== -1 ) {
this.groups.splice(this.groups.indexOf(group), 1); this.groups.splice(this.groups.indexOf(group), 1);
} }
}; };
}]) }])
// The accordion directive simply sets up the directive controller // The accordion directive simply sets up the directive controller
// and adds an accordion CSS class to itself element. // and adds an accordion CSS class to itself element.
.directive('accordion', function () { .directive('accordion', function () {
return { return {
restrict:'EA', restrict:'EA',
controller:'AccordionController', controller:'AccordionController',
transclude: true, transclude: true,
replace: false, replace: false,
templateUrl: 'template/accordion/accordion.html' templateUrl: 'template/accordion/accordion.html'
}; };
}) })
// The accordion-group directive indicates a block of html that will expand and collapse in an accordion // 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) { .directive('accordionGroup', ['$parse', '$transition', '$timeout', function($parse, $transition, $timeout) {
return { return {
require:'^accordion', // We need this directive to be inside an accordion require:'^accordion', // We need this directive to be inside an accordion
restrict:'EA', restrict:'EA',
transclude:true, // It transcludes the contents of the directive into the template 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 replace: true, // The element containing the directive will be replaced with the template
templateUrl:'template/accordion/accordion-group.html', templateUrl:'template/accordion/accordion-group.html',
scope:{ heading:'@' }, // Create an isolated scope and interpolate the heading attribute onto this scope scope:{ heading:'@' }, // Create an isolated scope and interpolate the heading attribute onto this scope
controller: ['$scope', function($scope) { controller: ['$scope', function($scope) {
this.setHeading = function(element) { this.setHeading = function(element) {
this.heading = element; this.heading = element;
}; };
}], }],
link: function(scope, element, attrs, accordionCtrl) { link: function(scope, element, attrs, accordionCtrl) {
var getIsOpen, setIsOpen; var getIsOpen, setIsOpen;
accordionCtrl.addGroup(scope); accordionCtrl.addGroup(scope);
scope.isOpen = false; scope.isOpen = false;
if ( attrs.isOpen ) { if ( attrs.isOpen ) {
getIsOpen = $parse(attrs.isOpen); getIsOpen = $parse(attrs.isOpen);
setIsOpen = getIsOpen.assign; setIsOpen = getIsOpen.assign;
scope.$watch( scope.$watch(
function watchIsOpen() { return getIsOpen(scope.$parent); }, function watchIsOpen() { return getIsOpen(scope.$parent); },
function updateOpen(value) { scope.isOpen = value; } function updateOpen(value) { scope.isOpen = value; }
); );
scope.isOpen = getIsOpen ? getIsOpen(scope.$parent) : false; scope.isOpen = getIsOpen ? getIsOpen(scope.$parent) : false;
} }
scope.$watch('isOpen', function(value) { scope.$watch('isOpen', function(value) {
if ( value ) { if ( value ) {
accordionCtrl.closeOthers(scope); accordionCtrl.closeOthers(scope);
} }
if ( setIsOpen ) { if ( setIsOpen ) {
setIsOpen(scope.$parent, value); setIsOpen(scope.$parent, value);
} }
}); });
} }
}; };
}]) }])
// Use accordion-heading below an accordion-group to provide a heading containing HTML // Use accordion-heading below an accordion-group to provide a heading containing HTML
// <accordion-group> // <accordion-group>
// <accordion-heading>Heading containing HTML - <img src="..."></accordion-heading> // <accordion-heading>Heading containing HTML - <img src="..."></accordion-heading>
// </accordion-group> // </accordion-group>
.directive('accordionHeading', function() { .directive('accordionHeading', function() {
return { return {
restrict: 'E', restrict: 'E',
transclude: true, // Grab the contents to be used as the heading transclude: true, // Grab the contents to be used as the heading
template: '', // In effect remove this element! template: '', // In effect remove this element!
replace: true, replace: true,
require: '^accordionGroup', require: '^accordionGroup',
compile: function(element, attr, transclude) { compile: function(element, attr, transclude) {
return function link(scope, element, attr, accordionGroupCtrl) { return function link(scope, element, attr, accordionGroupCtrl) {
// Pass the heading to the accordion-group controller // Pass the heading to the accordion-group controller
// so that it can be transcluded into the right place in the template // 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] // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat]
accordionGroupCtrl.setHeading(transclude(scope, function() {})); accordionGroupCtrl.setHeading(transclude(scope, function() {}));
}; };
} }
}; };
}) })
// Use in the accordion-group template to indicate where you want the heading to be transcluded // 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 // You must provide the property on the accordion-group controller that will hold the transcluded element
// <div class="accordion-group"> // <div class="accordion-group">
// <div class="accordion-heading" ><a ... accordion-transclude="heading">...</a></div> // <div class="accordion-heading" ><a ... accordion-transclude="heading">...</a></div>
// ... // ...
// </div> // </div>
.directive('accordionTransclude', function() { .directive('accordionTransclude', function() {
return { return {
require: '^accordionGroup', require: '^accordionGroup',
link: function(scope, element, attr, controller) { link: function(scope, element, attr, controller) {
scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) { scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) {
if ( heading ) { if ( heading ) {
element.html(''); element.html('');
element.append(heading); element.append(heading);
} }
}); });
} }
}; };
}); });
+299 -299
View File
@@ -1,299 +1,299 @@
describe('accordion', function () { describe('accordion', function () {
var $scope; var $scope;
beforeEach(module('ui.bootstrap.accordion')); beforeEach(module('ui.bootstrap.accordion'));
beforeEach(module('template/accordion/accordion.html')); beforeEach(module('template/accordion/accordion.html'));
beforeEach(module('template/accordion/accordion-group.html')); beforeEach(module('template/accordion/accordion-group.html'));
beforeEach(inject(function ($rootScope) { beforeEach(inject(function ($rootScope) {
$scope = $rootScope; $scope = $rootScope;
})); }));
describe('controller', function () { describe('controller', function () {
var ctrl, $element, $attrs; var ctrl, $element, $attrs;
beforeEach(inject(function($controller) { beforeEach(inject(function($controller) {
$attrs = {}; $element = {}; $attrs = {}; $element = {};
ctrl = $controller('AccordionController', { $scope: $scope, $element: $element, $attrs: $attrs }); ctrl = $controller('AccordionController', { $scope: $scope, $element: $element, $attrs: $attrs });
})); }));
describe('addGroup', function() { describe('addGroup', function() {
it('adds a the specified group to the collection', function() { it('adds a the specified group to the collection', function() {
var group1, group2; var group1, group2;
ctrl.addGroup(group1 = $scope.$new()); ctrl.addGroup(group1 = $scope.$new());
ctrl.addGroup(group2 = $scope.$new()); ctrl.addGroup(group2 = $scope.$new());
expect(ctrl.groups.length).toBe(2); expect(ctrl.groups.length).toBe(2);
expect(ctrl.groups[0]).toBe(group1); expect(ctrl.groups[0]).toBe(group1);
expect(ctrl.groups[1]).toBe(group2); expect(ctrl.groups[1]).toBe(group2);
}); });
}); });
describe('closeOthers', function() { describe('closeOthers', function() {
var group1, group2, group3; var group1, group2, group3;
beforeEach(function() { beforeEach(function() {
ctrl.addGroup(group1 = { isOpen: true, $on : angular.noop }); ctrl.addGroup(group1 = { isOpen: true, $on : angular.noop });
ctrl.addGroup(group2 = { isOpen: true, $on : angular.noop }); ctrl.addGroup(group2 = { isOpen: true, $on : angular.noop });
ctrl.addGroup(group3 = { 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() { it('should close other groups if close-others attribute is not defined', function() {
delete $attrs.closeOthers; delete $attrs.closeOthers;
ctrl.closeOthers(group2); ctrl.closeOthers(group2);
expect(group1.isOpen).toBe(false); expect(group1.isOpen).toBe(false);
expect(group2.isOpen).toBe(true); expect(group2.isOpen).toBe(true);
expect(group3.isOpen).toBe(false); expect(group3.isOpen).toBe(false);
}); });
it('should close other groups if close-others attribute is true', function() { it('should close other groups if close-others attribute is true', function() {
$attrs.closeOthers = 'true'; $attrs.closeOthers = 'true';
ctrl.closeOthers(group3); ctrl.closeOthers(group3);
expect(group1.isOpen).toBe(false); expect(group1.isOpen).toBe(false);
expect(group2.isOpen).toBe(false); expect(group2.isOpen).toBe(false);
expect(group3.isOpen).toBe(true); expect(group3.isOpen).toBe(true);
}); });
it('should not close other groups if close-others attribute is false', function() { it('should not close other groups if close-others attribute is false', function() {
$attrs.closeOthers = 'false'; $attrs.closeOthers = 'false';
ctrl.closeOthers(group2); ctrl.closeOthers(group2);
expect(group1.isOpen).toBe(true); expect(group1.isOpen).toBe(true);
expect(group2.isOpen).toBe(true); expect(group2.isOpen).toBe(true);
expect(group3.isOpen).toBe(true); expect(group3.isOpen).toBe(true);
}); });
describe('setting accordionConfig', function() { describe('setting accordionConfig', function() {
var originalCloseOthers; var originalCloseOthers;
beforeEach(inject(function(accordionConfig) { beforeEach(inject(function(accordionConfig) {
originalCloseOthers = accordionConfig.closeOthers; originalCloseOthers = accordionConfig.closeOthers;
accordionConfig.closeOthers = false; accordionConfig.closeOthers = false;
})); }));
afterEach(inject(function(accordionConfig) { afterEach(inject(function(accordionConfig) {
// return it to the original value // return it to the original value
accordionConfig.closeOthers = originalCloseOthers; accordionConfig.closeOthers = originalCloseOthers;
})); }));
it('should not close other groups if accordionConfig.closeOthers is false', function() { it('should not close other groups if accordionConfig.closeOthers is false', function() {
ctrl.closeOthers(group2); ctrl.closeOthers(group2);
expect(group1.isOpen).toBe(true); expect(group1.isOpen).toBe(true);
expect(group2.isOpen).toBe(true); expect(group2.isOpen).toBe(true);
expect(group3.isOpen).toBe(true); expect(group3.isOpen).toBe(true);
}); });
}); });
}); });
describe('removeGroup', function() { describe('removeGroup', function() {
it('should remove the specified group', function () { it('should remove the specified group', function () {
var group1, group2, group3; var group1, group2, group3;
ctrl.addGroup(group1 = $scope.$new()); ctrl.addGroup(group1 = $scope.$new());
ctrl.addGroup(group2 = $scope.$new()); ctrl.addGroup(group2 = $scope.$new());
ctrl.addGroup(group3 = $scope.$new()); ctrl.addGroup(group3 = $scope.$new());
ctrl.removeGroup(group2); ctrl.removeGroup(group2);
expect(ctrl.groups.length).toBe(2); expect(ctrl.groups.length).toBe(2);
expect(ctrl.groups[0]).toBe(group1); expect(ctrl.groups[0]).toBe(group1);
expect(ctrl.groups[1]).toBe(group3); expect(ctrl.groups[1]).toBe(group3);
}); });
it('should ignore remove of non-existing group', function () { it('should ignore remove of non-existing group', function () {
var group1, group2; var group1, group2;
ctrl.addGroup(group1 = $scope.$new()); ctrl.addGroup(group1 = $scope.$new());
ctrl.addGroup(group2 = $scope.$new()); ctrl.addGroup(group2 = $scope.$new());
expect(ctrl.groups.length).toBe(2); expect(ctrl.groups.length).toBe(2);
ctrl.removeGroup({}); ctrl.removeGroup({});
expect(ctrl.groups.length).toBe(2); expect(ctrl.groups.length).toBe(2);
}); });
}); });
}); });
describe('accordion-group', function () { describe('accordion-group', function () {
var scope, $compile; var scope, $compile;
var element, groups; var element, groups;
var findGroupLink = function (index) { var findGroupLink = function (index) {
return groups.eq(index).find('a').eq(0); return groups.eq(index).find('a').eq(0);
}; };
var findGroupBody = function (index) { var findGroupBody = function (index) {
return groups.eq(index).find('.accordion-body').eq(0); return groups.eq(index).find('.accordion-body').eq(0);
}; };
beforeEach(inject(function(_$rootScope_, _$compile_) { beforeEach(inject(function(_$rootScope_, _$compile_) {
scope = _$rootScope_; scope = _$rootScope_;
$compile = _$compile_; $compile = _$compile_;
})); }));
afterEach(function () { afterEach(function () {
element = groups = scope = $compile = undefined; element = groups = scope = $compile = undefined;
}); });
describe('with static groups', function () { describe('with static groups', function () {
beforeEach(function () { beforeEach(function () {
var tpl = var tpl =
"<accordion>" + "<accordion>" +
"<accordion-group heading=\"title 1\">Content 1</accordion-group>" + "<accordion-group heading=\"title 1\">Content 1</accordion-group>" +
"<accordion-group heading=\"title 2\">Content 2</accordion-group>" + "<accordion-group heading=\"title 2\">Content 2</accordion-group>" +
"</accordion>"; "</accordion>";
element = angular.element(tpl); element = angular.element(tpl);
angular.element(document.body).append(element); angular.element(document.body).append(element);
$compile(element)(scope); $compile(element)(scope);
scope.$digest(); scope.$digest();
groups = element.find('.accordion-group'); groups = element.find('.accordion-group');
}); });
afterEach(function() { afterEach(function() {
element.remove(); element.remove();
}); });
it('should create accordion groups with content', function () { it('should create accordion groups with content', function () {
expect(groups.length).toEqual(2); expect(groups.length).toEqual(2);
expect(findGroupLink(0).text()).toEqual('title 1'); expect(findGroupLink(0).text()).toEqual('title 1');
expect(findGroupBody(0).text().trim()).toEqual('Content 1'); expect(findGroupBody(0).text().trim()).toEqual('Content 1');
expect(findGroupLink(1).text()).toEqual('title 2'); expect(findGroupLink(1).text()).toEqual('title 2');
expect(findGroupBody(1).text().trim()).toEqual('Content 2'); expect(findGroupBody(1).text().trim()).toEqual('Content 2');
}); });
it('should change selected element on click', function () { it('should change selected element on click', function () {
findGroupLink(0).click(); findGroupLink(0).click();
scope.$digest(); scope.$digest();
expect(findGroupBody(0).scope().isOpen).toBe(true); expect(findGroupBody(0).scope().isOpen).toBe(true);
findGroupLink(1).click(); findGroupLink(1).click();
scope.$digest(); scope.$digest();
expect(findGroupBody(0).scope().isOpen).toBe(false); expect(findGroupBody(0).scope().isOpen).toBe(false);
expect(findGroupBody(1).scope().isOpen).toBe(true); expect(findGroupBody(1).scope().isOpen).toBe(true);
}); });
it('should toggle element on click', function() { it('should toggle element on click', function() {
findGroupLink(0).click(); findGroupLink(0).click();
scope.$digest(); scope.$digest();
expect(findGroupBody(0).scope().isOpen).toBe(true); expect(findGroupBody(0).scope().isOpen).toBe(true);
findGroupLink(0).click(); findGroupLink(0).click();
scope.$digest(); scope.$digest();
expect(findGroupBody(0).scope().isOpen).toBe(false); expect(findGroupBody(0).scope().isOpen).toBe(false);
}); });
}); });
describe('with dynamic groups', function () { describe('with dynamic groups', function () {
var model; var model;
beforeEach(function () { beforeEach(function () {
var tpl = var tpl =
"<accordion>" + "<accordion>" +
"<accordion-group ng-repeat='group in groups' heading='{{group.name}}'>{{group.content}}</accordion-group>" + "<accordion-group ng-repeat='group in groups' heading='{{group.name}}'>{{group.content}}</accordion-group>" +
"</accordion>"; "</accordion>";
element = angular.element(tpl); element = angular.element(tpl);
model = [ model = [
{name: 'title 1', content: 'Content 1'}, {name: 'title 1', content: 'Content 1'},
{name: 'title 2', content: 'Content 2'} {name: 'title 2', content: 'Content 2'}
]; ];
$compile(element)(scope); $compile(element)(scope);
scope.$digest(); scope.$digest();
}); });
it('should have no groups initially', function () { it('should have no groups initially', function () {
groups = element.find('.accordion-group'); groups = element.find('.accordion-group');
expect(groups.length).toEqual(0); expect(groups.length).toEqual(0);
}); });
it('should have a group for each model item', function() { it('should have a group for each model item', function() {
scope.groups = model; scope.groups = model;
scope.$digest(); scope.$digest();
groups = element.find('.accordion-group'); groups = element.find('.accordion-group');
expect(groups.length).toEqual(2); expect(groups.length).toEqual(2);
expect(findGroupLink(0).text()).toEqual('title 1'); expect(findGroupLink(0).text()).toEqual('title 1');
expect(findGroupBody(0).text().trim()).toEqual('Content 1'); expect(findGroupBody(0).text().trim()).toEqual('Content 1');
expect(findGroupLink(1).text()).toEqual('title 2'); expect(findGroupLink(1).text()).toEqual('title 2');
expect(findGroupBody(1).text().trim()).toEqual('Content 2'); expect(findGroupBody(1).text().trim()).toEqual('Content 2');
}); });
it('should react properly on removing items from the model', function () { it('should react properly on removing items from the model', function () {
scope.groups = model; scope.groups = model;
scope.$digest(); scope.$digest();
groups = element.find('.accordion-group'); groups = element.find('.accordion-group');
expect(groups.length).toEqual(2); expect(groups.length).toEqual(2);
scope.groups.splice(0,1); scope.groups.splice(0,1);
scope.$digest(); scope.$digest();
groups = element.find('.accordion-group'); groups = element.find('.accordion-group');
expect(groups.length).toEqual(1); expect(groups.length).toEqual(1);
}); });
}); });
describe('is-open attribute', function() { describe('is-open attribute', function() {
beforeEach(function () { beforeEach(function () {
var tpl = var tpl =
"<accordion>" + "<accordion>" +
"<accordion-group heading=\"title 1\" is-open=\"open1\">Content 1</accordion-group>" + "<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-group heading=\"title 2\" is-open=\"open2\">Content 2</accordion-group>" +
"</accordion>"; "</accordion>";
element = angular.element(tpl); element = angular.element(tpl);
scope.open1 = false; scope.open1 = false;
scope.open2 = true; scope.open2 = true;
$compile(element)(scope); $compile(element)(scope);
scope.$digest(); scope.$digest();
groups = element.find('.accordion-group'); groups = element.find('.accordion-group');
}); });
it('should open the group with isOpen set to true', function () { it('should open the group with isOpen set to true', function () {
expect(findGroupBody(0).scope().isOpen).toBe(false); expect(findGroupBody(0).scope().isOpen).toBe(false);
expect(findGroupBody(1).scope().isOpen).toBe(true); expect(findGroupBody(1).scope().isOpen).toBe(true);
}); });
}); });
describe('is-open attribute with dynamic content', function() { describe('is-open attribute with dynamic content', function() {
beforeEach(function () { beforeEach(function () {
var tpl = var tpl =
"<accordion>" + "<accordion>" +
"<accordion-group heading=\"title 1\" is-open=\"open1\"><div ng-repeat='item in items'>{{item}}</div></accordion-group>" + "<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-group heading=\"title 2\" is-open=\"open2\">Static content</accordion-group>" +
"</accordion>"; "</accordion>";
element = angular.element(tpl); element = angular.element(tpl);
scope.items = ['Item 1', 'Item 2', 'Item 3']; scope.items = ['Item 1', 'Item 2', 'Item 3'];
scope.open1 = true; scope.open1 = true;
scope.open2 = false; scope.open2 = false;
angular.element(document.body).append(element); angular.element(document.body).append(element);
$compile(element)(scope); $compile(element)(scope);
scope.$digest(); scope.$digest();
groups = element.find('.accordion-group'); groups = element.find('.accordion-group');
}); });
afterEach(function() { afterEach(function() {
element.remove(); element.remove();
}); });
it('should have visible group body when the group with isOpen set to true', function () { 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(0)[0].clientHeight).not.toBe(0);
expect(findGroupBody(1)[0].clientHeight).toBe(0); expect(findGroupBody(1)[0].clientHeight).toBe(0);
}); });
}); });
describe('accordion-heading element', function() { describe('accordion-heading element', function() {
beforeEach(function() { beforeEach(function() {
var tpl = var tpl =
'<accordion ng-init="a = [1,2,3]">' + '<accordion ng-init="a = [1,2,3]">' +
'<accordion-group heading="I get overridden">' + '<accordion-group heading="I get overridden">' +
'<accordion-heading>Heading Element <span ng-repeat="x in a">{{x}}</span> </accordion-heading>' + '<accordion-heading>Heading Element <span ng-repeat="x in a">{{x}}</span> </accordion-heading>' +
'Body' + 'Body' +
'</accordion-group>' + '</accordion-group>' +
'</accordion>'; '</accordion>';
element = $compile(tpl)(scope); element = $compile(tpl)(scope);
scope.$digest(); scope.$digest();
groups = element.find('.accordion-group'); groups = element.find('.accordion-group');
}); });
it('transcludes the <accordion-heading> content into the heading link', function() { it('transcludes the <accordion-heading> content into the heading link', function() {
expect(findGroupLink(0).text()).toBe('Heading Element 123 '); expect(findGroupLink(0).text()).toBe('Heading Element 123 ');
}); });
it('attaches the same scope to the transcluded heading and body', function() { 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); expect(findGroupLink(0).find('span').scope().$id).toBe(findGroupBody(0).find('span').scope().$id);
}); });
}); });
describe('accordion-heading, with repeating accordion-groups', function() { describe('accordion-heading, with repeating accordion-groups', function() {
it('should clone the accordion-heading for each group', 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); element = $compile('<accordion><accordion-group ng-repeat="x in [1,2,3]"><accordion-heading>{{x}}</accordion-heading></accordion-group></accordion>')(scope);
scope.$digest(); scope.$digest();
groups = element.find('.accordion-group'); groups = element.find('.accordion-group');
expect(groups.length).toBe(3); expect(groups.length).toBe(3);
expect(findGroupLink(0).text()).toBe('1'); expect(findGroupLink(0).text()).toBe('1');
expect(findGroupLink(1).text()).toBe('2'); expect(findGroupLink(1).text()).toBe('2');
expect(findGroupLink(2).text()).toBe('3'); expect(findGroupLink(2).text()).toBe('3');
}); });
}); });
}); });
}); });
+75 -75
View File
@@ -1,76 +1,76 @@
angular.module('ui.bootstrap.buttons', []) angular.module('ui.bootstrap.buttons', [])
.constant('buttonConfig', { .constant('buttonConfig', {
activeClass:'active', activeClass:'active',
toggleEvent:'click' toggleEvent:'click'
}) })
.directive('btnRadio', ['buttonConfig', function (buttonConfig) { .directive('btnRadio', ['buttonConfig', function (buttonConfig) {
var activeClass = buttonConfig.activeClass || 'active'; var activeClass = buttonConfig.activeClass || 'active';
var toggleEvent = buttonConfig.toggleEvent || 'click'; var toggleEvent = buttonConfig.toggleEvent || 'click';
return { return {
require:'ngModel', require:'ngModel',
link:function (scope, element, attrs, ngModelCtrl) { link:function (scope, element, attrs, ngModelCtrl) {
var value = scope.$eval(attrs.btnRadio); var value = scope.$eval(attrs.btnRadio);
//model -> UI //model -> UI
scope.$watch(function () { scope.$watch(function () {
return ngModelCtrl.$modelValue; return ngModelCtrl.$modelValue;
}, function (modelValue) { }, function (modelValue) {
if (angular.equals(modelValue, value)){ if (angular.equals(modelValue, value)){
element.addClass(activeClass); element.addClass(activeClass);
} else { } else {
element.removeClass(activeClass); element.removeClass(activeClass);
} }
}); });
//ui->model //ui->model
element.bind(toggleEvent, function () { element.bind(toggleEvent, function () {
if (!element.hasClass(activeClass)) { if (!element.hasClass(activeClass)) {
scope.$apply(function () { scope.$apply(function () {
ngModelCtrl.$setViewValue(value); ngModelCtrl.$setViewValue(value);
}); });
} }
}); });
} }
}; };
}]) }])
.directive('btnCheckbox', ['buttonConfig', function (buttonConfig) { .directive('btnCheckbox', ['buttonConfig', function (buttonConfig) {
var activeClass = buttonConfig.activeClass || 'active'; var activeClass = buttonConfig.activeClass || 'active';
var toggleEvent = buttonConfig.toggleEvent || 'click'; var toggleEvent = buttonConfig.toggleEvent || 'click';
return { return {
require:'ngModel', require:'ngModel',
link:function (scope, element, attrs, ngModelCtrl) { link:function (scope, element, attrs, ngModelCtrl) {
var trueValue = scope.$eval(attrs.btnCheckboxTrue); var trueValue = scope.$eval(attrs.btnCheckboxTrue);
var falseValue = scope.$eval(attrs.btnCheckboxFalse); var falseValue = scope.$eval(attrs.btnCheckboxFalse);
trueValue = angular.isDefined(trueValue) ? trueValue : true; trueValue = angular.isDefined(trueValue) ? trueValue : true;
falseValue = angular.isDefined(falseValue) ? falseValue : false; falseValue = angular.isDefined(falseValue) ? falseValue : false;
//model -> UI //model -> UI
scope.$watch(function () { scope.$watch(function () {
return ngModelCtrl.$modelValue; return ngModelCtrl.$modelValue;
}, function (modelValue) { }, function (modelValue) {
if (angular.equals(modelValue, trueValue)) { if (angular.equals(modelValue, trueValue)) {
element.addClass(activeClass); element.addClass(activeClass);
} else { } else {
element.removeClass(activeClass); element.removeClass(activeClass);
} }
}); });
//ui->model //ui->model
element.bind(toggleEvent, function () { element.bind(toggleEvent, function () {
scope.$apply(function () { scope.$apply(function () {
ngModelCtrl.$setViewValue(element.hasClass(activeClass) ? falseValue : trueValue); ngModelCtrl.$setViewValue(element.hasClass(activeClass) ? falseValue : trueValue);
}); });
}); });
} }
}; };
}]); }]);
+20 -20
View File
@@ -1,21 +1,21 @@
<div ng-controller="ButtonsCtrl"> <div ng-controller="ButtonsCtrl">
<h4>Single toggle</h4> <h4>Single toggle</h4>
<pre>{{singleModel}}</pre> <pre>{{singleModel}}</pre>
<button type="button" class="btn btn-primary" ng-model="singleModel" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0"> <button type="button" class="btn btn-primary" ng-model="singleModel" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
Single Toggle Single Toggle
</button> </button>
<h4>Checkbox</h4> <h4>Checkbox</h4>
<pre>{{checkModel}}</pre> <pre>{{checkModel}}</pre>
<div class="btn-group"> <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.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.middle" btn-checkbox>Middle</button>
<button type="button" class="btn btn-primary" ng-model="checkModel.right" btn-checkbox>Right</button> <button type="button" class="btn btn-primary" ng-model="checkModel.right" btn-checkbox>Right</button>
</div> </div>
<h4>Radio</h4> <h4>Radio</h4>
<pre>{{radioModel}}</pre> <pre>{{radioModel}}</pre>
<div class="btn-group"> <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="'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="'Middle'">Middle</button>
<button type="button" class="btn btn-primary" ng-model="radioModel" btn-radio="'Right'">Right</button> <button type="button" class="btn btn-primary" ng-model="radioModel" btn-radio="'Right'">Right</button>
</div> </div>
</div> </div>
+11 -11
View File
@@ -1,12 +1,12 @@
var ButtonsCtrl = function ($scope) { var ButtonsCtrl = function ($scope) {
$scope.singleModel = 1; $scope.singleModel = 1;
$scope.radioModel = 'Middle'; $scope.radioModel = 'Middle';
$scope.checkModel = { $scope.checkModel = {
left: false, left: false,
middle: true, middle: true,
right: false right: false
}; };
}; };
+2 -2
View File
@@ -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.
+93 -93
View File
@@ -1,94 +1,94 @@
describe('buttons', function () { describe('buttons', function () {
var $scope, $compile; var $scope, $compile;
beforeEach(module('ui.bootstrap.buttons')); beforeEach(module('ui.bootstrap.buttons'));
beforeEach(inject(function (_$rootScope_, _$compile_) { beforeEach(inject(function (_$rootScope_, _$compile_) {
$scope = _$rootScope_; $scope = _$rootScope_;
$compile = _$compile_; $compile = _$compile_;
})); }));
describe('checkbox', function () { describe('checkbox', function () {
var compileButton = function (markup, scope) { var compileButton = function (markup, scope) {
var el = $compile(markup)(scope); var el = $compile(markup)(scope);
scope.$digest(); scope.$digest();
return el; return el;
}; };
//model -> UI //model -> UI
it('should work correctly with default model values', function () { it('should work correctly with default model values', function () {
$scope.model = false; $scope.model = false;
var btn = compileButton('<button ng-model="model" btn-checkbox>click</button>', $scope); var btn = compileButton('<button ng-model="model" btn-checkbox>click</button>', $scope);
expect(btn).not.toHaveClass('active'); expect(btn).not.toHaveClass('active');
$scope.model = true; $scope.model = true;
$scope.$digest(); $scope.$digest();
expect(btn).toHaveClass('active'); expect(btn).toHaveClass('active');
}); });
it('should bind custom model values', function () { it('should bind custom model values', function () {
$scope.model = 1; $scope.model = 1;
var btn = compileButton('<button ng-model="model" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">click</button>', $scope); var btn = compileButton('<button ng-model="model" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">click</button>', $scope);
expect(btn).toHaveClass('active'); expect(btn).toHaveClass('active');
$scope.model = 0; $scope.model = 0;
$scope.$digest(); $scope.$digest();
expect(btn).not.toHaveClass('active'); expect(btn).not.toHaveClass('active');
}); });
//UI-> model //UI-> model
it('should toggle default model values on click', function () { it('should toggle default model values on click', function () {
$scope.model = false; $scope.model = false;
var btn = compileButton('<button ng-model="model" btn-checkbox>click</button>', $scope); var btn = compileButton('<button ng-model="model" btn-checkbox>click</button>', $scope);
btn.click(); btn.click();
expect($scope.model).toEqual(true); expect($scope.model).toEqual(true);
btn.click(); btn.click();
expect($scope.model).toEqual(false); expect($scope.model).toEqual(false);
}); });
it('should toggle custom model values on click', function () { it('should toggle custom model values on click', function () {
$scope.model = 0; $scope.model = 0;
var btn = compileButton('<button ng-model="model" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">click</button>', $scope); var btn = compileButton('<button ng-model="model" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">click</button>', $scope);
btn.click(); btn.click();
expect($scope.model).toEqual(1); expect($scope.model).toEqual(1);
btn.click(); btn.click();
expect($scope.model).toEqual(0); expect($scope.model).toEqual(0);
}); });
}); });
describe('radio', function () { describe('radio', function () {
var compileButtons = function (markup, scope) { var compileButtons = function (markup, scope) {
var el = $compile('<div>'+markup+'</div>')(scope); var el = $compile('<div>'+markup+'</div>')(scope);
scope.$digest(); scope.$digest();
return el.find('button'); return el.find('button');
}; };
//model -> UI //model -> UI
it('should work correctly set active class based on model', function () { 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); 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(0)).not.toHaveClass('active');
expect(btns.eq(1)).not.toHaveClass('active'); expect(btns.eq(1)).not.toHaveClass('active');
$scope.model = 2; $scope.model = 2;
$scope.$digest(); $scope.$digest();
expect(btns.eq(0)).not.toHaveClass('active'); expect(btns.eq(0)).not.toHaveClass('active');
expect(btns.eq(1)).toHaveClass('active'); expect(btns.eq(1)).toHaveClass('active');
}); });
//UI->model //UI->model
it('should work correctly set active class based on model', function () { 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); 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(); expect($scope.model).toBeUndefined();
btns.eq(0).click(); btns.eq(0).click();
expect($scope.model).toEqual(1); expect($scope.model).toEqual(1);
btns.eq(1).click(); btns.eq(1).click();
expect($scope.model).toEqual(2); expect($scope.model).toEqual(2);
}); });
}); });
}); });
+15 -15
View File
@@ -1,16 +1,16 @@
<div ng-controller="ModalDemoCtrl"> <div ng-controller="ModalDemoCtrl">
<button class="btn" ng-click="open()">Open me!</button> <button class="btn" ng-click="open()">Open me!</button>
<div modal="shouldBeOpen" close="close()" options="opts"> <div modal="shouldBeOpen" close="close()" options="opts">
<div class="modal-header"> <div class="modal-header">
<h3>I'm a modal!</h3> <h3>I'm a modal!</h3>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<ul> <ul>
<li ng-repeat="item in items">{{item}}</li> <li ng-repeat="item in items">{{item}}</li>
</ul> </ul>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn btn-warning cancel" ng-click="close()">Cancel</button> <button class="btn btn-warning cancel" ng-click="close()">Cancel</button>
</div> </div>
</div> </div>
</div> </div>
+8 -8
View File
@@ -1,9 +1,9 @@
<div ng-controller="PaginationDemoCtrl"> <div ng-controller="PaginationDemoCtrl">
<pagination num-pages="noOfPages" current-page="currentPage"></pagination> <pagination num-pages="noOfPages" current-page="currentPage"></pagination>
<pagination num-pages="noOfPages" current-page="currentPage" class="pagination-small" previous-text="&laquo;" next-text="&raquo;"></pagination> <pagination num-pages="noOfPages" current-page="currentPage" class="pagination-small" previous-text="&laquo;" next-text="&raquo;"></pagination>
<pagination boundary-links="true" num-pages="noOfPages" current-page="currentPage" max-size="maxSize"></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 num-pages="noOfPages" current-page="currentPage" max-size="maxSize"></pagination>
<pagination direction-links="false" num-pages="noOfPages" current-page="currentPage"></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> <button class="btn" ng-click="setPage(3)">Set current page to: 3</button>
The selected page no: {{currentPage}} The selected page no: {{currentPage}}
</div> </div>
+9 -9
View File
@@ -1,9 +1,9 @@
var PaginationDemoCtrl = function ($scope) { var PaginationDemoCtrl = function ($scope) {
$scope.noOfPages = 7; $scope.noOfPages = 7;
$scope.currentPage = 4; $scope.currentPage = 4;
$scope.maxSize = 5; $scope.maxSize = 5;
$scope.setPage = function (pageNo) { $scope.setPage = function (pageNo) {
$scope.currentPage = pageNo; $scope.currentPage = pageNo;
}; };
}; };
+4 -4
View File
@@ -1,5 +1,5 @@
A lightweight pagination directive that is focused on ... providing pagination! 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 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. It also provides optional attribute max-size to limit the size of pagination bar.
+79 -79
View File
@@ -1,79 +1,79 @@
angular.module('ui.bootstrap.position', []) angular.module('ui.bootstrap.position', [])
/** /**
* A set of utility methods that can be use to retrieve position of DOM elements. * 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 * 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, * relation to other, existing elements (this is the case for tooltips, popovers,
* typeahead suggestions etc.). * typeahead suggestions etc.).
*/ */
.factory('$position', ['$document', '$window', function ($document, $window) { .factory('$position', ['$document', '$window', function ($document, $window) {
function getStyle(el, cssprop) { function getStyle(el, cssprop) {
if (el.currentStyle) { //IE if (el.currentStyle) { //IE
return el.currentStyle[cssprop]; return el.currentStyle[cssprop];
} else if ($window.getComputedStyle) { } else if ($window.getComputedStyle) {
return $window.getComputedStyle(el)[cssprop]; return $window.getComputedStyle(el)[cssprop];
} }
// finally try and get inline style // finally try and get inline style
return el.style[cssprop]; return el.style[cssprop];
} }
/** /**
* Checks if a given element is statically positioned * Checks if a given element is statically positioned
* @param element - raw DOM element * @param element - raw DOM element
*/ */
function isStaticPositioned(element) { function isStaticPositioned(element) {
return (getStyle(element, "position") || 'static' ) === 'static'; return (getStyle(element, "position") || 'static' ) === 'static';
} }
/** /**
* returns the closest, non-statically positioned parentOffset of a given element * returns the closest, non-statically positioned parentOffset of a given element
* @param element * @param element
*/ */
var parentOffsetEl = function (element) { var parentOffsetEl = function (element) {
var docDomEl = $document[0]; var docDomEl = $document[0];
var offsetParent = element.offsetParent || docDomEl; var offsetParent = element.offsetParent || docDomEl;
while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) { while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) {
offsetParent = offsetParent.offsetParent; offsetParent = offsetParent.offsetParent;
} }
return offsetParent || docDomEl; return offsetParent || docDomEl;
}; };
return { return {
/** /**
* Provides read-only equivalent of jQuery's position function: * Provides read-only equivalent of jQuery's position function:
* http://api.jquery.com/position/ * http://api.jquery.com/position/
*/ */
position: function (element) { position: function (element) {
var elBCR = this.offset(element); var elBCR = this.offset(element);
var offsetParentBCR = { top: 0, left: 0 }; var offsetParentBCR = { top: 0, left: 0 };
var offsetParentEl = parentOffsetEl(element[0]); var offsetParentEl = parentOffsetEl(element[0]);
if (offsetParentEl != $document[0]) { if (offsetParentEl != $document[0]) {
offsetParentBCR = this.offset(angular.element(offsetParentEl)); offsetParentBCR = this.offset(angular.element(offsetParentEl));
offsetParentBCR.top += offsetParentEl.clientTop; offsetParentBCR.top += offsetParentEl.clientTop;
offsetParentBCR.left += offsetParentEl.clientLeft; offsetParentBCR.left += offsetParentEl.clientLeft;
} }
return { return {
width: element.prop('offsetWidth'), width: element.prop('offsetWidth'),
height: element.prop('offsetHeight'), height: element.prop('offsetHeight'),
top: elBCR.top - offsetParentBCR.top, top: elBCR.top - offsetParentBCR.top,
left: elBCR.left - offsetParentBCR.left left: elBCR.left - offsetParentBCR.left
}; };
}, },
/** /**
* Provides read-only equivalent of jQuery's offset function: * Provides read-only equivalent of jQuery's offset function:
* http://api.jquery.com/offset/ * http://api.jquery.com/offset/
*/ */
offset: function (element) { offset: function (element) {
var boundingClientRect = element[0].getBoundingClientRect(); var boundingClientRect = element[0].getBoundingClientRect();
return { return {
width: element.prop('offsetWidth'), width: element.prop('offsetWidth'),
height: element.prop('offsetHeight'), height: element.prop('offsetHeight'),
top: boundingClientRect.top + ($window.pageYOffset || $document[0].body.scrollTop), top: boundingClientRect.top + ($window.pageYOffset || $document[0].body.scrollTop),
left: boundingClientRect.left + ($window.pageXOffset || $document[0].body.scrollLeft) left: boundingClientRect.left + ($window.pageXOffset || $document[0].body.scrollLeft)
}; };
} }
}; };
}]); }]);
+118 -118
View File
@@ -1,118 +1,118 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" ng-app="position"> <html lang="en" ng-app="position">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<script src="../../../misc/test-lib/angular.js"></script> <script src="../../../misc/test-lib/angular.js"></script>
<script src="../position.js"></script> <script src="../position.js"></script>
<style type="text/css"> <style type="text/css">
.container { .container {
border: 1px solid red; border: 1px solid red;
} }
.container-relative { .container-relative {
border: 1px solid red; border: 1px solid red;
position: relative; position: relative;
} }
.content { .content {
border: 5px solid #808080; border: 5px solid #808080;
background-color: dodgerblue; background-color: dodgerblue;
width: 200px; width: 200px;
} }
.positioned { .positioned {
border: 5px solid #808080; border: 5px solid #808080;
background-color: green; background-color: green;
position: absolute; position: absolute;
} }
</style> </style>
<script type="text/javascript"> <script type="text/javascript">
angular.module('position', ['ui.bootstrap.position']).directive('position', function ($compile, $position) { angular.module('position', ['ui.bootstrap.position']).directive('position', function ($compile, $position) {
return { return {
link: function (scope, element, attrs) { link: function (scope, element, attrs) {
var positionedEl = angular.element('<div class="positioned">Positioned</div>'); var positionedEl = angular.element('<div class="positioned">Positioned</div>');
var elPosition = $position.position(element); var elPosition = $position.position(element);
elPosition.left += elPosition.width; elPosition.left += elPosition.width;
positionedEl.css({left: elPosition.left + 'px', top: elPosition.top + 'px'}); positionedEl.css({left: elPosition.left + 'px', top: elPosition.top + 'px'});
element.after($compile(positionedEl)(scope)); element.after($compile(positionedEl)(scope));
} }
}; };
}); });
</script> </script>
</head> </head>
<body class="container"> <body class="container">
<h3>Within body</h3> <h3>Within body</h3>
<div class="content" position>Content</div> <div class="content" position>Content</div>
<h3>Within statically positioned DIV</h3> <h3>Within statically positioned DIV</h3>
<div class="container"> <div class="container">
<div class="content" position>Content</div> <div class="content" position>Content</div>
</div> </div>
<h3>Within relative-positioned DIV - position specified in CSS</h3> <h3>Within relative-positioned DIV - position specified in CSS</h3>
<div class="container-relative"> <div class="container-relative">
<div class="content" position>Content</div> <div class="content" position>Content</div>
</div> </div>
<h3>Within relative-positioned DIV</h3> <h3>Within relative-positioned DIV</h3>
<div style="position: relative; left: 200px" class="container"> <div style="position: relative; left: 200px" class="container">
<div class="content" position>Content</div> <div class="content" position>Content</div>
</div> </div>
<h3>Within absolute-positioned DIV</h3> <h3>Within absolute-positioned DIV</h3>
<div style="position: absolute; left: 400px" class="container"> <div style="position: absolute; left: 400px" class="container">
<div class="content" position>Content</div> <div class="content" position>Content</div>
</div> </div>
<h3>Next to a float element</h3> <h3>Next to a float element</h3>
<div class="container"> <div class="container">
<div class="content" style="float: right" position>Content</div> <div class="content" style="float: right" position>Content</div>
</div> </div>
<h3>Within a table</h3> <h3>Within a table</h3>
<table class="container"> <table class="container">
<tr> <tr>
<td>Some other content</td> <td>Some other content</td>
<td> <td>
<div class="content" position>Content</div> <div class="content" position>Content</div>
</td> </td>
</tr> </tr>
</table> </table>
<h3>Within a table that is inside a relative-positioned DIV</h3> <h3>Within a table that is inside a relative-positioned DIV</h3>
<div style="position: relative; left: 200px" class="container"> <div style="position: relative; left: 200px" class="container">
<table class="container"> <table class="container">
<tr> <tr>
<td>Some other content</td> <td>Some other content</td>
<td> <td>
<div class="content" position>Content</div> <div class="content" position>Content</div>
</td> </td>
</tr> </tr>
</table> </table>
</div> </div>
<h3>Inside looong text</h3> <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>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>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>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>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> <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> <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> <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"> <div style="position: fixed; bottom: 0px" class="container">
<h3>Within fixed div</h3> <h3>Within fixed div</h3>
<div class="content" position>Content</div> <div class="content" position>Content</div>
</div> </div>
</body> </body>
</html> </html>
+23 -23
View File
@@ -1,23 +1,23 @@
<div ng-controller="TabsDemoCtrl"> <div ng-controller="TabsDemoCtrl">
Select a tab by setting active binding to true: Select a tab by setting active binding to true:
<br /> <br />
<button class="btn" ng-click="tabs[0].active = true"> <button class="btn" ng-click="tabs[0].active = true">
Select second tab Select second tab
</button> </button>
<button class="btn" ng-click="tabs[1].active = true"> <button class="btn" ng-click="tabs[1].active = true">
Select third tab Select third tab
</button> </button>
<br /><br /> <br /><br />
<tabset> <tabset>
<tab heading="Static title">Static content</tab> <tab heading="Static title">Static content</tab>
<tab ng-repeat="tab in tabs" heading="{{tab.title}}" active="tab.active"> <tab ng-repeat="tab in tabs" heading="{{tab.title}}" active="tab.active">
{{tab.content}} {{tab.content}}
</tab> </tab>
<tab select="alertMe()"> <tab select="alertMe()">
<tab-heading> <tab-heading>
<i class="icon-bell"></i> Select me for alert! <i class="icon-bell"></i> Select me for alert!
</tab-heading> </tab-heading>
I've got an HTML heading, and a select callback. Pretty cool! I've got an HTML heading, and a select callback. Pretty cool!
</tab> </tab>
</tabset> </tabset>
</div> </div>
+12 -12
View File
@@ -1,12 +1,12 @@
var TabsDemoCtrl = function ($scope) { var TabsDemoCtrl = function ($scope) {
$scope.tabs = [ $scope.tabs = [
{ title:"Dynamic Title 1", content:"Dynamic content 1" }, { title:"Dynamic Title 1", content:"Dynamic content 1" },
{ title:"Dynamic Title 2", content:"Dynamic content 2" } { title:"Dynamic Title 2", content:"Dynamic content 2" }
]; ];
$scope.alertMe = function() { $scope.alertMe = function() {
setTimeout(function() { setTimeout(function() {
alert("You've selected the alert tab!"); alert("You've selected the alert tab!");
}); });
}; };
}; };
+3 -3
View File
@@ -1,4 +1,4 @@
<div class='container-fluid' ng-controller="TypeaheadCtrl"> <div class='container-fluid' ng-controller="TypeaheadCtrl">
<pre>Model: {{selected| json}}</pre> <pre>Model: {{selected| json}}</pre>
<input type="text" ng-model="selected" typeahead="state for state in states | filter:$viewValue"> <input type="text" ng-model="selected" typeahead="state for state in states | filter:$viewValue">
</div> </div>
+4 -4
View File
@@ -1,5 +1,5 @@
function TypeaheadCtrl($scope) { function TypeaheadCtrl($scope) {
$scope.selected = undefined; $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']; $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'];
} }
+7 -7
View File
@@ -1,8 +1,8 @@
Typeahead is a AngularJS version of [Twitter Bootstrap typeahead plugin](http://twitter.github.com/bootstrap/javascript.html#typeahead) 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. 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 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) * 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 * works with promises and it means that you can retrieve matches using the `$http` service with minimal effort
+402 -402
View File
@@ -1,403 +1,403 @@
describe('typeahead tests', function () { describe('typeahead tests', function () {
beforeEach(module('ui.bootstrap.typeahead')); beforeEach(module('ui.bootstrap.typeahead'));
beforeEach(module('template/typeahead/typeahead.html')); beforeEach(module('template/typeahead/typeahead.html'));
describe('syntax parser', function () { describe('syntax parser', function () {
var typeaheadParser, scope, filterFilter; var typeaheadParser, scope, filterFilter;
beforeEach(inject(function (_$rootScope_, _filterFilter_, _typeaheadParser_) { beforeEach(inject(function (_$rootScope_, _filterFilter_, _typeaheadParser_) {
typeaheadParser = _typeaheadParser_; typeaheadParser = _typeaheadParser_;
scope = _$rootScope_; scope = _$rootScope_;
filterFilter = _filterFilter_; filterFilter = _filterFilter_;
})); }));
it('should parse the simplest array-based syntax', function () { it('should parse the simplest array-based syntax', function () {
scope.states = ['Alabama', 'California', 'Delaware']; scope.states = ['Alabama', 'California', 'Delaware'];
var result = typeaheadParser.parse('state for state in states | filter:$viewValue'); var result = typeaheadParser.parse('state for state in states | filter:$viewValue');
var itemName = result.itemName; var itemName = result.itemName;
var locals = {$viewValue:'al'}; var locals = {$viewValue:'al'};
expect(result.source(scope, locals)).toEqual(['Alabama', 'California']); expect(result.source(scope, locals)).toEqual(['Alabama', 'California']);
locals[itemName] = 'Alabama'; locals[itemName] = 'Alabama';
expect(result.viewMapper(scope, locals)).toEqual('Alabama'); expect(result.viewMapper(scope, locals)).toEqual('Alabama');
expect(result.modelMapper(scope, locals)).toEqual('Alabama'); expect(result.modelMapper(scope, locals)).toEqual('Alabama');
}); });
it('should parse the simplest function-based syntax', function () { it('should parse the simplest function-based syntax', function () {
scope.getStates = function ($viewValue) { scope.getStates = function ($viewValue) {
return filterFilter(['Alabama', 'California', 'Delaware'], $viewValue); return filterFilter(['Alabama', 'California', 'Delaware'], $viewValue);
}; };
var result = typeaheadParser.parse('state for state in getStates($viewValue)'); var result = typeaheadParser.parse('state for state in getStates($viewValue)');
var itemName = result.itemName; var itemName = result.itemName;
var locals = {$viewValue:'al'}; var locals = {$viewValue:'al'};
expect(result.source(scope, locals)).toEqual(['Alabama', 'California']); expect(result.source(scope, locals)).toEqual(['Alabama', 'California']);
locals[itemName] = 'Alabama'; locals[itemName] = 'Alabama';
expect(result.viewMapper(scope, locals)).toEqual('Alabama'); expect(result.viewMapper(scope, locals)).toEqual('Alabama');
expect(result.modelMapper(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 () { it('should allow to specify custom model mapping that is used as a label as well', function () {
scope.states = [ scope.states = [
{code:'AL', name:'Alabama'}, {code:'AL', name:'Alabama'},
{code:'CA', name:'California'}, {code:'CA', name:'California'},
{code:'DE', name:'Delaware'} {code:'DE', name:'Delaware'}
]; ];
var result = typeaheadParser.parse("state.name for state in states | filter:$viewValue | orderBy:'name':true"); var result = typeaheadParser.parse("state.name for state in states | filter:$viewValue | orderBy:'name':true");
var itemName = result.itemName; var itemName = result.itemName;
expect(itemName).toEqual('state'); expect(itemName).toEqual('state');
expect(result.source(scope, {$viewValue:'al'})).toEqual([ expect(result.source(scope, {$viewValue:'al'})).toEqual([
{code:'CA', name:'California'}, {code:'CA', name:'California'},
{code:'AL', name:'Alabama'} {code:'AL', name:'Alabama'}
]); ]);
var locals = {$viewValue:'al'}; var locals = {$viewValue:'al'};
locals[itemName] = {code:'AL', name:'Alabama'}; locals[itemName] = {code:'AL', name:'Alabama'};
expect(result.viewMapper(scope, locals)).toEqual('Alabama'); expect(result.viewMapper(scope, locals)).toEqual('Alabama');
expect(result.modelMapper(scope, locals)).toEqual('Alabama'); expect(result.modelMapper(scope, locals)).toEqual('Alabama');
}); });
it('should allow to specify custom view and model mappers', function () { it('should allow to specify custom view and model mappers', function () {
scope.states = [ scope.states = [
{code:'AL', name:'Alabama'}, {code:'AL', name:'Alabama'},
{code:'CA', name:'California'}, {code:'CA', name:'California'},
{code:'DE', name:'Delaware'} {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 result = typeaheadParser.parse("state.code as state.name + ' ('+state.code+')' for state in states | filter:$viewValue | orderBy:'name':true");
var itemName = result.itemName; var itemName = result.itemName;
expect(result.source(scope, {$viewValue:'al'})).toEqual([ expect(result.source(scope, {$viewValue:'al'})).toEqual([
{code:'CA', name:'California'}, {code:'CA', name:'California'},
{code:'AL', name:'Alabama'} {code:'AL', name:'Alabama'}
]); ]);
var locals = {$viewValue:'al'}; var locals = {$viewValue:'al'};
locals[itemName] = {code:'AL', name:'Alabama'}; locals[itemName] = {code:'AL', name:'Alabama'};
expect(result.viewMapper(scope, locals)).toEqual('Alabama (AL)'); expect(result.viewMapper(scope, locals)).toEqual('Alabama (AL)');
expect(result.modelMapper(scope, locals)).toEqual('AL'); expect(result.modelMapper(scope, locals)).toEqual('AL');
}); });
}); });
describe('typeaheadPopup - result rendering', function () { describe('typeaheadPopup - result rendering', function () {
var scope, $rootScope, $compile; var scope, $rootScope, $compile;
beforeEach(inject(function (_$rootScope_, _$compile_) { beforeEach(inject(function (_$rootScope_, _$compile_) {
$rootScope = _$rootScope_; $rootScope = _$rootScope_;
scope = $rootScope.$new(); scope = $rootScope.$new();
$compile = _$compile_; $compile = _$compile_;
})); }));
it('should render initial results', function () { it('should render initial results', function () {
scope.matches = ['foo', 'bar', 'baz']; scope.matches = ['foo', 'bar', 'baz'];
scope.active = 1; scope.active = 1;
var el = $compile("<div><typeahead-popup matches='matches' active='active' select='select(activeIdx)'></typeahead-popup></div>")(scope); var el = $compile("<div><typeahead-popup matches='matches' active='active' select='select(activeIdx)'></typeahead-popup></div>")(scope);
$rootScope.$digest(); $rootScope.$digest();
var liElems = el.find('li'); var liElems = el.find('li');
expect(liElems.length).toEqual(3); expect(liElems.length).toEqual(3);
expect(liElems.eq(0)).not.toHaveClass('active'); expect(liElems.eq(0)).not.toHaveClass('active');
expect(liElems.eq(1)).toHaveClass('active'); expect(liElems.eq(1)).toHaveClass('active');
expect(liElems.eq(2)).not.toHaveClass('active'); expect(liElems.eq(2)).not.toHaveClass('active');
}); });
it('should change active item on mouseenter', function () { it('should change active item on mouseenter', function () {
scope.matches = ['foo', 'bar', 'baz']; scope.matches = ['foo', 'bar', 'baz'];
scope.active = 1; scope.active = 1;
var el = $compile("<div><typeahead-popup matches='matches' active='active' select='select(activeIdx)'></typeahead-popup></div>")(scope); var el = $compile("<div><typeahead-popup matches='matches' active='active' select='select(activeIdx)'></typeahead-popup></div>")(scope);
$rootScope.$digest(); $rootScope.$digest();
var liElems = el.find('li'); var liElems = el.find('li');
expect(liElems.eq(1)).toHaveClass('active'); expect(liElems.eq(1)).toHaveClass('active');
expect(liElems.eq(2)).not.toHaveClass('active'); expect(liElems.eq(2)).not.toHaveClass('active');
liElems.eq(2).trigger('mouseenter'); liElems.eq(2).trigger('mouseenter');
expect(liElems.eq(1)).not.toHaveClass('active'); expect(liElems.eq(1)).not.toHaveClass('active');
expect(liElems.eq(2)).toHaveClass('active'); expect(liElems.eq(2)).toHaveClass('active');
}); });
it('should select an item on mouse click', function () { it('should select an item on mouse click', function () {
scope.matches = ['foo', 'bar', 'baz']; scope.matches = ['foo', 'bar', 'baz'];
scope.active = 1; scope.active = 1;
$rootScope.select = angular.noop; $rootScope.select = angular.noop;
spyOn($rootScope, 'select'); spyOn($rootScope, 'select');
var el = $compile("<div><typeahead-popup matches='matches' active='active' select='select(activeIdx)'></typeahead-popup></div>")(scope); var el = $compile("<div><typeahead-popup matches='matches' active='active' select='select(activeIdx)'></typeahead-popup></div>")(scope);
$rootScope.$digest(); $rootScope.$digest();
var liElems = el.find('li'); var liElems = el.find('li');
liElems.eq(2).find('a').trigger('click'); liElems.eq(2).find('a').trigger('click');
expect($rootScope.select).toHaveBeenCalledWith(2); expect($rootScope.select).toHaveBeenCalledWith(2);
}); });
}); });
describe('typeaheadHighlight', function () { describe('typeaheadHighlight', function () {
var highlightFilter; var highlightFilter;
beforeEach(inject(function (typeaheadHighlightFilter) { beforeEach(inject(function (typeaheadHighlightFilter) {
highlightFilter = typeaheadHighlightFilter; highlightFilter = typeaheadHighlightFilter;
})); }));
it('should higlight a match', function () { it('should higlight a match', function () {
expect(highlightFilter('before match after', 'match')).toEqual('before <strong>match</strong> after'); expect(highlightFilter('before match after', 'match')).toEqual('before <strong>match</strong> after');
}); });
it('should higlight a match with mixed case', function () { it('should higlight a match with mixed case', function () {
expect(highlightFilter('before MaTch after', 'match')).toEqual('before <strong>MaTch</strong> after'); expect(highlightFilter('before MaTch after', 'match')).toEqual('before <strong>MaTch</strong> after');
}); });
it('should higlight all matches', function () { it('should higlight all matches', function () {
expect(highlightFilter('before MaTch after match', 'match')).toEqual('before <strong>MaTch</strong> after <strong>match</strong>'); expect(highlightFilter('before MaTch after match', 'match')).toEqual('before <strong>MaTch</strong> after <strong>match</strong>');
}); });
it('should do nothing if no match', function () { it('should do nothing if no match', function () {
expect(highlightFilter('before match after', 'nomatch')).toEqual('before match after'); expect(highlightFilter('before match after', 'nomatch')).toEqual('before match after');
}); });
it('issue 316 - should work correctly for regexp reserved words', function () { it('issue 316 - should work correctly for regexp reserved words', function () {
expect(highlightFilter('before (match after', '(match')).toEqual('before <strong>(match</strong> after'); expect(highlightFilter('before (match after', '(match')).toEqual('before <strong>(match</strong> after');
}); });
}); });
describe('typeahead', function () { describe('typeahead', function () {
var $scope, $compile, $document; var $scope, $compile, $document;
var changeInputValueTo; var changeInputValueTo;
beforeEach(inject(function (_$rootScope_, _$compile_, _$document_, $sniffer) { beforeEach(inject(function (_$rootScope_, _$compile_, _$document_, $sniffer) {
$scope = _$rootScope_; $scope = _$rootScope_;
$scope.source = ['foo', 'bar', 'baz']; $scope.source = ['foo', 'bar', 'baz'];
$compile = _$compile_; $compile = _$compile_;
$document = _$document_; $document = _$document_;
changeInputValueTo = function (element, value) { changeInputValueTo = function (element, value) {
var inputEl = findInput(element); var inputEl = findInput(element);
inputEl.val(value); inputEl.val(value);
inputEl.trigger($sniffer.hasEvent('input') ? 'input' : 'change'); inputEl.trigger($sniffer.hasEvent('input') ? 'input' : 'change');
$scope.$digest(); $scope.$digest();
}; };
})); }));
//utility functions //utility functions
var prepareInputEl = function(inputTpl) { var prepareInputEl = function(inputTpl) {
var el = $compile(angular.element(inputTpl))($scope); var el = $compile(angular.element(inputTpl))($scope);
$scope.$digest(); $scope.$digest();
return el; return el;
}; };
var findInput = function(element) { var findInput = function(element) {
return element.find('input'); return element.find('input');
}; };
var findDropDown = function(element) { var findDropDown = function(element) {
return element.find('ul.typeahead'); return element.find('ul.typeahead');
}; };
var findMatches = function(element) { var findMatches = function(element) {
return findDropDown(element).find('li'); return findDropDown(element).find('li');
}; };
var triggerKeyDown = function(element, keyCode) { var triggerKeyDown = function(element, keyCode) {
var inputEl = findInput(element); var inputEl = findInput(element);
var e = $.Event("keydown"); var e = $.Event("keydown");
e.which = keyCode; e.which = keyCode;
inputEl.trigger(e); inputEl.trigger(e);
}; };
//custom matchers //custom matchers
beforeEach(function () { beforeEach(function () {
this.addMatchers({ this.addMatchers({
toBeClosed: function() { toBeClosed: function() {
var typeaheadEl = findDropDown(this.actual); var typeaheadEl = findDropDown(this.actual);
this.message = function() { this.message = function() {
return "Expected '" + angular.mock.dump(this.actual) + "' to be closed."; return "Expected '" + angular.mock.dump(this.actual) + "' to be closed.";
}; };
return typeaheadEl.css('display')==='none' && findMatches(this.actual).length === 0; return typeaheadEl.css('display')==='none' && findMatches(this.actual).length === 0;
}, toBeOpenWithActive: function(noOfMatches, activeIdx) { }, toBeOpenWithActive: function(noOfMatches, activeIdx) {
var typeaheadEl = findDropDown(this.actual); var typeaheadEl = findDropDown(this.actual);
var liEls = findMatches(this.actual); var liEls = findMatches(this.actual);
this.message = function() { this.message = function() {
return "Expected '" + angular.mock.dump(this.actual) + "' to be opened."; return "Expected '" + angular.mock.dump(this.actual) + "' to be opened.";
}; };
return typeaheadEl.css('display')==='block' && liEls.length === noOfMatches && $(liEls[activeIdx]).hasClass('active'); return typeaheadEl.css('display')==='block' && liEls.length === noOfMatches && $(liEls[activeIdx]).hasClass('active');
} }
}); });
}); });
//coarse grained, "integration" tests //coarse grained, "integration" tests
describe('initial state and model changes', function () { describe('initial state and model changes', function () {
it('should be closed by default', function () { it('should be closed by default', function () {
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source'></div>"); var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source'></div>");
expect(element).toBeClosed(); expect(element).toBeClosed();
}); });
it('should correctly render initial state if the "as" keyword is used', function () { it('should correctly render initial state if the "as" keyword is used', function () {
$scope.states = [{code: 'AL', name: 'Alaska'}, {code: 'CL', name: 'California'}]; $scope.states = [{code: 'AL', name: 'Alaska'}, {code: 'CL', name: 'California'}];
$scope.result = $scope.states[0]; $scope.result = $scope.states[0];
var element = prepareInputEl("<div><input ng-model='result' typeahead='state as state.name for state in states'></div>"); var element = prepareInputEl("<div><input ng-model='result' typeahead='state as state.name for state in states'></div>");
var inputEl = findInput(element); var inputEl = findInput(element);
expect(inputEl.val()).toEqual('Alaska'); expect(inputEl.val()).toEqual('Alaska');
}); });
it('should not get open on model change', function () { it('should not get open on model change', function () {
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source'></div>"); var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source'></div>");
$scope.$apply(function(){ $scope.$apply(function(){
$scope.result = 'foo'; $scope.result = 'foo';
}); });
expect(element).toBeClosed(); expect(element).toBeClosed();
}); });
}); });
describe('basic functionality', function () { describe('basic functionality', function () {
it('should open and close typeahead based on matches', 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>"); var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source | filter:$viewValue'></div>");
changeInputValueTo(element, 'ba'); changeInputValueTo(element, 'ba');
expect(element).toBeOpenWithActive(2, 0); expect(element).toBeOpenWithActive(2, 0);
}); });
it('should not open typeahead if input value smaller than a defined threshold', function () { 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>"); var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source | filter:$viewValue' typeahead-min-length='2'></div>");
changeInputValueTo(element, 'b'); changeInputValueTo(element, 'b');
expect(element).toBeClosed(); expect(element).toBeClosed();
}); });
it('should support custom model selecting function', function () { it('should support custom model selecting function', function () {
$scope.updaterFn = function(selectedItem) { $scope.updaterFn = function(selectedItem) {
return 'prefix' + selectedItem; return 'prefix' + selectedItem;
}; };
var element = prepareInputEl("<div><input ng-model='result' typeahead='updaterFn(item) as item for item in source | filter:$viewValue'></div>"); var element = prepareInputEl("<div><input ng-model='result' typeahead='updaterFn(item) as item for item in source | filter:$viewValue'></div>");
changeInputValueTo(element, 'f'); changeInputValueTo(element, 'f');
triggerKeyDown(element, 13); triggerKeyDown(element, 13);
expect($scope.result).toEqual('prefixfoo'); expect($scope.result).toEqual('prefixfoo');
}); });
it('should support custom label rendering function', function () { it('should support custom label rendering function', function () {
$scope.formatterFn = function(sourceItem) { $scope.formatterFn = function(sourceItem) {
return 'prefix' + sourceItem; return 'prefix' + sourceItem;
}; };
var element = prepareInputEl("<div><input ng-model='result' typeahead='item as formatterFn(item) for item in source | filter:$viewValue'></div>"); var element = prepareInputEl("<div><input ng-model='result' typeahead='item as formatterFn(item) for item in source | filter:$viewValue'></div>");
changeInputValueTo(element, 'fo'); changeInputValueTo(element, 'fo');
var matchHighlight = findMatches(element).find('a').html(); var matchHighlight = findMatches(element).find('a').html();
expect(matchHighlight).toEqual('prefix<strong>fo</strong>o'); expect(matchHighlight).toEqual('prefix<strong>fo</strong>o');
}); });
it('should by default bind view value to model even if not part of matches', function () { 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>"); var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source | filter:$viewValue'></div>");
changeInputValueTo(element, 'not in matches'); changeInputValueTo(element, 'not in matches');
expect($scope.result).toEqual('not in matches'); expect($scope.result).toEqual('not in matches');
}); });
it('should support the editable property to limit model bindings to matches only', function () { 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>"); 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'); changeInputValueTo(element, 'not in matches');
expect($scope.result).toEqual(undefined); expect($scope.result).toEqual(undefined);
}); });
it('should bind loading indicator expression', inject(function ($timeout) { it('should bind loading indicator expression', inject(function ($timeout) {
$scope.isLoading = false; $scope.isLoading = false;
$scope.loadMatches = function(viewValue) { $scope.loadMatches = function(viewValue) {
return $timeout(function() { return [];}, 1000); return $timeout(function() { return [];}, 1000);
}; };
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in loadMatches()' typeahead-loading='isLoading'></div>"); var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in loadMatches()' typeahead-loading='isLoading'></div>");
changeInputValueTo(element, 'foo'); changeInputValueTo(element, 'foo');
expect($scope.isLoading).toBeTruthy(); expect($scope.isLoading).toBeTruthy();
$timeout.flush(); $timeout.flush();
expect($scope.isLoading).toBeFalsy(); expect($scope.isLoading).toBeFalsy();
})); }));
}); });
describe('selecting a match', function () { describe('selecting a match', function () {
it('should select a match on enter', 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 element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source | filter:$viewValue'></div>");
var inputEl = findInput(element); var inputEl = findInput(element);
changeInputValueTo(element, 'b'); changeInputValueTo(element, 'b');
triggerKeyDown(element, 13); triggerKeyDown(element, 13);
expect($scope.result).toEqual('bar'); expect($scope.result).toEqual('bar');
expect(inputEl.val()).toEqual('bar'); expect(inputEl.val()).toEqual('bar');
}); });
it('should select a match on tab', function () { 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 element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source | filter:$viewValue'></div>");
var inputEl = findInput(element); var inputEl = findInput(element);
changeInputValueTo(element, 'b'); changeInputValueTo(element, 'b');
triggerKeyDown(element, 9); triggerKeyDown(element, 9);
expect($scope.result).toEqual('bar'); expect($scope.result).toEqual('bar');
expect(inputEl.val()).toEqual('bar'); expect(inputEl.val()).toEqual('bar');
}); });
it('should select match on click', function () { 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 element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source | filter:$viewValue'></div>");
var inputEl = findInput(element); var inputEl = findInput(element);
changeInputValueTo(element, 'b'); changeInputValueTo(element, 'b');
var match = $(findMatches(element)[1]).find('a')[0]; var match = $(findMatches(element)[1]).find('a')[0];
$(match).click(); $(match).click();
$scope.$digest(); $scope.$digest();
expect($scope.result).toEqual('baz'); expect($scope.result).toEqual('baz');
expect(inputEl.val()).toEqual('baz'); expect(inputEl.val()).toEqual('baz');
}); });
it('should correctly update inputs value on mapping where label is not derived from the model', function () { 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'}]; $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 element = prepareInputEl("<div><input ng-model='result' typeahead='state.code as state.name for state in states | filter:$viewValue'></div>");
var inputEl = findInput(element); var inputEl = findInput(element);
changeInputValueTo(element, 'Alas'); changeInputValueTo(element, 'Alas');
triggerKeyDown(element, 13); triggerKeyDown(element, 13);
expect($scope.result).toEqual('AL'); expect($scope.result).toEqual('AL');
expect(inputEl.val()).toEqual('Alaska'); expect(inputEl.val()).toEqual('Alaska');
}); });
}); });
describe('regressions tests', function () { describe('regressions tests', function () {
it('issue 231 - closes matches popup on click outside typeahead', 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 element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source | filter:$viewValue'></div>");
var inputEl = findInput(element); var inputEl = findInput(element);
changeInputValueTo(element, 'b'); changeInputValueTo(element, 'b');
$document.find('body').click(); $document.find('body').click();
$scope.$digest(); $scope.$digest();
expect(element).toBeClosed(); expect(element).toBeClosed();
}); });
}); });
}); });
}); });
+236 -236
View File
@@ -1,237 +1,237 @@
angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position'])
/** /**
* A helper service that can parse typeahead's syntax (string provided by users) * A helper service that can parse typeahead's syntax (string provided by users)
* Extracted to a separate service for ease of unit testing * Extracted to a separate service for ease of unit testing
*/ */
.factory('typeaheadParser', ['$parse', function ($parse) { .factory('typeaheadParser', ['$parse', function ($parse) {
// 00000111000000000000022200000000000000003333333333333330000000000044000 // 00000111000000000000022200000000000000003333333333333330000000000044000
var TYPEAHEAD_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/; var TYPEAHEAD_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/;
return { return {
parse:function (input) { parse:function (input) {
var match = input.match(TYPEAHEAD_REGEXP), modelMapper, viewMapper, source; var match = input.match(TYPEAHEAD_REGEXP), modelMapper, viewMapper, source;
if (!match) { if (!match) {
throw new Error( throw new Error(
"Expected typeahead specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_'" + "Expected typeahead specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_'" +
" but got '" + input + "'."); " but got '" + input + "'.");
} }
return { return {
itemName:match[3], itemName:match[3],
source:$parse(match[4]), source:$parse(match[4]),
viewMapper:$parse(match[2] || match[1]), viewMapper:$parse(match[2] || match[1]),
modelMapper:$parse(match[1]) modelMapper:$parse(match[1])
}; };
} }
}; };
}]) }])
.directive('typeahead', ['$compile', '$parse', '$q', '$document', '$position', 'typeaheadParser', function ($compile, $parse, $q, $document, $position, typeaheadParser) { .directive('typeahead', ['$compile', '$parse', '$q', '$document', '$position', 'typeaheadParser', function ($compile, $parse, $q, $document, $position, typeaheadParser) {
var HOT_KEYS = [9, 13, 27, 38, 40]; var HOT_KEYS = [9, 13, 27, 38, 40];
return { return {
require:'ngModel', require:'ngModel',
link:function (originalScope, element, attrs, modelCtrl) { link:function (originalScope, element, attrs, modelCtrl) {
var selected; var selected;
//minimal no of characters that needs to be entered before typeahead kicks-in //minimal no of characters that needs to be entered before typeahead kicks-in
var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1; var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1;
//expressions used by typeahead //expressions used by typeahead
var parserResult = typeaheadParser.parse(attrs.typeahead); var parserResult = typeaheadParser.parse(attrs.typeahead);
//should it restrict model values to the ones selected from the popup only? //should it restrict model values to the ones selected from the popup only?
var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false; var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false;
var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop; var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop;
//pop-up element used to display matches //pop-up element used to display matches
var popUpEl = angular.element( var popUpEl = angular.element(
"<typeahead-popup " + "<typeahead-popup " +
"matches='matches' " + "matches='matches' " +
"active='activeIdx' " + "active='activeIdx' " +
"select='select(activeIdx)' "+ "select='select(activeIdx)' "+
"query='query' "+ "query='query' "+
"position='position'>"+ "position='position'>"+
"</typeahead-popup>"); "</typeahead-popup>");
//create a child scope for the typeahead directive so we are not polluting original scope //create a child scope for the typeahead directive so we are not polluting original scope
//with typeahead-specific data (matches, query etc.) //with typeahead-specific data (matches, query etc.)
var scope = originalScope.$new(); var scope = originalScope.$new();
originalScope.$on('$destroy', function(){ originalScope.$on('$destroy', function(){
scope.$destroy(); scope.$destroy();
}); });
var resetMatches = function() { var resetMatches = function() {
scope.matches = []; scope.matches = [];
scope.activeIdx = -1; scope.activeIdx = -1;
}; };
var getMatchesAsync = function(inputValue) { var getMatchesAsync = function(inputValue) {
var locals = {$viewValue: inputValue}; var locals = {$viewValue: inputValue};
isLoadingSetter(originalScope, true); isLoadingSetter(originalScope, true);
$q.when(parserResult.source(scope, locals)).then(function(matches) { $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 //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 //but we are interested only in responses that correspond to the current view value
if (inputValue === modelCtrl.$viewValue) { if (inputValue === modelCtrl.$viewValue) {
if (matches.length > 0) { if (matches.length > 0) {
scope.activeIdx = 0; scope.activeIdx = 0;
scope.matches.length = 0; scope.matches.length = 0;
//transform labels //transform labels
for(var i=0; i<matches.length; i++) { for(var i=0; i<matches.length; i++) {
locals[parserResult.itemName] = matches[i]; locals[parserResult.itemName] = matches[i];
scope.matches.push({ scope.matches.push({
label: parserResult.viewMapper(scope, locals), label: parserResult.viewMapper(scope, locals),
model: matches[i] model: matches[i]
}); });
} }
scope.query = inputValue; scope.query = inputValue;
//position pop-up with matches - we need to re-calculate its position each time we are opening a window //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 //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 //due to other elements being rendered
scope.position = $position.position(element); scope.position = $position.position(element);
scope.position.top = scope.position.top + element.prop('offsetHeight'); scope.position.top = scope.position.top + element.prop('offsetHeight');
} else { } else {
resetMatches(); resetMatches();
} }
isLoadingSetter(originalScope, false); isLoadingSetter(originalScope, false);
} }
}, function(){ }, function(){
resetMatches(); resetMatches();
isLoadingSetter(originalScope, false); isLoadingSetter(originalScope, false);
}); });
}; };
resetMatches(); resetMatches();
//we need to propagate user's query so we can higlight matches //we need to propagate user's query so we can higlight matches
scope.query = undefined; scope.query = undefined;
//plug into $parsers pipeline to open a typeahead on view changes initiated from DOM //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 //$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue
modelCtrl.$parsers.push(function (inputValue) { modelCtrl.$parsers.push(function (inputValue) {
resetMatches(); resetMatches();
if (selected) { if (selected) {
return inputValue; return inputValue;
} else { } else {
if (inputValue && inputValue.length >= minSearch) { if (inputValue && inputValue.length >= minSearch) {
getMatchesAsync(inputValue); getMatchesAsync(inputValue);
} }
} }
return isEditable ? inputValue : undefined; return isEditable ? inputValue : undefined;
}); });
modelCtrl.$render = function () { modelCtrl.$render = function () {
var locals = {}; var locals = {};
locals[parserResult.itemName] = selected || modelCtrl.$viewValue; locals[parserResult.itemName] = selected || modelCtrl.$viewValue;
element.val(parserResult.viewMapper(scope, locals) || modelCtrl.$viewValue); element.val(parserResult.viewMapper(scope, locals) || modelCtrl.$viewValue);
selected = undefined; selected = undefined;
}; };
scope.select = function (activeIdx) { scope.select = function (activeIdx) {
//called from within the $digest() cycle //called from within the $digest() cycle
var locals = {}; var locals = {};
locals[parserResult.itemName] = selected = scope.matches[activeIdx].model; locals[parserResult.itemName] = selected = scope.matches[activeIdx].model;
modelCtrl.$setViewValue(parserResult.modelMapper(scope, locals)); modelCtrl.$setViewValue(parserResult.modelMapper(scope, locals));
modelCtrl.$render(); modelCtrl.$render();
}; };
//bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27) //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27)
element.bind('keydown', function (evt) { element.bind('keydown', function (evt) {
//typeahead is open and an "interesting" key was pressed //typeahead is open and an "interesting" key was pressed
if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) { if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) {
return; return;
} }
evt.preventDefault(); evt.preventDefault();
if (evt.which === 40) { if (evt.which === 40) {
scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length;
scope.$digest(); scope.$digest();
} else if (evt.which === 38) { } else if (evt.which === 38) {
scope.activeIdx = (scope.activeIdx ? scope.activeIdx : scope.matches.length) - 1; scope.activeIdx = (scope.activeIdx ? scope.activeIdx : scope.matches.length) - 1;
scope.$digest(); scope.$digest();
} else if (evt.which === 13 || evt.which === 9) { } else if (evt.which === 13 || evt.which === 9) {
scope.$apply(function () { scope.$apply(function () {
scope.select(scope.activeIdx); scope.select(scope.activeIdx);
}); });
} else if (evt.which === 27) { } else if (evt.which === 27) {
evt.stopPropagation(); evt.stopPropagation();
resetMatches(); resetMatches();
scope.$digest(); scope.$digest();
} }
}); });
$document.bind('click', function(){ $document.bind('click', function(){
resetMatches(); resetMatches();
scope.$digest(); scope.$digest();
}); });
element.after($compile(popUpEl)(scope)); element.after($compile(popUpEl)(scope));
} }
}; };
}]) }])
.directive('typeaheadPopup', function () { .directive('typeaheadPopup', function () {
return { return {
restrict:'E', restrict:'E',
scope:{ scope:{
matches:'=', matches:'=',
query:'=', query:'=',
active:'=', active:'=',
position:'=', position:'=',
select:'&' select:'&'
}, },
replace:true, replace:true,
templateUrl:'template/typeahead/typeahead.html', templateUrl:'template/typeahead/typeahead.html',
link:function (scope, element, attrs) { link:function (scope, element, attrs) {
scope.isOpen = function () { scope.isOpen = function () {
return scope.matches.length > 0; return scope.matches.length > 0;
}; };
scope.isActive = function (matchIdx) { scope.isActive = function (matchIdx) {
return scope.active == matchIdx; return scope.active == matchIdx;
}; };
scope.selectActive = function (matchIdx) { scope.selectActive = function (matchIdx) {
scope.active = matchIdx; scope.active = matchIdx;
}; };
scope.selectMatch = function (activeIdx) { scope.selectMatch = function (activeIdx) {
scope.select({activeIdx:activeIdx}); scope.select({activeIdx:activeIdx});
}; };
} }
}; };
}) })
.filter('typeaheadHighlight', function() { .filter('typeaheadHighlight', function() {
function escapeRegexp(queryToEscape) { function escapeRegexp(queryToEscape) {
return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1"); return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1");
} }
return function(matchItem, query) { return function(matchItem, query) {
return query ? matchItem.replace(new RegExp(escapeRegexp(query), 'gi'), '<strong>$&</strong>') : query; return query ? matchItem.replace(new RegExp(escapeRegexp(query), 'gi'), '<strong>$&</strong>') : query;
}; };
}); });
+4 -4
View File
@@ -1,5 +1,5 @@
<div class="accordion-group"> <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-heading" ><a class="accordion-toggle" ng-click="isOpen = !isOpen" accordion-transclude="heading">{{heading}}</a></div>
<div class="accordion-body" collapse="!isOpen"> <div class="accordion-body" collapse="!isOpen">
<div class="accordion-inner" ng-transclude></div> </div> <div class="accordion-inner" ng-transclude></div> </div>
</div> </div>
+4 -4
View File
@@ -1,5 +1,5 @@
<ul class="typeahead dropdown-menu" ng-style="{display: isOpen()&&'block' || 'none', top: position.top+'px', left: position.left+'px'}"> <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)"> <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> <a tabindex="-1" ng-click="selectMatch($index)" ng-bind-html-unsafe="match.label | typeaheadHighlight:query"></a>
</li> </li>
</ul> </ul>